作者:37手游移动客户端团队
背景
在做插件化过程中,宿主需要用到插件的资源,涉及到加载插件的资源;
因为插件是以apk的方式存在的,所以插件的ID和宿主的ID可能导致重复;
为了解决这个问题,需要把插件的资源ID重新排一遍,才给宿主加载和使用。
资源加载
Android项目中的资源常规是通过R文件来索引的。打包的时候aapt将工程中的资源名与id在R文件中映射起来。使用资源时是通过Resources获取的,如:
resources.getDrawable(xxxid)
那么怎么加载插件中的资源呢?先看Resources的构造方法:
public Resources(AssetManager assetManager, DisplayMetrics metrics, Configuration config) {
}
Resources的初始化依赖:AssetManager,DisplayMetrics和Configuration。
在AssetManager中有一个方法
public int addAssetPath(String path)
该方法可用于AssetManager增加外部资源,但是AssetManager中该方法不对外公开,需要反射调用,代码大概如下:
val method = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
method.isAccessible = true
method.invoke(hostAssetManager, apk)
资源冲突方案
经过上述的处理后,插件的资源和宿主的资源是混合再一起的,因为所有的资源索引不做处理时都是以0x7faabbbb格式按顺序生成的。所以会存在插件资源ID和宿主资源ID冲突的情况(注意:资源索引共四个字节32位,第一个字节代表PackgeID,第二个字节代表TypeID,后两个字节代表资源值)
怎么解决冲突呢?有几种方案:
方案一:资源隔离
资源隔离就是宿主和插件使用不同的Resources对象,这样使用的资源文件不同,不存在冲突。代码调整如下:
//通过反射生成一个新的AssetManager实例
val am = AssetManager.class.newInstance()
//添加资源
val method = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
method.isAccessible = true
method.invoke(am, apk)
//生成新的Resources对象
val pluginResources = Resources(am, hostResources.displayMetrics, hostResources.configuration)
插件使用这个PluginResources。但是这里会有一些缺点:
1、宿主和插件的资源无法共享,使用起来不太方便
2、PluginResouces中不包含系统资源,在某些用到系统资源的场景会报错。如:加载前端页面,使用select标签时会使用到系统的一些资源
3、某些业务场景下,插件的Resources和宿主的Resources需要用同一个。如:游戏发行sdk,sdk相关界面非activity,使用到的Resources是游戏研发方activity(在宿主中)。
方案二:修改资源id
修改资源id,当前常用的方案有:
方案1:Android的资源id是aapt生成的,修改aapt,让插件中的资源id不从0x7f开始,比如从0x6f开始
方案2:生成插件apk后,修改插件apk中resources.arsc文件。resources.arsc文件是有固定格式的文件
可通过解析该文件修改资源索引值。但是存在一定缺陷:
1、修改难度较大,需要了解resources.arsc格式
2、只修改resources.arsc是不够的,因为代码中使用的是R类,还同时需要修改代码中的值;
方案3:通过反编译修改插件中的public.xml文件中的索引值,以及修改smali中的R类值
public.xml这个文件是哪来的?
该文件是apktool在反编译apk时,根据apk包中的resources.arsc文件生成。 没看过resource.arsc? (自己拖个apk到IDE看吧)
public.xml有什么作用
publc.xml是aapt在打包资源时用来固定资源id的,如果资源在public.xml中有对应的id了,那么打包资源时就用已经有的id。
public.xml中的id的格式
共四个字节32位,第一个字节代表PackgeID,第二个字节代表TypeID,后两个字节代表资源值
通常系统资源PackageID是01,而我们自己的资源PackageID是7f
TypeID,比如attr为01,string为02。但是并不固定,并不一定attr就是01。但是在public.xml中,同类型的该字节一定是一样的,否则回编译会失败。
R类
R类这里有个知识点,library模块中生成的R类中的成员的值不是常量,不带final。app模块生成的R类的值是常量值。而常量值在java编译时会被优化,最终代码中输出的就是常量值,而不是R.id.xxx这样。而library的因为是变量,不会被优化,代码中会保留R.id.xxx
R类和public.xml的关系
从本质上讲,其实并没有啥关系。但是由于在代码中我们会使用R.id去查找资源,这就关联上了。如果都用getIdentifier的方式先获取id,那把R类删了也没事。
public.xml打包后对应的就是resources.arsc中的值,而资源值生成Java类,这个类就是R类。也就是说平时使用R类,就是用里面的索引值去到resources.arsc中找到对应资源位置,再去加载。
修改步骤如下:
1、反编译插件apk
2、修改public.xml,将0x7faabbbb中的aa的值调高,比如0x7f010001→0x7f510001,因为第二个字节是TypeID,是按顺序增加的,而这个type实际上是没有多少的。修改成比较大的值后就不会冲突了。 (具体修改可参考提供代码中的PublicXmlBean类实现)
3、修改R$x.smali的值,反编译后R类就是一些smali代码,扫描smali代码,将其中的值也都按同样的规则增大。这样R类和resources.arsc依旧是对应的,代码中的R类正常。但是这里需要注意的是application模块的R类由于是final常量值,会被处理成常量。这种情况下修改R类其实不会有作用。而library的情况则可以。因此需要将涉及到R的代码及资源文件下沉为library模块依赖。
4、回编译,得到修改资源索引后的插件
源码工程简介
概要
程序入口
插件生成小tips
插件源码工程中的插件apk结构(也就是plugin.apk):
实际代码下沉为resmodule作为library模块,app模块中并无实际功能,仅仅作为application模块引用library,这样就可以输出我们的插件plugin.apk。 这样做的目的上面有说,其实就是避免我们的代码等在app模块,被弄成常量了。
效果
用工具修改前的R.layout.activity_main的值为2131296284,转换为16进制为0x7f09001c
用工具修改后再次查看R.layout.activity_main的值为2136539164,即0x7f59001c
总结
本文介绍了插件资源加载的多种方案,可以按照自己的使用场景以及团队技术能力选择合适的方案
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap