com.android.tools.r8.ApiLevelException: Invoke-customs are only supported starting with Android O

原因是某些第三方库使用了Java 1.8API,导致整个项目必须使用Java 1.8

解决方法就是增加如下编译命令

完整的例子如下:

参考链接


Android 6.0/10.0扫描不到Ble设备需开启位置权限

蓝牙设备扫描需要定位权限的原因在于蓝牙定位的存在,蓝牙定位属于精确定位,因此需要开启精确定位权限,而精确定位跟GPS又是直接相关的,因此才会导致GPS权限需要获取。

项目中需要用到android Ble蓝牙4.0开发技术,于是开启了蓝牙填坑之旅,说实话,蓝牙开发坑真多,跳出一个又进入下一个,每次遇到 问题,就觉得不可能解决了,还好在自己的摸索中,都一一的化解了,以此来记录安卓蓝牙开发的心得。

     接手的蓝牙开发项目,原来的同事已经写好,不用再去写,开始也就大概看了看android蓝牙开发相关资料,对比项目中的蓝牙开发代码,发现发现搜索、连接蓝牙有一样的处理也有不一样的处理,想着项目不一样,代码处理肯定不一样,于是就没去深究,毕竟项目运行起来使用正常,然而,没过几天,客户提出蓝牙搜索太慢,然后就去调试,先看代码是搜索蓝牙8秒钟才显示搜索到的蓝牙设备列表,查看日志,其实不到8秒就搜索到外围设备,只是搜索8秒后停止扫描才显示蓝牙设备,那就把显示成3、4秒呗。
        正这样想,同事拿来一部荣耀8,装的APP怎么就是搜不到蓝牙设备,然后调试其他手机,都能搜索到,于是就纳闷了,荣耀8怎么就是搜索不到蓝牙设备呢,其他手机都能,再看一遍代码,也没有专门针对荣耀8处理的地方,再想,荣耀8是系统7.0的手机,是不是跟权限没开有关呢,对比其他华为手机,申请的蓝牙权限也都开着,荣耀8就是搜索不到,是不是荣耀8手机有问题,同事告知下载一个第三方蓝牙调试工具查查看,令人疑惑的是荣耀8用第三方蓝牙调试工具立刻就能扫描显示出来,试了几次都是立刻扫描出来,当时心想,我靠,什么鬼,这么迅速就能把蓝牙设备扫描显示出来,领导也说,别人的调试工具都能做这么快,你也得做这么快,想着目前的代码都是几秒后才能搜索到,目前的荣耀8还搜索不出来,客户那边还一个劲儿的给老板吐槽太慢,于是压力剧增,真正开始了蓝牙开发不归之旅。
     但是再考虑索慢时,领导说,得把荣耀8搜索不到的问题先解决,因为荣耀8是项目实施人员给客户安装硬件调试数据要用的,那就先把搜索慢暂时放一放,想着一边搜索慢没还没着落,荣耀8手机就是搜索不到,想想就头大,查找荣耀8为什么搜索不到蓝牙设备的资料,也没发现有效的办法,这时,用荣耀手机的人说,另一个公司的项目app(简称B吧,荣耀8搜索不到蓝牙设备的项目简称A吧)就能正常搜索到蓝牙设备,一打开调试还真是每次都能搜索到,看了一下两个项目的代码:

      两个项目中的蓝牙适配器的获取,去发现,动态注册广播、广播接收蓝牙搜索结果、蓝牙连接都一样,至于手机是否支持ble4.0,蓝牙是否开启的逻辑,连接上对数据的处理、断开mBluetoothGatt.disconnect(),关闭mBluetoothGatt.close() 的逻辑网上有很多,我这里就不在详说;
       经过2个项目对比,发现就蓝牙搜索多久停止不一样,荣耀8能搜索到蓝牙设备的时间项目代码设置是10秒,而搜索不到的项目是设置8秒,带着半信半疑把项目A的蓝牙搜索时间由8秒改成10秒,运行,奇迹还真是出现了,荣耀8可以正常搜索到蓝牙设备了,反复测了好多次都能正常搜索到蓝牙设备,我是不信邪的人,还改成8秒试试,果然还是搜索不到。
     于是和领导说,你看荣耀8必须10秒才能搜索 到蓝牙设备,搜索到的蓝牙设备列表不晚点展示不行啊,领导自然 是不同意,拿出第三方蓝牙提哦啊是工具说,那第三方就能很快搜索到,你想想其他法吧,想着有的手机快,有的手机慢,提出意见,能不能搜索到一个设备就展示到列表,不必等10秒后再展示,这样体验好些,反正领导说不行,既然领导说不行,那就回去研究吧。
    查了很多蓝牙搜索慢怎么办?看了半天,当然这期间调试过各种其他方法,都没用,功夫不负有心人,其中一个博客说:
