手机插件加载失败,手机无法加载插件解决方法

首页 > 实用技巧 > 作者:YD1662023-04-18 00:33:27

摘要

本文介绍了 Android 插件化框架中,插件使用宿主资源时资源错乱的问题,以及错乱的原因、业界通用解决方案、我们提出的优化方案。

本文将按照如下顺序,循序渐进地进行讲解:

1. Android 插件化中资源的动态化

Android 发展了这么多年,市面上涌现出许多插件化/热修复框架,无论是插件化还是热修复,都是为了实现对主Apk以外内容的动态化,这些内容包括 dex(class)、res(资源)、so(动态库)等。对于每一种内容,业界都有许多实现方案,尽管方案各不相同,但底层原理都差不多,网上也有许多文章和开源项目可以学习参考。

名词解释

宿主:直接安装到用户手机上的 App,宿主中的代码在宿主安装到用户手机上的那一刻就定死了,不能再改变了(热修复也只是让错误的逻辑不走而已,并没有改变原有的代码)。

插件:独立于宿主之外的一个文件。需要被宿主动态加载的 class、res、so 等的集合。(热修复中这部分通常称为 patch,这里为了方便,就叫插件吧)

java 代码:为了描述方便,apk 中的 dex 在编译前一律称为 java 代码,编译后一律称为 dex(这个说法不准确,不要被我误导了,一般为java / kotlin- > class- > dex )

说到 Android 资源的动态化,思路都大同小异:

aapt2 的出现使资源固定、packageId 修改变得容易了很多!

尽管 Android 资源的动态化技术已经十分成熟,但是在实践过程中还是有许多不足,比如“资源固定”就经常被业务同学吐槽。

2. Android 中的资源介绍

在介绍资源固定之前,首先简单介绍一下 Android 中资源相关的基础知识。

2.1 Android 中的资源 id

Android 代码在编译成 apk 之后,每个资源都对应一个唯一的资源 id,资源 id 是一个 8 位的 16 进制 int 值 0xPPTTEEEE :

注意:

2.2 Android 中的资源使用方式

Android 中使用资源通常有两种方式:

  1. 在 java 代码中通过 R 的内部类进行访问,具体语法为:

[<package_name>].R.<resource_type>.<resource_name>

  1. 在 xml 中通过符号使用,具体语法为:

@[<package_name>:]<resource_type>/<resource_name>

xml 中也可以通过 ? 代替 @ 的形式引用样式属性。也可以引入自定义属性,如 android:layout_width 。这两种用法不影响下文的介绍。

那么这两种方式有什么区别呢?

从代码书写的角度来说,都是通过一个资源名称(resource_name)来访问资源。我们反编译一下 apk,看看编译后是什么样的。

分别在项目 app module、library module、xml 中编写如下代码

手机插件加载失败,手机无法加载插件解决方法(1)

我们反编译一下 apk,看看这三种代码在 apk 中是如何表现的。

手机插件加载失败,手机无法加载插件解决方法(2)

可以发现 appTest 方法和 xml 中的资源变成了数字(0x7f0e0069),libTest 方法中的资源依旧是通过 Lcom/bytedance/lib/R$string;->test 访问的

结论:

那么为什么 libTest 方法中是通过 field 引用,而 appTest 中就变成数字了呢?

2.3 Android 中资源编译的简单流程

假设有一个工程,只有一个 app module,通过 maven 仓库依赖若干三方 aar,项目编译时的简化流程如下图:

手机插件加载失败,手机无法加载插件解决方法(3)

  1. 下载三方 aar;
  2. 将 app module 和三方 aar 中的资源经过 aapt2 进行编译、链接,最终生成R.jar和ap_
  3. R.jar 包含了最终打入 apk 的所有 R.class,每个依赖对应一个。aapt2 也会默认按照字母排序为每个资源分配唯一的 id 值。注意:新增删除一个资源都会导致它后面的资源 id 改变。aapt2 允许通过配置干预 id 的分配。
  4. ap_ 文件中包含了所有编译好的资源文件。
  5. App module 的 java 文件与 R.jar 一起被 javac 编译。由于 R.jar 中的 field 都是 final,因此 app module 中通过 R 引用的资源全部被内联成了数值。而三方 aar 中由于已经是 class,无需进行编译,因此依旧是通过 R 引用来使用资源;
  6. 最后把 app module 编译出来的 .class、三方 aar 中的 .class 转成 dex,与 ap_ 一起压缩到 apk 中。

