Android 插件化(三):如何加载插件里的资源

文章内容

本文整理自享学团队直播课《Android 插件化(三):资源加载》。

前面两讲把插件的类和 Activity 都"骗"进来了,但插件的 Activity 跑起来之后,getResources() 拿到的还是宿主的资源——图片、字符串、布局全是宿主那套。这一讲要做的就是让插件用自己的 AssetManager 单独加载一份属于插件的资源。

要做到这一点,你得先想通两件事:

  1. 上一讲那两个 Hook 点,只适配了 8.0。如果拿到 6.0、7.0、9.0 上跑,会不会崩?——会,所以得先做版本适配。
  2. 插件和宿主都用同样的 0x7f 开头生成资源 ID,直接合并必然冲突,所以得给插件单独建一份资源。

这两个问题分别对应本讲的前半段和后半段。

一、上一讲那两个 Hook 点,在别的版本上会失效

上一讲的占坑法(ProxyActivity 方案)靠着两个 Hook 点把插件 Activity"骗"过 AMS:

Hook 点一:在 AMS 的代理对象上做动态代理,拦截 startActivity,把插件 Activity 的 intent 替换成占坑 ProxyActivity 的 intent。
Hook 点二:在 ActivityThread 的 Handler 上挂一个 Callback,在 AMS 放行之后、onCreate 之前,再把 intent 从 ProxyActivity 换回插件 Activity。

两个 Hook 点的版本适配:Hook 点一 6.0 用 getDefault()/8.0 用 getService()(字段名不变);Hook 点二 8.0 用 LAUNCH_ACTIVITY=100(ActivityClientRecord)、9.0 改用 ClientTransaction → LaunchActivityItem

上一讲的代码只针对 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 的那段逻辑完全不用动,要改的只是"去哪个类、拿哪个字段"。适配就一句判断:

java
// 反射拿 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 的源码流程走一遍:

  1. AMS 放行后,通过 Binder 回调 App 进程。进入的是 ActivityThread 的内部类 ApplicationThread(它本身是个 Binder)。
  2. ApplicationThread 里调用的方法变成了 scheduleTransaction(8.0 里叫 scheduleLaunchActivity)。
  3. scheduleTransaction 会丢一个 ClientTransaction 对象,走 sendMessage(9.0 对应的 case 是 159),最后交到 ActivityThread。注意 ClientTransactionHandler 是 ActivityThread 的抽象父类(ActivityThread extends ClientTransactionHandler),scheduleTransaction 就定义在这个父类里。
  4. 真正干活的是一个 TransactionExecutor。它把 ClientTransaction 里的 item 逐个取出来执行。这个 item 是 ClientTransactionItem,它的实现类在 9.0 里叫 LaunchActivityItem。

那个 LaunchActivityItem 才是 9.0 里真正携带 Intent 的类——它相当于把 8.0 的 ActivityClientRecord 包了一层封装。

所以 Hook 点二在 9.0 上的适配,思路变了:不能再直接 message.obj 拿 ActivityClientRecord,而要:

  1. message.obj 拿到的是 ClientTransaction 对象。
  2. ClientTransaction 内部有个 mActivityCallbacks 的列表(getCallbacks() 返回 List<ClientTransactionItem>)。
  3. 遍历这个列表,判断每个 item 的类名是不是 LaunchActivityItem。
  4. 命中之后,反射 LaunchActivityItem 里的 Intent 字段,把它换回插件 Activity 的 intent。

这里有个很实用的技巧,遇到版本差异导致你"猜不出结构"的时候特别管用:打印。 你拿不到结构没关系,把 message.obj 打出来,把 ClientTransaction 打出来,把它的 getCallbacks() 打出来,item 是哪个类、类名是什么,一打印全出来。很多同学一遇到 9.0 就慌"反射不行了",其实 9.0 只是不让乱反射它标记了 @hide 的字段,LaunchActivityItem 这些没标记的照样能反射。

9.0 适配后的 Hook 点二核心代码如下:

java
// 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 的启动流程"。很多同学一看到这种题就想把整个流程图背一遍,但老师的意思是:说几个关键点就够了,不用背全流程。