btAdapt.startDiscovery();搜索返回的结果慢, 现在都用这个 btAdapt.startLeScan(getmLeScanCallback); 其他也没多说什么,喜出望外,这不是正是我要的吗,于是改蓝牙搜索,上代码:

            经过这样修改之后,奇迹又出现了,蓝牙搜索到确实快了很多,一秒左右甚至不到一秒就把蓝牙设备搜索出来了, 真是令人喜出望外,想起华为荣耀8那么奇葩,不知这个方法在华为荣耀8上测试如何,然而,2秒,4秒,8秒,10秒,20秒都在华为荣耀8搜索不出来,心又凉了半截,暗想这个方法看来还不适用,又白整了,于是又查各种资料,有的说android系统6.0以上的手机需要定位权限,低头一想,还真是,一直测试的手机是android系统6.0以下的,华为荣耀8是7.0以上的,抱着试试的态度,申请权限,还真是努力出奇迹,定位权限允许后,华为荣耀8确实也在不到一秒将蓝牙设备搜索出来,测试了其他6.0以上的手机,都正常很快搜出来。
        测试没有其他问题,打包发布,然而客户不给定位权限,问公司怎么一点搜索不出来,告知需要允许定位,才能搜索到,客户觉得一个蓝牙搜索你跟我要定位,很是不理解,老板也认为这样的体验也不好,客户不给权限,就无法使用,非常不友好。告知领导6.0以上必须给定位权限,否则搜不到, 这个没法啊,领导说你看第三方的蓝牙调试工具,在6.0以上的手机,不用打开定位,而且搜索的更快,第三方都能做到,你想办法吧, 我当然不苟同,下载了几个第三方蓝牙调试工具,有的确实要定位权限不允许定位就无法搜索到,有的确实禁止定位权限也能快速搜索到蓝牙设备,这么啪啪打脸,只能埋头去想办法了,同时,为了保证程序兼容性,公司又买了几部6.0以上的安卓手机,让我更惊心的是,魅族6和华为畅享7不仅要允许定位权限,GPS定位还要打开,GPS不打开,也是搜索不到蓝牙设备,想起以前开发的项目,由于安卓碎片化,不同的手机厂商对定位,GPS管理不一样,有的打开定位权限GPS权限也给了,有的打开GPS定位权限也给了,有的是他们两个独立,处理起来很麻烦,而蓝牙有的要GPS,有的不要GPS,6.0一下还不需要任何定位权限,想着这处理起来太麻烦,还是安心去找不要定位权限的方法吧,其实当时心里是没底的,虽然第三方已经实现这种不需要定位权限就能搜索到,心想,第三方也不知用的是什么黑科技,于是就查找android系统6.0一上的手机不需要定位权限就能把蓝牙设备搜索出来等相关资料,其中最有帮助的查到 btAdapt.startLeScan(getmLeScanCallback) 这个方法已过时,google已经在api里将其划掉,不推荐使用,推荐使用:

喜出过望,难道是用的方法过时了,所以需要定位权限,上代码:

停止扫描的方法是:

        然而,虽然使用了新方法,速度也是一样快,但是还是需要定位权限,魅族6和华为畅享7还得打开GPS,看来这个也不是正解,当然项目中的扫描外围蓝牙设备的方法也都换成了scanner.startScan(scanCallback);
