89、插件化开发:插件化原理、类加载、资源加载、Activity生命周期管理
插件化开发,说白了就是让App能够动态加载外部的APK或DEX文件。我最早接触这个技术,是因为公司要做一个“轻量级热更新”方案。那时候App发版流程特别长,改一行代码都要等两周。嗯,插件化就是解决这类问题的利器。
你想想看,主App像一个宿主,插件就像乐高积木。你可以随时拼上去,也可以拆下来。今天我们就聊聊它的核心:类加载、资源加载、还有Activity生命周期管理。
核心思想:宿主App通过ClassLoader加载插件中的类,通过AssetManager加载插件中的资源,通过占位Activity代理插件Activity的生命周期。
插件化原理:一张图说清楚
先看一张我画的流程图。它把插件化的核心流程串起来了。
类加载:DexClassLoader 的正确用法
Android加载类,靠的是ClassLoader。宿主能加载自己的类,但加载不了插件里的类。怎么办?
我习惯用 DexClassLoader。它可以从一个外部的APK或DEX文件中加载类。看代码:
// 插件APK路径
String pluginPath = getFilesDir().getAbsolutePath() + "/plugin.apk";
// 解压目录
String dexOutputDir = getFilesDir().getAbsolutePath() + "/dex_output/";
DexClassLoader classLoader = new DexClassLoader(
pluginPath,
dexOutputDir,
null,
getClassLoader()
);
// 加载插件中的类
Class<?> pluginClass = classLoader.loadClass("com.example.plugin.MainActivity");
这里有个坑。我曾经在项目里直接传了 null 作为父加载器,结果插件里引用了宿主的一些类,全报 ClassNotFoundException。记住:父加载器一定要传宿主的ClassLoader,否则插件看不到宿主的类。
小技巧:如果你想让插件也能访问宿主的类,就把宿主的ClassLoader作为父加载器传进去。反过来,如果想让插件完全隔离,可以传BootClassLoader。
资源加载:AssetManager 的魔法
类加载搞定了,但资源呢?插件里的布局、图片、字符串,宿主怎么访问?
嗯,这里要用到 AssetManager 的一个隐藏方法。我最早看到这个方案时,觉得它像个黑魔法。但确实好用。
// 创建AssetManager
AssetManager assetManager = AssetManager.class.newInstance();
// 反射调用addAssetPath方法
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginPath);
// 创建Resources
Resources pluginResources = new Resources(
assetManager,
getResources().getDisplayMetrics(),
getResources().getConfiguration()
);
// 现在可以通过pluginResources访问插件资源了
int layoutId = pluginResources.getIdentifier(
"activity_main", "layout", "com.example.plugin"
);
为什么说它是黑魔法?因为 addAssetPath 是隐藏API,不同Android版本行为不一样。我在Android 10上遇到过一个问题:调用 addAssetPath 后,资源ID竟然和宿主冲突了。后来发现是插件和宿主用了相同的资源名称。
避坑指南:插件和宿主的资源名一定要避免重复。我建议给插件的资源加统一前缀,比如 plugin_。否则资源ID冲突会让你排查到怀疑人生。
Activity生命周期管理:占位代理模式
这是插件化最头疼的部分。Activity必须在AndroidManifest里注册,但插件里的Activity不可能提前注册到宿主里。怎么办?
行业里通用的方案是占位代理模式。说白了,就是在宿主里注册一个“万能”的Activity,用它来代理插件里的所有Activity。
我画个简单的流程:
- 宿主注册一个
ProxyActivity(占位Activity) - 启动插件Activity时,实际启动的是
ProxyActivity ProxyActivity通过反射创建插件Activity实例- 把
ProxyActivity的生命周期事件转发给插件Activity
代码大概长这样:
public class ProxyActivity extends AppCompatActivity {
private String pluginClassName;
private Object pluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取要启动的插件Activity类名
pluginClassName = getIntent().getStringExtra("plugin_class");
// 通过插件ClassLoader加载
Class<?> clazz = pluginClassLoader.loadClass(pluginClassName);
pluginActivity = clazz.newInstance();
// 调用插件Activity的onCreate
Method onCreate = clazz.getMethod("onCreate", Bundle.class);
onCreate.invoke(pluginActivity, savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
Method onResume = pluginActivity.getClass().getMethod("onResume");
onResume.invoke(pluginActivity);
}
// 其他生命周期方法类似...
}
你可能会问:那插件Activity里的 setContentView 怎么办?它拿到的 Window 是谁的?
嗯,这里有个关键点:插件Activity需要持有宿主的Context。我一般会在创建插件Activity时,把 ProxyActivity 的Context传进去。这样插件Activity就能正常使用 setContentView、findViewById 这些方法了。
核心要点:插件Activity本身不继承Activity,它只是一个普通的Java对象。所有生命周期方法都由ProxyActivity代理调用。插件Activity通过宿主Context来操作UI。
我踩过的几个坑
做插件化开发,坑是真的多。我挑几个典型的说说:
- 资源ID冲突:插件和宿主用了相同的资源ID,导致加载错资源。解决方案是给插件资源加前缀,或者用独立的资源包。
- so库加载:插件里的native库,宿主ClassLoader加载不到。需要手动把插件路径加到
System.load的搜索路径里。 - 广播接收器:插件里的BroadcastReceiver没法动态注册?可以在ProxyActivity里统一注册,然后转发给插件。
- Fragment兼容:插件里的Fragment如果用了
getActivity(),返回的是ProxyActivity。需要做一层包装。
我曾经在一个项目里,插件和宿主用了同一个第三方库的不同版本。结果运行时各种方法找不到。最后只能统一版本,或者用 ClassLoader 隔离。
总结一下
插件化开发,核心就三件事:
| 模块 | 技术方案 | 注意事项 |
|---|---|---|
| 类加载 | DexClassLoader | 父加载器传宿主ClassLoader |
| 资源加载 | AssetManager.addAssetPath | 资源名加前缀,避免冲突 |
| Activity管理 | 占位代理模式 | 传递宿主Context,代理生命周期 |
说实话,插件化开发在2024年的今天,用得比以前少了。因为Google推出了App Bundle和动态功能模块,官方方案更稳定。但如果你需要完全动态化、或者要支持低版本Android,插件化依然是最灵活的选择。
嗯,今天就聊到这里。代码示例你可以直接拿去用,但记得根据你的Android版本做适配。不同版本的系统API差异,有时候真的让人头大。