文章内容
- 一、上一讲那两个 Hook 点,在别的版本上会失效
- 1.1 Hook 点一的适配:getDefault() vs getService()
- 1.2 Hook 点二的适配:8.0 的 LAUNCHACTIVITY 在 9.0 里没了
- 二、顺带一个高频面试题:简述 Activity 启动流程
- 三、插件化第三件事:让插件 Activity 用上自己的资源
- 3.1 res 资源和 assets 资源,到底有什么区别
- 3.2 三层关系:Context → Resources → AssetManager
- 3.3 看源码:AssetManager 是怎么被创建出来的
- 3.4 为什么不能直接"合并",只能用"创建"
- 四、代码:LoadUtil.loadResource
- 4.1 方法一:在宿主 Application 里建一个全局 resource
- 4.2 方法二:资源放插件这边,插件自己加载
- 4.3 验证
- 五、这三讲到底用了哪些知识点
- 六、小结
本文整理自享学团队直播课《Android 插件化(三):资源加载》。
前面两讲把插件的类和 Activity 都"骗"进来了,但插件的 Activity 跑起来之后,getResources() 拿到的还是宿主的资源——图片、字符串、布局全是宿主那套。这一讲要做的就是让插件用自己的 AssetManager 单独加载一份属于插件的资源。
要做到这一点,你得先想通两件事:
- 上一讲那两个 Hook 点,只适配了 8.0。如果拿到 6.0、7.0、9.0 上跑,会不会崩?——会,所以得先做版本适配。
- 插件和宿主都用同样的
0x7f开头生成资源 ID,直接合并必然冲突,所以得给插件单独建一份资源。
这两个问题分别对应本讲的前半段和后半段。
一、上一讲那两个 Hook 点,在别的版本上会失效
上一讲的占坑法(ProxyActivity 方案)靠着两个 Hook 点把插件 Activity"骗"过 AMS:
Hook 点一:在 AMS 的代理对象上做动态代理,拦截 startActivity,把插件 Activity 的 intent 替换成占坑 ProxyActivity 的 intent。
Hook 点二:在 ActivityThread 的 Handler 上挂一个 Callback,在 AMS 放行之后、onCreate 之前,再把 intent 从 ProxyActivity 换回插件 Activity。