第一天毫无结果,灰头土脸,昏天暗地,焦头烂额地回家。
第二天继续查找相关资料,在一个偶然的机会,看到一篇博客说,把app中的build.gradle中的:

中的  targetSdkVersion 25改成 targetSdkVersion 22,就不需要定位权限也可以搜索道蓝牙设备了,看到网上很少有人这样说的,搜了那么多资料,就一篇这样说的,带着半信半疑将targetSdkVersion改成 22, 编译,也没报任何错,运行,还真是又出奇迹,测试了手上的所有手机,都不需要定位,不要GPS就可以搜索到蓝牙设备。
       至于为什么会这样,可以查找 android compileSdkVersion、buildToolsVersion,targetSdkVersion之间的区别,网上有很多解释,保证能解答你的疑惑,下面附上一些简单的解释
     targetSDKVersion
    简单来说就代表着你的App能够适配的系统版本,意味着你的App在这个版本的手机上做了充分的 前向 兼容性处理和实际测试。其实我们写代码时都是经常干这么一件事,就是 if(Build.VERSION.SDK_INT >= 23) { ... } ,这就是兼容性处理最典型的一个例子。如果你的target设置得越高,其实调用系统提供的API时,所得到的处理也是不一样的,甚至有些新的API是只有新的系统才有的Android6.0普通权限normal permission 和 危险权限dangerous permission 。
 Normal Permission:写在xml文件里,那么App安装时就会默认获得这些权限,即使是在Android6.0系统的手机上,用户也无法在安装后动态取消这些normal权限,这和以前的权限系统是一样的,不变。
Dangerous Permission:还是得写在xml文件里,但是App安装时具体如果执行授权分以下几种情况:
1、targetSDKVersion < 23 & API(手机系统) < 6.0 :安装时默认获得权限,且用户无法在安装App之后取消权限。
 2、targetSDKVersion < 23 & API(手机系统) >= 6.0 :安装时默认获得权限,但是用户可以在安装App完成后动态取消授权( 取消时手机会弹出提醒,告诉用户这个是为旧版手机打造的应用,让用户谨慎操作 )。
3、targetSDKVersion >= 23 & API(手机系统) < 6.0 :安装时默认获得权限,且用户无法在安装App之后取消权限。
 4、targetSDKVersion >= 23 & API(手机系统) >= 6.0 :安装时不会获得权限,可以在运行时向用户申请权限。用户授权以后仍然可以在设置界面中取消授权,用户主动在设置界面取消后,在app运行过程中可能会出现crash。
其他:
        1.虽然解决了大问题,但是目前项目中发现,连续扫描-断开-扫描四五次,就无法再搜索到外围蓝牙设备,尤其是在每次搜索1-2秒内扫描断开,4、5次无论如何也是搜索不到,需要重新进入界面才能扫描到,每次扫描花费在4、5秒以上的,出现4、5次扫不到的几率就很低了,当然,4、5秒是老板和客户无法忍受的。
2.蓝牙连接成功自动断开;

参考链接


Android dp 和 CSS px

最近在给WebView前端传递标题栏高度的时候,传递的是实际像素Pixel,结果前端直接设置这个数值的时候,发现太高了。搜索了一下,发现前端的CSS样式设置的像素值是逻辑像素,这个逻辑像素跟实际像素是不同的,对于Android来说,就是设备的dp数值。

参考文章如下:

前情提要:设计师给的设计稿是1242分辨率的(iPhone6p),标注的字体大小是54px,前端工程师写的H5页面是18px,效果正常且能自适应iPhone6。来调查一下 为什么是标注数值除以3?为什么自动适应iPhone6?css里的px和Android的dp有怎样的关系?

在进行具体的分析之前,首先得知道下面这些关键性基本概念(术语)。

物理像素(physical pixel)

一个物理像素是显示器(手机屏幕)上最小的物理显示单元,在操作系统的调度下,每一个设备像素都有自己的颜色值和亮度值。

设备独立像素(density-independent pixel)

设备独立像素(也叫密度无关像素),可以认为是计算机坐标系统中得一个点,这个点代表一个可以由程序使用的虚拟像素(比如: css像素),然后由相关系统转换为物理像素。