面试官真正想听的是你有没有抓住这几个跨进程和机制的点:

  1. startActivity 启动之后,会先走到 Instrumentation.execStartActivity(Instrumentation 上没有 startActivity 这个方法)。
  2. Instrumentation 调用 AMS 的 startActivity(这一步是跨进程的,App 进程 → System 进程)。
  3. AMS 对 Activity 做校验和合法性检查。插件 Activity 就是在这里因为"没注册"被拦下来的。
  4. AMS 通过 ApplicationThread 把结果跨进程传回 App 进程(这是第二次跨进程,只调用 System → App)。
  5. 回到 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"。对,但这只是最表面的。真要答得完整,面试官会认可你下面这几点:

  1. 有没有生成 ID:res 资源会在编译期被 aapt 编进 R.java,每个资源一个 0x7f 开头的 ID。assets 资源不会生成 ID。
  2. XML 里能不能访问:因为有 ID,res 资源可以在布局 XML 里用 @string/xxx、@color/xxx 直接引用。assets 资源不行,XML 访问不到。
  3. 访问速度:有 ID 的资源查找更快——因为资源编译后生成了一张索引表(resources.arsc),它是按 包 → 类型 → 序号 三级建索引的,用 ID 查等于直接按下标定位,比 assets 按文件名在 APK 里找条目快得多。
  4. 能不能自动适配限定符:res 下的资源支持 drawable-hdpi、values-zh、layout-land 这类限定符,系统会根据设备的屏幕密度、语言、方向自动挑一份给你;assets 完全没有这套机制,你放什么就是什么,路径得写死。

那 assets 有啥好处?它在某些场景下操作更自由、更方便(比如放一些不想被编译处理的原始文件)。

但这里要戳破一个很多人没想过的点:这两类资源内核其实是同一个。你去看源码,getResources().getString(id) 底层调用的是 getText(id).toString(),而 getText 最终要跟 AssetManager 打交道;getAssets().open() 同样走 AssetManager:

java
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

要加载插件资源,得先搞清楚系统里资源是怎么组织起来的:

  1. Context:Activity 有 Activity 的 context,Application 有 Application 的 context。这俩不是同一个对象(面试时如果被问"activity 的 context 和 application 的 context 一样吗",答案是"不一样")。getResources() 就是在 context 上调的。
  2. Resources:Context.getResources() 返回的就是 Resources。Resources 实际上是个装饰模式:它内部包了一个 ResourcesImpl(mResourcesImpl),真正的加载逻辑在 ResourcesImpl 里。
  3. 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 的。

资源加载的三层关系:Context → Resources → ResourcesImpl → AssetManager,每层用箭头标注调用关系;(Resources 是装饰模式,内部持有 mResourcesImpl)

3.3 看源码:AssetManager 是怎么被创建出来的

顺着 Activity 启动链(8.0 的 handleLaunchActivity → performLaunchActivity)往下走,你会发现创建 Context 的调用链:

  1. performLaunchActivity 里会创建 Context,实际是 contextImpl.createActivityContext(...) 之类的方法。真正创建的类叫 ContextImpl。
  2. ContextImpl 创建的时候,会立刻调用 setResources(...),把这套 context 和一个 Resources 绑定起来。
  3. 那这个 Resources 哪来的?统一归 ResourcesManager 管。它走的是 getOrCreateResources(获取或创建):之前建过就直接复用,没建过才新建。
  4. 新建时走 ResourcesManager.createResourcesImpl(key),这个方法里先调 createAssetManager(key) 造出一个 AssetManager,再把它当参数传进 ResourcesImpl 的构造函数。
  5. createAssetManager(key) 做了两件事:new AssetManager(),接着调 addAssetPath(...),把资源目录加进来。

对应的 AOSP 代码(API 26 / 27):

java
// 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 路径塞进去,资源加载是找到插件的资源路径塞进去。

资源加载源码创建链路:handleLaunchActivity → performLaunchActivity → ContextImpl.createActivityContext → setResources → ResourcesManager.getOrCreateResources → createResourcesImpl → createAssetManager → new AssetManager + addAssetPath;addAssetPath 就是 Hook 点

所以问题就是:把这个路径从宿主的改成插件的。 改完之后,运行 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() 返回这套就行。

资源 ID 结构与合并冲突:32 位 ID = 包 id(0x7f) + 类型字节 + 序号;宿主与插件独立编译、包 id 都是 0x7f、序号都从 0 递增,合并时必然大面积重叠冲突

四、代码:LoadUtil.loadResource

方案定了:给插件单独建一套 AssetManager + Resources。核心方法叫 loadResource,用反射实现。

首先,我们要反射的目标是 AssetManager 里的 addAssetPath 方法。所以要反射它,得先拿到 AssetManager 的类对象(Class),通过类对象拿方法,再调用。

java
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() 返回它:

java
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 统一处理:

java
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,自己加载、自己缓存:

java
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() 返回它:

java
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 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android 插件化(三):如何加载插件里的资源 · Yinchen 的博客