89、插件化开发:插件化原理、类加载、资源加载、Activity生命周期管理

插件化开发,说白了就是让App能够动态加载外部的APK或DEX文件。我最早接触这个技术,是因为公司要做一个“轻量级热更新”方案。那时候App发版流程特别长,改一行代码都要等两周。嗯,插件化就是解决这类问题的利器。

你想想看,主App像一个宿主,插件就像乐高积木。你可以随时拼上去,也可以拆下来。今天我们就聊聊它的核心:类加载、资源加载、还有Activity生命周期管理。

核心思想:宿主App通过ClassLoader加载插件中的类,通过AssetManager加载插件中的资源,通过占位Activity代理插件Activity的生命周期。

插件化原理:一张图说清楚

先看一张我画的流程图。它把插件化的核心流程串起来了。

宿主App 插件管理器 下载/加载插件APK 自定义ClassLoader 资源加载 Activity管理 插件APK 插件类、资源、Activity 插件化核心流程 宿主 → 插件管理器 → 加载插件 → 类/资源/Activity代理 类加载:DexClassLoader 资源加载:AssetManager 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。

我画个简单的流程:

  1. 宿主注册一个 ProxyActivity(占位Activity)
  2. 启动插件Activity时,实际启动的是 ProxyActivity
  3. ProxyActivity 通过反射创建插件Activity实例
  4. 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就能正常使用 setContentViewfindViewById 这些方法了。

核心要点:插件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差异,有时候真的让人头大。


公众号:蓝海资料掘金营,微信deep3321