所以说,物理像素和设备独立像素之间存在着一定的对应关系,这就是接下来要说的设备像素比

设备像素比(device pixel ratio)

设备像素比(简称dpr)定义了物理像素和设备独立像素的对应关系,它的值可以按如下的公式的得到:

在javascript中,可以通过window.devicePixelRatio获取到当前设备的dpr。

在css中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)。


iPhone6的物理像素是750 x 1334,设备像素比(devicePixelRatio)是2,设备独立像素是375 x 667 (750/2 x 1334/2)

iPhone6p的物理像素是1080 x 1920,但logic pixel是1242 x 2208, 设备像素比(devicePixelRatio)是3,设备独立像素是414 x 736 (1242/3 x 2208/3)

css里面有物理像素逻辑像素的概念,在这里,逻辑像素可以理解为设备独立像素

其实,logic point更适合翻译成逻辑点。无论是逻辑像素还是逻辑点,只需要理解一个逻辑点(逻辑像素)需要一个或一个以上的物理像素来展示就可以了。

那么,为什么iPhone6p是这样奇葩的逻辑点呢?

  • 如果逻辑点分辨率用 360x640, 360x640@3x 正好是 1080x1920。但是逻辑pt分辨率 360x640 就会比 iPhone 6的 375x667 还低,也就是说相同字号的情况下,iPhone 6如果一行显示了25个字,而 iPhone 6 Plus按这个逻辑pt方案,一行就会只能显示24 个字了。
  • 如果逻辑点分辨率用 540x960,540x960@2x 正好是 1080x1920。但是iOS UI 元素尺寸在屏幕上的实际物理面积一下子就变小了,比如标签栏或导航栏按钮的物理高度只有原来的 81.5% ,点击面积就只有iPhone 6 的0.815*0.815=66.4%,用户点击就困难了。
  • 如果物理分辨率做到1242x2208就减少了一个从1242压缩到1080的过程,但是1242的物理分辨率是在1080p和2k屏之前的尺寸,功耗和成本都会提升,而且这样非1440的2k分辨率的屏幕采购也是问题。

至于为什么一定是 414x736,有人估计应该是在 5.5inch 和 ppi=461 这两个前提限定的情况下,按这个 414x736 pt 分辨率,屏幕上 UI 元素操作物理大小最接近 iPhone 6上的表现。


Android世界中更加复杂,这里列举了几款手机的基本数据

  Nexus 4 Nexus 6 Nexus 6p LG L24 Mi 4c
物理像素 768 x 1280 1440 x 2560 1440 x 2560 1440 x 2560 1080 x 1920
设备像素比(dpr) 2 3.5 3.3 4 3
设备独立像素(dip/dp) 384 x 640 412 x 732 435 x 773 360 x 640 360 x 640

Android提供了获取density的方法:

density的官方定义是The logical density of the display,翻译过来是屏幕的逻辑密度,其实就是之前提到的设备像素比(dpr)


在浏览器中,获取设备像素比就比较简单:

比如,我写了一个网页,用手机浏览器打开:

结果显示与Android的density的结果是一致的。

注:<meta name="viewport" content="width=device-width, initial-scale=1">
By specifying width=”device-width”, you are asking the browser to apply a scaling factor to its screen pixels. One css pixel occupies one or more screen pixels. How many more? This value is called the css pixel ratio.


回顾一下

同一台设备,它的设备像素比(dpr)是确定的,无论通过Android api 获取还是浏览器 js/css 获取。
The css px is actually Device Independent Pixel(dip), and it is a common pattern to use px as dp in css.
css中的px具有与Android中的dp等同的效果

参考链接


Centos 7 配置Jenkins构建Android持续集成(离线环境)

安装配置Jenkins

为了安全考虑,首先需要解锁Jenkins

继续阅读Centos 7 配置Jenkins构建Android持续集成(离线环境)

Gradle传递System Property

最近在搭建Jenkins环境,实现Android自动化编译的过程中,由于内网服务器不能访问外网,因此只能配置Robolectric访问内网的服务器。