因此就很容易理解为啥 libTest 中依旧是通过 R 来使用资源,而 appTest 中通过数值直接引用(被内联)。

libTest module 虽然被 app module 通过源码依赖,但是在资源编译这块其实是类似的,这里不展开介绍。

2.4 总结

Android 中的资源的无论是通过 java 代码使用还是 xml 使用,最终都是通过资源 id 值进行查找的。

把 apk 拖到 as 中,查看 resources.arsc 文件,可以看到它里面包含了 apk 中所有资源的 id 索引,以及该资源名对应的真正资源或值。很容易想到,App 运行起来也是通过资源 id 值经过这个资源表来查找真正的资源内容。

3. 插件使用宿主资源

3.1 插件如何使用宿主资源

想象一下,我们想要把 App 的直播功能做成一个插件动态下发,直播功能所需要的大部分资源都在直播插件中,但是总有一些资源来自宿主,如一些通用的 UI 组件中包含的资源(support/androidx 库)等。

那么,假设宿主中有一张图片名为 icon,直播插件中的 xml 通过 @drawable/icon 引用了这张图片,同时也在代码中通过 R.drawable.icon 引用了它,实际直播插件中是没有 icon 这张图片的,它存在于宿主中。宿主编译完后,按照前面的知识点,宿主中的 icon 对应的数值被编译成 0x7f010001。

插件本身也是一个 apk,根据前面介绍的知识点,插件编译完成后,xml 中的 @drawable/icon 会编成一个数值(0x7f010001),java 代码中的 R.drawable.icon 也会直接或间接编成一个数值(0x7f010001)。当这个插件运行在宿主上,按照前面的介绍,插件会去查找 0x7f010001,发现可以找到,这样就正确的使用了宿主资源。

插件编译时我们会做一些处理,使插件中可以引用到宿主 id。

3.2 插件使用宿主资源有什么问题

前文介绍过,新增或删除一个资源都可能导致其他许多资源的 id 被改变。

我们的宿主编译出来后 icon 为 0x7f010001,基于已有的宿主编译出一个插件后,插件中引用的 icon 也是 0x7f010001,此时没什么问题。

宿主迭代后,新增了一个新的资源 aicon,按照前面介绍的资源 id 分配规则,新版本的宿主中 aicon 的 id 值为 0x7f010001,icon 的 id 值被分配为 0x7f010002。老版本的插件下发到新版本的宿主上时依旧会通过 0x7f010001去宿主中找 icon,自然就找错了。运气好一点可能只是图片展示异常,运气不好点可能就直接 crash 了。

3.3 如何解决这类问题

为了解决这个问题,业界目前有一个通用、稳定的方案——资源固定。宿主编译时通过 aapt2 提供的参数对插件使用到的资源进行固定,使宿主每次打包时这些资源的值永远不发生改变。

资源固定方案的弊端:

  1. 一个插件对应一个宿主的情况:
  2. 必须把宿主的所有资源都进行固定。如果只固定插件使用的资源,当一个宿主有两个插件时,两个插件各自给宿主固定自己需要的资源,在代码合并时,很容易引发冲突,因为资源固定的值是不允许重复的;
  3. 当宿主接入多个涉及到资源固定的框架,如:插件化、资源热修复、游戏重打包框架等,这些框架之间进行资源固定时也需要考虑统一固定,这个成本是很高的;
  4. 资源固定提高了宿主接入框架的成本。
  5. 一个插件运行在多个宿主的情况:
  6. 当一个插件想要运行在多个宿主上,就需要每个宿主针对该插件的资源使用情况进行资源固定。一旦某个宿主已经对某个资源进行了固定,导致其与该插件要求的资源固定产生冲突,插件就需要对该宿主进行妥协,根据该宿主已有的资源固定重新生成固定规则。这样就无法实现一个插件在多个宿主上运行。我们目前有一个需求:同一个插件需要在上千个宿主上运行,如果不能解决这个问题,可能需要打成百上千个插件出来,很明显是不合理的;
  7. 资源固定提高了宿主接入框架的成本。