上一讲的代码只针对 8.0(API 26/28)写的。如果你把那段代码原样跑到 9.0 上,会发生什么?你点一下启动插件 Activity,onCreate 里打印的却是"我是代理的 activity",说明 Hook 点二根本没生效,intent 没有换回来。
为什么会这样?因为 9.0 里两个 Hook 点依赖的内部代码变了。适配的核心就是:看你代码里用到的反射目标,在不同版本里名字、结构变了没有,变了就改,没变就留着。 这也是所有 Android 版本适配的通用思路——直接去看源码找变化点,比背差异表管用。
1.1 Hook 点一的适配:getDefault() vs getService()
上一讲通过某处反射拿到 AMS 的单例(一个 Singleton<IActivityManager>),再反射它的 mInstance 拿到真正的 IActivityManager 代理。这中间涉及一个静态字段,而这个字段在不同版本里连名字带类都变了:
| 版本 | 类 | 静态字段 | 取法 |
|---|---|---|---|
| API 23(6.0)/ 7.x | android.app.ActivityManagerNative |
gDefault |
getDefault() |
| API 26+(8.0) | android.app.ActivityManager |
IActivityManagerSingleton |
getService() |
8.0 之前这个单例根本不在 ActivityManager 里,而在 ActivityManagerNative 这个类里,字段名叫 gDefault;到 8.0,ActivityManagerNative 被废弃,单例搬进了 ActivityManager,改名叫 IActivityManagerSingleton——类名和字段名一起变了,不只是"从哪个方法拿"变了。
没变的是外面那层壳:两版都是 android.util.Singleton,都靠反射它的 mInstance 拿到真正的 IActivityManager。所以动态代理拦截 startActivity 的那段逻辑完全不用动,要改的只是"去哪个类、拿哪个字段"。适配就一句判断:
// 反射拿 AMS 单例所在的 Singleton 对象:类名和字段名都按版本分支Object singleton;if (Build.VERSION.SDK_INT >= 26) { // 8.0+:android.app.ActivityManager.IActivityManagerSingleton Class<?> activityManagerClass = Class.forName("android.app.ActivityManager"); Field singletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton"); singletonField.setAccessible(true); singleton = singletonField.get(null);} else { // 6.0 / 7.0:android.app.ActivityManagerNative.gDefault Class<?> managerNativeClass = Class.forName("android.app.ActivityManagerNative"); Field singletonField = managerNativeClass.getDeclaredField("gDefault"); singletonField.setAccessible(true); singleton = singletonField.get(null);} // 往下两版完全一样:反射 Singleton.mInstance 拿到真正的 IActivityManagerClass<?> singletonClass = Class.forName("android.util.Singleton");Field mInstanceField = singletonClass.getDeclaredField("mInstance");mInstanceField.setAccessible(true);Object realIActivityManager = mInstanceField.get(singleton);1.2 Hook 点二的适配:8.0 的 LAUNCH_ACTIVITY 在 9.0 里没了
Hook 点二是重灾区,因为 9.0 把 Activity 启动的内部结构改了。
8.0 里,Activity 启动这条消息的 message.what 是 LAUNCH_ACTIVITY,值就是 100。message.obj 是一个 ActivityClientRecord,里面带着一个 Intent 字段。所以上一讲 Hook 点二的做法是:拿到 message.obj(ActivityClientRecord),反射它里面的 intent,把它换掉。
到 9.0,你去看 ActivityThread 的 Handler.handleMessage,会惊讶地发现:case LAUNCH_ACTIVITY(也就是 100)不见了。 9.0 的 case 从 110 开始,之前那些 100、101、102……全没了。那 Activity 启动信息去哪儿了?
顺着 9.0 的源码流程走一遍:
- AMS 放行后,通过 Binder 回调 App 进程。进入的是
ActivityThread的内部类ApplicationThread(它本身是个 Binder)。 ApplicationThread里调用的方法变成了scheduleTransaction(8.0 里叫scheduleLaunchActivity)。scheduleTransaction会丢一个ClientTransaction对象,走sendMessage(9.0 对应的 case 是 159),最后交到ActivityThread。注意ClientTransactionHandler是ActivityThread的抽象父类(ActivityThread extends ClientTransactionHandler),scheduleTransaction就定义在这个父类里。- 真正干活的是一个
TransactionExecutor。它把ClientTransaction里的 item 逐个取出来执行。这个 item 是ClientTransactionItem,它的实现类在 9.0 里叫LaunchActivityItem。
那个 LaunchActivityItem 才是 9.0 里真正携带 Intent 的类——它相当于把 8.0 的 ActivityClientRecord 包了一层封装。
所以 Hook 点二在 9.0 上的适配,思路变了:不能再直接 message.obj 拿 ActivityClientRecord,而要:
message.obj拿到的是ClientTransaction对象。ClientTransaction内部有个mActivityCallbacks的列表(getCallbacks()返回List<ClientTransactionItem>)。- 遍历这个列表,判断每个 item 的类名是不是
LaunchActivityItem。 - 命中之后,反射
LaunchActivityItem里的Intent字段,把它换回插件 Activity 的 intent。
这里有个很实用的技巧,遇到版本差异导致你"猜不出结构"的时候特别管用:打印。 你拿不到结构没关系,把 message.obj 打出来,把 ClientTransaction 打出来,把它的 getCallbacks() 打出来,item 是哪个类、类名是什么,一打印全出来。很多同学一遇到 9.0 就慌"反射不行了",其实 9.0 只是不让乱反射它标记了 @hide 的字段,LaunchActivityItem 这些没标记的照样能反射。
9.0 适配后的 Hook 点二核心代码如下:
// 9.0 适配版:message.obj 是 ClientTransactionObject clientTransaction = msg.obj; // 1. 反射拿 mActivityCallbacks(一个 List<ClientTransactionItem>)Class<?> ctClass = clientTransaction.getClass();Field callbacksField = ctClass.getDeclaredField("mActivityCallbacks");callbacksField.setAccessible(true);List<?> callbacks = (List<?>) callbacksField.get(clientTransaction); // 2. 遍历,找到 LaunchActivityItem(9.0 里真正的 Intent 藏在这里)for (Object callback : callbacks) { if ("android.app.servertransaction.LaunchActivityItem".equals(callback.getClass().getName())) { // 3. 反射 LaunchActivityItem 里的 mIntent 字段 Field intentField = callback.getClass().getDeclaredField("mIntent"); intentField.setAccessible(true); Intent proxyIntent = (Intent) intentField.get(callback); if (proxyIntent != null) { // 取出第一个 Hook 塞进去的插件 Intent,整个换回来(与上一讲 Hook 点一配套) Intent pluginIntent = proxyIntent.getParcelableExtra(HookUtil.TARGET_INTENT_KEY); if (pluginIntent != null) { intentField.set(callback, pluginIntent); } } break; }}注意 LaunchActivityItem 不在 android.app 下,而是 android.app.servertransaction 包里的一个类,判断时要用全类名 android.app.servertransaction.LaunchActivityItem。
这一版跑在 9.0 上,onCreate 终于打印"我是插件的 activity"了。适配完成。
二、顺带一个高频面试题:简述 Activity 启动流程
讲资源加载之前,老师在课间插了一个很常见的面试题——"简述一下 Activity 的启动流程"。很多同学一看到这种题就想把整个流程图背一遍,但老师的意思是:说几个关键点就够了,不用背全流程。
面试官真正想听的是你有没有抓住这几个跨进程和机制的点:
startActivity启动之后,会先走到Instrumentation.execStartActivity(Instrumentation上没有startActivity这个方法)。Instrumentation调用 AMS 的startActivity(这一步是跨进程的,App 进程 → System 进程)。- AMS 对 Activity 做校验和合法性检查。插件 Activity 就是在这里因为"没注册"被拦下来的。
- AMS 通过
ApplicationThread把结果跨进程传回 App 进程(这是第二次跨进程,只调用 System → App)。 - 回到 App 进程后,
ApplicationThread通过 Handler 消息机制把消息发给ActivityThread,通知启动onCreate、onStart等生命周期方法。
这里有两个细节最容易被说错,也恰恰是面试官挖坑的地方:
- 两次跨进程:一次是 App 发指令给 AMS,一次是 AMS 通知 App。你不能只提一次。
scheduleLaunchActivity在哪:它不在ActivityThread类里,而在ApplicationThread(ActivityThread的内部类)里。ApplicationThread本身是个 Binder。很多同学张口就说"ActivityThread 里的 scheduleLaunchActivity",这一听就是没看源码。
把这两个点答出来,再补几个你熟悉的细节,这道题就稳了。
三、插件化第三件事:让插件 Activity 用上自己的资源
回到正题——资源加载。
插件 Activity 虽然启动成功了,但它现在用的资源全是宿主的。插件的 HelloWorld 引用了插件自己 res 里的布局和字符串,结果跑起来显示的并不是插件要的东西。为什么?因为 getResources() 返回的是宿主的 Resources,它只认宿主的资源。
3.1 res 资源和 assets 资源,到底有什么区别
平时加载资源主要分两类:
- res 里的资源:通过
getResources().getString(R.string.xxx)、getResources().getDrawable(R.drawable.xxx)这样拿。 - assets 里的资源:通过
getAssets().open("xxx.txt")这样拿。
(课上老师其实是拿 res/raw 文件夹和 assets 文件夹做的对比,结论对整个 res 目录同样适用,只是 raw 是 res 的一种。)
这两类有区别吗?很多人会说"一个有 ID 一个没 ID"。对,但这只是最表面的。真要答得完整,面试官会认可你下面这几点:
- 有没有生成 ID:res 资源会在编译期被 aapt 编进
R.java,每个资源一个0x7f开头的 ID。assets 资源不会生成 ID。 - XML 里能不能访问:因为有 ID,res 资源可以在布局 XML 里用
@string/xxx、@color/xxx直接引用。assets 资源不行,XML 访问不到。 - 访问速度:有 ID 的资源查找更快——因为资源编译后生成了一张索引表(
resources.arsc),它是按 包 → 类型 → 序号 三级建索引的,用 ID 查等于直接按下标定位,比 assets 按文件名在 APK 里找条目快得多。 - 能不能自动适配限定符:res 下的资源支持
drawable-hdpi、values-zh、layout-land这类限定符,系统会根据设备的屏幕密度、语言、方向自动挑一份给你;assets 完全没有这套机制,你放什么就是什么,路径得写死。
那 assets 有啥好处?它在某些场景下操作更自由、更方便(比如放一些不想被编译处理的原始文件)。
但这里要戳破一个很多人没想过的点:这两类资源内核其实是同一个。你去看源码,getResources().getString(id) 底层调用的是 getText(id).toString(),而 getText 最终要跟 AssetManager 打交道;getAssets().open() 同样走 AssetManager:
public String getString(@StringRes int id) throws NotFoundException { return getText(id).toString();} public CharSequence getText(@StringRes int id) throws NotFoundException { // 绕了一圈,最后还是找 AssetManager 要 CharSequence res = mResourcesImpl.getAssets().getResourceText(id); if (res != null) { return res; } throw new NotFoundException("String resource ID #0x" + Integer.toHexString(id));}所以不管是 res 还是 assets,底层都是靠 AssetManager 去加载资源的。 区别只在于 res 会先按资源 ID 在 resources.arsc 里查出对应的资源文件名,再用 AssetManager 去读;assets 是直接用文件名找。这就是为什么我们加载插件资源时,核心操作对象是 AssetManager。
3.2 三层关系:Context → Resources → AssetManager
要加载插件资源,得先搞清楚系统里资源是怎么组织起来的:
Context:Activity 有Activity的 context,Application 有Application的 context。这俩不是同一个对象(面试时如果被问"activity 的 context 和 application 的 context 一样吗",答案是"不一样")。getResources()就是在 context 上调的。Resources:Context.getResources()返回的就是Resources。Resources实际上是个装饰模式:它内部包了一个ResourcesImpl(mResourcesImpl),真正的加载逻辑在ResourcesImpl里。AssetManager:在ResourcesImpl里。它才是真正干活的那个:管理 APK 里的资源文件,提供加载接口。
简单来说:Context 里有一个 Resources,Resources 里有一个 ResourcesImpl,ResourcesImpl 里有一个 AssetManager。 最终所有资源都要经 AssetManager 之手。
(补一句版本前提:ResourcesImpl 是 Android 7.0(API 24)才拆出来的,为了支持多套配置。7.0 之前 Resources 直接持有 mAssets(AssetManager)字段。这不影响本讲的方案——我们直接 new Resources(assets, ...),不依赖内部字段;但如果你想改成"反射替换现有 Resources 的 AssetManager"那种做法,就必须按版本分别反射 Resources.mAssets(<7.0)和 Resources.mResourcesImpl.mAssets(≥7.0)。)
所以问题变成:能不能给插件单独造一套"AssetManager + Resources",让插件的 Activity 用的是这套? 要回答这个问题,得先看系统是怎么创建 AssetManager 的。

3.3 看源码:AssetManager 是怎么被创建出来的
顺着 Activity 启动链(8.0 的 handleLaunchActivity → performLaunchActivity)往下走,你会发现创建 Context 的调用链:
performLaunchActivity里会创建 Context,实际是contextImpl.createActivityContext(...)之类的方法。真正创建的类叫ContextImpl。ContextImpl创建的时候,会立刻调用setResources(...),把这套 context 和一个Resources绑定起来。- 那这个
Resources哪来的?统一归ResourcesManager管。它走的是getOrCreateResources(获取或创建):之前建过就直接复用,没建过才新建。 - 新建时走
ResourcesManager.createResourcesImpl(key),这个方法里先调createAssetManager(key)造出一个AssetManager,再把它当参数传进ResourcesImpl的构造函数。 createAssetManager(key)做了两件事:new AssetManager(),接着调addAssetPath(...),把资源目录加进来。
对应的 AOSP 代码(API 26 / 27):
// ResourcesManager.javaprivate @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) { ... final AssetManager assets = createAssetManager(key); // ← 先造 AssetManager if (assets == null) { return null; } ... // ← 再把它传进 ResourcesImpl ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj); return impl;} protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { final AssetManager assets = new AssetManager(); // ← 就是这两行 if (key.mResDir != null) { if (assets.addAssetPath(key.mResDir) == 0) { // ← 返回 0 表示失败 Log.e(TAG, "failed to add asset path " + key.mResDir); return null; } } // 还会依次 addAssetPath(splitResDirs)、addOverlayPath、addAssetPathAsSharedLibrary return assets;}重点来了。createAssetManager 里那行 addAssetPath(...) 传入的字符串,就是资源目录/资源路径(这里传的是宿主的资源路径)。addAssetPath 的作用就是把某一个路径下的资源加进这个 AssetManager 里,让它以后能从这个路径加载资源。
这就是我们的 Hook 点!跟第一讲"类加载"的思路一模一样:类加载是找到插件的 dex 路径塞进去,资源加载是找到插件的资源路径塞进去。

所以问题就是:把这个路径从宿主的改成插件的。 改完之后,运行 App 加载的到底是插件资源还是宿主资源?——当然是插件资源。那这不是 Hook 点是什么?
3.4 为什么不能直接"合并",只能用"创建"
改路径有两种思路:替换,或者合并。
- 替换:直接把宿主资源路径换成插件资源路径。问题是宿主自己的资源就没了,宿主会崩。而且你这个是想给插件单独用的,不是想顶掉宿主。
- 合并:把宿主资源路径和插件资源路径都塞进同一个 AssetManager。听上去很美好,但会踩一个大坑——资源 ID 冲突。
为什么冲突?看资源 ID 的结构。一个资源 ID 是 32 位:最高一个字节是包 id(应用自己的资源是 0x7f,系统资源是 0x01),中间一个字节是资源类型,最后两个字节是该类型下从 0 开始递增的序号。
要注意,中间那个类型字节是 aapt 编译这个包时按"类型出现的先后顺序"分配的,不是平台写死的常量——所以在不同 APK 里,同一个 drawable 未必都是同一个编号、color 也未必都是 0x04。这不影响结论:宿主和插件是各自独立编译的,包 id 都是 0x7f,序号都从 0 开始递增,两套 ID 必然大面积重叠。 宿主某个 color 是 0x7f0400,插件里很可能也有一个资源占着同一个值。把两份资源合并进同一个 AssetManager,0x7f0400 到底指哪个?冲突了。
要解决合并冲突,得用 aapt 在编译时把插件的资源 ID 重新分配一遍。这很麻烦,而且你拿到的插件 APK 可能根本没预留重编空间。
所以本讲用的方案是第三种,也是最干净的:创建。 给插件专门创建一个新的 AssetManager 和配套的 Resources,让插件自己的一套完全独立,跟宿主互不干扰。反正插件也不常用宿主的 Resources 对象去拿资源——它在插件里建一套,自己在 BaseActivity 里重写 getResources() 返回这套就行。

四、代码:LoadUtil.loadResource
方案定了:给插件单独建一套 AssetManager + Resources。核心方法叫 loadResource,用反射实现。
首先,我们要反射的目标是 AssetManager 里的 addAssetPath 方法。所以要反射它,得先拿到 AssetManager 的类对象(Class),通过类对象拿方法,再调用。
public class LoadUtil { // 用插件 APK 的路径,创建一个只加载插件资源的 Resources public static Resources loadResource(Context context, String apkPath) { try { // 1. 拿到 AssetManager 类对象,反射创建一个 AssetManager 实例 Class<?> assetManagerClass = Class.forName("android.content.res.AssetManager"); AssetManager assetManager = (AssetManager) assetManagerClass.newInstance(); // 2. 反射拿到 addAssetPath 方法(注意它有一个只收一个 String 参数的重载) Method addAssetPath = assetManagerClass.getDeclaredMethod("addAssetPath", String.class); addAssetPath.setAccessible(true); // 3. 调用 addAssetPath,把插件 APK 路径塞进去(这里就是 Hook:只加载插件资源) // 补充:返回值是 cookie,0 表示添加失败(路径不存在 / 不是合法 APK),这里提前拦一下 int cookie = (int) addAssetPath.invoke(assetManager, apkPath); if (cookie == 0) { Log.e("LoadUtil", "addAssetPath failed: " + apkPath); return null; } // 4. 返回一个和这个 AssetManager 绑定的 Resources return new Resources( assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); } catch (Exception e) { e.printStackTrace(); } return null; }}这里有几个细节说明一下:
- 为什么反射
newInstance而不是直接new AssetManager():AssetManager的无参构造被标了@hide,不在android.jar里,你在 App 工程里直接new编译就过不去。 addAssetPath有重载:一个参数的版本接收String path。反射取方法时,如果不想取错,最好在getDeclaredMethod里把参数类型写清楚(String.class),这样才保证拿到的是那个收一个字符串参数的方法。- 为什么最后返回
Resources而不是AssetManager:因为插件里的人习惯用的是getResources()(得到Resources),直接给它一个裸的AssetManager不好用。所以我们创建一个Resources包裹这个 AssetManager,交给插件用。 - 创建
Resources需要AssetManager之外还要DisplayMetrics和Configuration。这两个从宿主现有的Resources里拿(context.getResources().getDisplayMetrics()/getConfiguration())。 - 补充一点:和
AssetManager的构造不同,Resources(AssetManager, DisplayMetrics, Configuration)是 SDK 里公开的构造,可以直接new,只是被标了@Deprecated(官方推荐走Context.createConfigurationContext()那条路)。插件化这个场景里我们要的就是"绑定到指定 AssetManager"的能力,所以仍然用它。
(Android 9 上的额外提醒:P 起 AssetManager() 和 addAssetPath() 被标成了 @UnsupportedAppUsage,属于灰名单——反射仍然可用,只是 Logcat 会打 Accessing hidden method ... 的警告,不会崩。另外从 P 开始,系统自己的 ResourcesManager.createAssetManager 已经改用 AssetManager.Builder() 来构建了(native 侧换成 AssetManager2),但不影响我们反射 new AssetManager() + addAssetPath 这条老路——只是你去 9.0 源码里找 new AssetManager() 会找不到。)
创建好这套 Resources 之后,剩下要做的事就是:让插件 Activity 拿到的 getResources() 返回这一套,而不是宿主那套。
4.1 方法一:在宿主 Application 里建一个全局 resource
写在宿主这边。在 MyApplication(宿主 Application 子类)里创建一个全局的 Resources 变量,并覆盖 getResources() 返回它:
public class MyApplication extends Application { public static Resources mResource; @Override public void onCreate() { super.onCreate(); // 用插件 APK 路径,创建一套只加载插件资源的 Resources mResource = LoadUtil.loadResource(this, "/sdcard/plugin.apk"); } // 关键的一步:宿主 Application 的 getResources() 被改成返回插件那套 // 这也正是这个方案的副作用——宿主自己再从 Application context 取资源就取错了 @Override public Resources getResources() { return mResource != null ? mResource : super.getResources(); }}然后插件这边,通过重写 getResources() 拿到它。因为插件可能有多个 Activity,所以建一个 BaseActivity 统一处理:
public class BaseActivity extends Activity { @Override public Resources getResources() { // MyApplication 已经把 getResources() 改成返回插件那套了,这里直接用 if (getApplication() != null && getApplication().getResources() != null) { return getApplication().getResources(); } return super.getResources(); }}但这个方法有个明显的副作用:你把宿主 MyApplication.getResources() 给覆盖了,宿主自己就不能再通过 Application context 拿资源了。 宿主想用自己的资源就出问题了。
所以本讲主推的是第二种方法。
4.2 方法二:资源放插件这边,插件自己加载
换个角度想:这个 Resources 一定要在宿主这边创建吗?不一定。我也可以在插件创建的时候,由插件自己加载——反正插件启动的时候肯定有 context。
于是把 loadResource 和那个全局 Resources 挪到插件这边。插件里建一个 LoadUtil,自己加载、自己缓存:
public class LoadUtil { private static Resources mResource; // 单例:插件内部的全局资源,只加载一次 public static Resources getResources() { if (mResource == null) { mResource = loadResource(...); // 用插件的 APK 路径 } return mResource; } public static Resources loadResource(Context context, String apkPath) { // ... 同前面,反射 AssetManager + addAssetPath + 返回 Resources }}插件里的 BaseActivity 重写 getResources() 返回它:
public class BaseActivity extends Activity { @Override public Resources getResources() { Resources resource = LoadUtil.getResources(); if (resource != null) { return resource; } return super.getResources(); }}这样宿主那边完全不用动,宿主用自己的 Resources,插件用自己的一套,互不干扰。这就是推荐的做法。
4.3 验证
重新编译插件,重启宿主,点击启动插件 Activity。日志打印"我是插件的 activity",同时插件里 setContentView 引用的那个布局、字符串也渲染出来了——注意那个 HelloWorld 是插件资源里的,不是宿主塞过去的。
到此,插件化的三大件(类、四大组件、资源)就全部能加载了。
五、这三讲到底用了哪些知识点
回顾一下,类加载、Activity 启动、资源加载这三讲串起来,其实覆盖了插件化最核心的几个知识点:
- 反射:反射是钩子语言的根基,拿类、拿方法、拿字段、改字段全靠它。
- 类加载:
DexClassLoader把插件的 dex 塞进路径,让宿主能 loadClass 出插件类。 - 动态代理:Hook 点一拦截
startActivity靠的就是 JDK 动态代理。 - Hook 技术:往系统链路里安插钩子,改掉传递中的对象。
- 四大组件启动流程:Activity 启动的跨进程机制、AMS 校验、Handler 消息机制。
- AMS:Activity 的全局调度者,插件 Activity 卡在它的注册校验上。
- 资源加载:AssetManager + addAssetPath(本章介绍的内容)。
- 如果做资源合并,还需要 aapt(重编资源 ID);真正完整的插件化框架还会用到 PMS(PackageManager)等。
六、小结
这一讲解决了两件事:先把上一讲的两个 Hook 点做版本适配(6.0 / 7.0 / 8.0 / 9.0),让占坑法在各版本都稳;然后搞定插件化的第三块拼图——资源加载。
资源加载的核心结论:Android 所有资源最终都靠 AssetManager 加载,所以给插件单独创建一个 AssetManager,调用 addAssetPath 把插件 APK 的资源路径加进去,再包成一个 Resources 返回,插件 Activity 重写 getResources() 用这套就行。
TODO:
- 去找一个和本讲不同的 Hook 点入口(比如从
Application创建资源的路径入手,而不是 Activity 这条链),画出它的时序图。这一讲的路口很多,Activity 只是其中之一。
本文由 AgentWorkFlow 生成