根据官方文档 Configuring Robolectric,发现如果要更改Robolectric默认的下载服务器链接地址,需要在项目的每个lib库中都配置如下参数,才能实现:

但是这样配置有一个问题,就是没办法动态调整链接地址。

而我们使用

手工指定的参数,并没有在编译的时候生效。

参数丢失的原因是因为测试用例在新的JVM中执行,传入的参数不会自动带给新创建的JVM

这时需要在Gradle脚本中将读到的值重新设到系统属性里面,才可以被Java程序读到。

每个项目的build.gradle中都增加上面的配置之后,就可以保证在外部编译的时候动态指定参数了。

参考链接


warn: multiple substitutions specified in non-positional format; did you mean to add the formatted="false" attribute?

编辑strings.xml的时候

或者

提示下面的错误

出现这个错误的原因主要是因为strings字串中包含百分号(%),

有几种方式解决

1.用两个百分号表示一个百分号即

2.用转义符表示

二:无需格式化

根据错误提示可知,如果字符串无需格式化,可在<string> 标签上增加属性:formatted="false",即

三:关于上面问题的延伸

如果你需要使用String.format(String, Object...) 来格式化你的字符串,你可以把格式化参数放在你的字符串中,参见下面的例子:

在这个例子中,这个格式化的字符串有2个参数, %1$s是个字符串%2$d是个浮点数,你可以在你的程序中按照下面的方法来根据参数来格式化字符串:

目前AndroidSDK已经支持直接格式化字符串,不需要使用String.format(String, Object...) ,如下

那么根据例子上说的我需要把%s换成%1$s才行了,修改后编译通过,程序成功启动。

参考链接


Robolectric单元测试报错“org.mockito.exceptions.base.MockitoException Caused by: java.lang.ClassCastException”

使用Robolectric进行Android代码测试的时候,随着测试用例的增多,可能会报告如下错误(Windows下常见):

原因为Mockto使用了编译缓存导致加载类的时候出现异常。解决方法是禁止Mockto缓存测试类的代码。

Android测试项目的src/test/java下创建一个名为org.mockito.configuration的包,然后实现一个名为MockitoConfiguration.java的类,如下:

这样当再次执行测试用例的时候,就已经不使用缓存了。

参考链接


Android WebView/X5截长图解决方案

  1. 普通WebView如何截取长图
  2. 针对X5内核中WebView如何截取长图

日常开发中,遇到为WebView截取长图算是一种常见的需求。网上聪明的程序员们提供了多种截取WebView长图的方法,这为我们的开发提供了很多便利。现在,也有很多APP是集成了X5内核的,网上对于X5内核的截长图方案介绍比较少,所以这里我整理了对WebView截取长图的比较通用可行的方法,并且对使用了x5内核的WebView的截图方法进行分享。

普通WebView截长图方案

普通WebView截取长图,这里是指项目中没有集成X5内核的情况。利用Google文档上的api可以顺利截图。以Android5.0为版本分界线,截图采用不同的处理方式。

1. Android5.0以下版本

2. Android5.0及以上版本

在Android5.0及以上版本,Android对WebView进行了优化,为了减少内存使用和提高性能,使用WebView加载网页时只绘制显示部分。如果我们不做处理,仍然使用上述代码截图的话,就会出现只截到屏幕内显示的WebView内容,其它部分是空白的情况。
这时候,我们通过调用WebView.enableSlowWholeDocumentDraw()方法可以关闭这种优化,但要注意的是,该方法需要在WebView实例被创建前就要调用,否则没有效果。

另外这个方法一旦开启,会影响到整个进程中的WebView实例,并且没有办法关闭。

这个代码的本质是设置了一个全局变量,并且没有提供关闭接口。其真实调用的代码如下:

我们在WebView实例被创建前加入代码:

另外,当应用存在多个进程的时候,比如消息推送进程,LBS定位进程存在的情况下,务必确保只在主进程中初始化这个设置,否则运行时可能报错。

根据Google文档中描述,capturePicture()方法已不鼓励使用,推荐我们通过webViewonDraw(Canvas)去获取图像,所以这里我们去拿到网页的宽高后,就调用webView.draw(Canvas)方法生成webView截图。