为了解决上述的问题,我们研究了一套新的方案解决资源错乱问题。

4. 免资源固定方案

同一个版本的插件运行在不同版本甚至不同的 App 上时,插件的代码是固定的,而宿主中的资源 id 是会改变的,为了解决资源错乱问题,当前的思路是保证宿主每次出新版本时资源 id 不变。那么有没有办法在不约束宿主的情况下,让插件始终跟宿主的资源 id 保持一致呢?

由于插件打包时,宿主是未知的,并且对于一个插件跑在多个宿主的情况,宿主也是多样的。所以没法指定让插件把 id 打成满足宿主的样子,而前文也介绍过,插件中引用宿主 id 的地方都是常量。那怎么办呢?

是否可以在插件运行到宿主上时,动态修改插件中的内容,实现插件与宿主 id 值匹配的效果。

比如插件中使用了宿主的资源 icon,对应的 id 值为 0x7f010001。当该插件运行在一个 icon 为 0x7f010002的宿主上时,由于运行时资源查找都是通过 id 值进行的,此时我们只能知道插件是在找一个 id 为 0x7f010001 的资源。通过某些手段,如果我们可以把 0x7f010001 映射成 icon 这个字符串,然后利用 Android 系统提供的Resources#getIdentifier方法,动态获取到当前宿主中 icon 对应的资源 id,即可保证插件加载到正确的资源。

这个工作需要在插件编译时、运行时分别做一些工作配合完成实现。

4.1 插件编译时工作

本小节内容基于 agp4.1 介绍,各个版本有些许差异,但总体思路大同小异。

前面介绍了,插件使用宿主资源主要有两种情况:1.通过 java 代码 2.通过 xml。

4.1.1 处理 java 代码中引用宿主的资源

java 代码在编译成 class 之后,对于引用宿主资源 id 的代码,有的会编译成数值,有的依旧是通过 R 引用。对于后者,我们可以很容易找出来,对于前者就有些困难了,因为单纯去扫描 class 中 0x7f 开头的数字,很容易误判,把一个无意义的数字也当作资源 id 处理。

前面讲了为什么 class 中的资源 id 会内联成数值,那我们不让它内联不就好了吗?只需要在编译过程中处理 R.jar,移除 class 中所有的 final 字段,就可以保证插件中引用宿主的资源 id 全部通过 R 进行引用。

这块需要对 agp 的工作流程、gradle plugin 的开发有一定的了解,用到了 asm 字节码修改技术和 agp 提供的 transform api,不了解的同学可以单独查一下,这块就不详细介绍了。

简单来说就是通过这两项技术,可以在编译 apk 时,对 class 文件进行修改。

开始实践

  1. 由于 R.jar 是在 processResourcesTask 中生成的,因此可以写一个 gradle plugin,在 processResourcesTask 的 doLast 中获取到 R.jar,修改 R.jar 中的字节码,将 field 中的 id 为 0x7f 开头的字段的 final 修饰符全部移除。这样就可以保证插件 class 中所有引用宿主资源的地方都不会被内联成数值;
  2. 经过第一步的处理,插件中引用的宿主资源全部通过 R.xx.xx 来引用,但插件 R 中的数值依旧是无法与宿主对应的。因此我们继续写一个 transform,扫描出插件中通过 R 引用资源的地方,利用 asm 将其从原来的 R 引用修改为方法调用。插件运行时,原本类似 R.drawable.test 的代码不再是获取一个常量数值,而是调用一个方法,内部动态计算当前宿主中对应的值。

手机插件加载失败,手机无法加载插件解决方法(4)

首页 123下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.