android8.0出来后,照旧又是一轮兼容适配,但对比5.x -> 6.x -> 7.x, 这次的升级对于开发者来说没有太大的变化,而且按以往的尿性来看,铺开的速度也不会很快,然后兼容的任务优先级就降级了,然而,最后还是出事了。。。。。
故事发生在一行代码上1
2
3
4...
BitmapDrawable drawable =
(BitmapDrawable)this.getPackageManager().getApplicationIcon(this.getPackageName());
...
代码很简单,就是拿到应用的icon,然后转成BitmapDrawable,再去做其他的事,这个操作在以往的系统上一点毛病都没有,但在8.x上,这个操作去造成了崩溃,原因也很简单,getApplicationIcon时,系统返回的类型不再默认是BitmapDrawable了,而是一种新类型 AdaptiveIconDrawable , 强转时轻易就崩溃了。
这是一个小问题,再说,强转的操作本来就不合理,毕竟人家getApplicationIcon的返回类型就是Drawable,关键是接下来的修复方法。
因为很容易发现,这是因为使用了8.0以上的系统才出现的问题,按照以往的经验来说,一般都是开发时把编译版本(compileSdkVersion)和目标版本(targetSdkVersion)设得过高导致的,那我降一下总行了吧。。于是就降成了23,然而,问题还是没有解决。
这下我才开始重视起来,既然跟编译没关系,那就肯定是跟包体本身有关系了,由于是在getAplicationIcon时触发的,那会不会是因为launch icon的原因,于是去看一下工程结构,果然发现了一个跟api level = 26 相关的文件:
这是一个xml的描述文件
1 |
|
从定义上就可以看出来,这是一个合成的图标,后前后背景,由系统动态合成,这是8.0的一个新特性,在版本差异中,我们可以看到说明
(更多差异可以参考 android差异)
由于我只降低了编译版本和目标版本,这个描述文件还是在本地且会编译到最终的apk里的,而8.0的系统是不会根据你的编译版本和目标版本要选择返回不同的类型,它看到存在这个目录,就默认应用支持AdaptiveIcon的特性了。
原理分析
深入到系统源码里,可以从Context入手。
先从getPackageManager开始,Context的实现类ContextImpl里,获取PackageManager的实现为
(以下全为8.0的代码)
1 | 226 |
返回的是ApplicationPackageManager类型,而ApplicationPackageManager里获取icon的代码如下
1 | 1194 public Drawable getApplicationIcon(ApplicationInfo info) { |
再深入到ApplicationInfo里,发现最终还是交回给了PackageManager自己来加载1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
282450 public Drawable loadItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo) {
2451 Drawable dr = loadUnbadgedItemIcon(itemInfo, appInfo);
2452 if (itemInfo.showUserIcon != UserHandle.USER_NULL) {
2453 return dr;
2454 }
2455 return getUserBadgedIcon(dr, new UserHandle(mContext.getUserId()));
2456 }
2458 /**
2459 * @hide
2460 */
2461 public Drawable loadUnbadgedItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo) {
2462 if (itemInfo.showUserIcon != UserHandle.USER_NULL) {
2463 Bitmap bitmap = getUserManager().getUserIcon(itemInfo.showUserIcon);
2464 if (bitmap == null) {
2465 return UserIcons.getDefaultUserIcon(itemInfo.showUserIcon, /* light= */ false);
2466 }
2467 return new BitmapDrawable(bitmap);
2468 }
2469 Drawable dr = null;
2470 if (itemInfo.packageName != null) {
2471 dr = getDrawable(itemInfo.packageName, itemInfo.icon, appInfo);
2472 }
2473 if (dr == null) {
2474 dr = itemInfo.loadDefaultIcon(this);
2475 }
2476 return dr;
2477 }
中间经历了获取Resources的过程,这个过程与其他版本的无异,getDrawable关键代码如下1
21129 final Resources r = getResourcesForApplication(appInfo);
1130 final Drawable dr = r.getDrawable(resId, null);
最终调用到Resources#getDrawableForDensity方法
1 | 871 public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) { |
对于 xml 定义的drawable,ResourcesImpl#loadDrawableForCookie里,会调用到Drawable的静态方法
1 | 755 if (file.endsWith(".xml")) { |
Drawable#createFromXmlForDensity会调用到Drawable#createFromXmlInnerForDensity1
2
3
4
5
6
71291
1292 static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
1293 @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
1294 @Nullable Theme theme) throws XmlPullParserException, IOException {
1295 return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
1296 density, theme);
1297 }
r.getDrawableInflater返回DrawableInflater的类型,DrawableInflater#inflateFromXmlForDensity会根据xml的tag来判断需要生成什么类型的资源
1 | 148 private Drawable inflateFromTag(@NonNull String name) { |
而adaptive-icon就返回了AdaptiveIconDrawable类型,所以最后getApplicationIcon就返回了AdaptiveIconDrawable类型。
一些问题
我在验证问题的时候,为了模拟问题场景,在一个项目中,新增了一个anydpi的启动图标,但其内容是这样子的
结果一运行起来手机就自动重启了,然后一进桌面就死一进就死,还有可能黑屏。此时我以为触发到什么缺陷了,把手机都搞坏,而且多台8.0的同时遭殃,但它们居然都是没root的!
直到去复查这个问题时才发现,原来触发到的不是系统的问题,是桌面launcher的问题,因为我设置的xml里存在着循环引用
1 | <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
foreground的引用里默认把自己引了,因为我把xml的名字也命名成launcher了。。。因为桌面launcher会去加载app的icon,所以就即使app不启动也会触发到这个问题。。。
要恢复很简单,只需要使用adb删除这个app即可。