X5内核截取长图

使用X5内核截取长图有两种方法,并且都可以不用考虑版本问题,这为我们提供了方便。在X5内核下,如果使用WebViewonDraw(Canvas)方法,会出现或多或少的问题,所以对这个方法弃坑了。以下是两个截图方法:

1. 使用X5内核方法snapshotWholePage(Canvas, boolean, boolean)

X5内核中提供了一个截取整个WebView界面的方法snapshotWholePage(Canvas, boolean, boolean),但是这个方法有个缺点,就是不以屏幕上WebView的宽高截图,只是以WebViewcontentWidthcontentHeight为宽高截图,所以截出来的图片会不怎么清晰,但作为缩略图效果还是不错了。

2. 使用capturePicture()截取清晰长图

如果想要在X5内核下截到清晰的长图,不能使用snapshotWholePage(),依然可以采用capturePicture()。X5内核下使用capturePicture()进行截图,可以直接拿到WebView的清晰长图,但这是个Deprecated的方法,使用的时候要做好异常处理。

总结

以上是WebView截长图方法的总结和分享,对X5内核的截图也是尝试了多种途径最后找到满意的解决方案。另外,截长图会占用大量内存,容易触发OOM,所以代码中也要注意对OOM的处理。

在使用了X5内核的项目中,使用WebView截取长图的判断逻辑可以是:

目前(2020/08/01)之前版本的X5 SDK,如果编译APK的时候指定targetSdkVersion版本高于 28(Android O)的情况下,调用snapshotWholePage(Canvas, boolean, boolean)可能会无法获取到截图,图片内容全黑。

观察日志发生如下报错:

原因为从API 29(Android P)开始,Google对于某些反射调用私有方法的行为进行了限制,比如动态反射赋值android.graphics.Canvas.java的私有变量mBitmap。这些调用会被抛出异常阻止。

参考链接


AsyncTask is Deprecated, Now What?

For the past decade, AysncTask has been one of the most widely used solutions for writing concurrent code in Android. However, it earned very controversial reputation. On the one hand, AsyncTask powered, and still powers, many Android applications. On the other hand, most professional Android developers openly dislike this API.

All in all, I’d say that Android community has love-hate relationship with AsyncTask. But there are big news: the era of AsyncTask is about to end because a commit that deprecated it had just landed in Android Open Source Project.

In this post I’ll review the official statement motivating AsyncTask’s deprecation, as well as the real reasons why it had to be deprecated. As you’ll see, these are different sets of reasons. In addition, towards the end of this article, I’ll share my thoughts on the future of Android’s concurrency APIs.

Official Reason for Deprecation of AsyncTask

The official deprecation of AsyncTask, as well as the motivation for that decision, were introduced with this commit. The newly added first paragraph of Javadoc states:

AsyncTask was intended to enable proper and easy use of the UI thread. However, the most common use case was for integrating into UI, and that would cause Context leaks, missed callbacks, or crashes on configuration changes. It also has inconsistent behavior on different versions of the platform, swallows exceptions from doInBackground, and does not provide much utility over using Executors directly.

While that’s the official statement by Google, there are several inaccuracies there which are worth pointing out.

Fist of all, AsyncTask has never been intended to “enable proper and easy use of the UI thread”. It was intended to offload long-running operations from UI thread to background threads, and then deliver the results of these operations back to UI thread. I know, I’m nitpicking here. However, in my opinion, when Google deprecates API that they themselves invented and promoted for years, it would be more respectful towards developers who use this API today, and will continue to use for years to come, to invest more effort into deprecation message to prevent further confusion.

That said, the more interesting part of this deprecation message is this: “that would cause Context leaks, missed callbacks, or crashes on configuration changes”. So, Google basically states that the most common use case for AsyncTask automatically results in very serious problems. However, there are many high-quality applications out there which use AsyncTask and work flawlessly. Even some classes inside AOSP itself use AsyncTask. How comes they don’t experience these problems?

To answer this question, let’s discuss the relationship between AsyncTask and memory leaks in details.

继续阅读AsyncTask is Deprecated, Now What?