目录
  1. 1. 一、插件化的核心挑战
  2. 2. 二、类的加载:DexClassLoader 机制
    1. 2.1. 2.1 类的隔离与共享
  3. 3. 三、资源的加载:AssetManager 与 addAssetPath
  4. 4. 四、Activity 的插件化:代理模式
    1. 4.1. 4.1 代理 Activity 的生命周期转发
    2. 4.2. 4.2 插件 Activity 的实现
  5. 5. 五、主流插件化框架对比
    1. 5.1. 5.1 RePlugin(360 开源)
    2. 5.2. 5.2 VirtualApp(商业级沙箱)
    3. 5.3. 5.3 Shadow(腾讯开源)
  6. 6. 六、Android App Bundle 与插件化的关系
  7. 7. 七、面试常问题目
解读开源框架系列-插件化框架设计

一、插件化的核心挑战

插件化(Pluginization)是比组件化更激进的技术方案:业务模块在编译期完全独立,运行时从服务器下载并动态加载到宿主 App 中。这需要攻克 Android 系统设计的多个壁垒——Android 在设计之初并不支持动态加载 APK。

插件化需要解决四大问题:

  1. 类的加载:插件 APK 中的类如何被宿主 ClassLoader 找到并加载?
  2. 资源的加载:插件 APK 中的图片、布局、字符串等资源如何被访问?
  3. 四大组件的加载:插件中的 Activity/Service/BroadcastReceiver/ContentProvider 如何正常工作?这些组件必须在 AndroidManifest.xml 中注册,而插件的 Manifest 没有被系统解析。
  4. 进程隔离与安全保障:如何防止插件代码影响宿主应用的稳定性?

二、类的加载:DexClassLoader 机制

Android 的 ClassLoader 体系中,DexClassLoader 可以加载指定路径的 DEX/JAR/APK 文件:

// java.lang.ClassLoader (AOSP libcore)
DexClassLoader pluginClassLoader = new DexClassLoader(
pluginApkPath, // 插件 APK 的路径(外存或 data/data 目录)
optimizedDirectory, // dex2oat 的输出目录
null, // native lib 目录(插件化可设为独立路径)
hostClassLoader // parent 设为宿主 ClassLoader
);

关键设计——parent 设置为宿主 ClassLoader

             BootClassLoader (Framework 类)

Host PathClassLoader (宿主 APK)
↑ ↑
Plugin DexClassLoader Plugin DexClassLoader
(插件A) (插件B)

这种设计有两个效果:

  1. 插件可以访问宿主的类和 Framework 的类(因为插件 ClassLoader 的 parent 是宿主 ClassLoader)。
  2. 宿主的 ClassLoader 找不到插件的类(宿主 ClassLoader 中不包含插件 DEX 的路径),这意味着宿主和插件之间的代码必须通过反射或接口通信。

如果需要宿主调用插件的类,可以在宿主 ClassLoader 的 DexPathList 中插入插件的 DEX 路径(通过反射修改 dexElements),但这会打破隔离性。

2.1 类的隔离与共享

实际生产环境中,插件化框架通常支持两种类加载策略:

// RePlugin 的类加载策略
// com.qihoo360.replugin.base.IPC
// 共享类:放在宿主 APK 中,所有插件共享(如公共库、协议类)
// 隔离类:每个插件独立加载,避免类冲突

三、资源的加载:AssetManager 与 addAssetPath

Android 的资源框架基于 ResourcesAssetManager。每个 APK 有自己的 resources.arsc 文件(资源索引表)和 res 目录。

要让宿主访问插件的资源,需要创建独立的 Resources 对象:

// 创建插件的 Resources 对象
public Resources createPluginResources(String pluginApkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
// addAssetPath 是隐藏 API(@hide),通过反射调用
Method addAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginApkPath);

Resources hostResources = context.getResources();
return new Resources(
assetManager, // 插件的 AssetManager
hostResources.getDisplayMetrics(), // 屏幕信息
hostResources.getConfiguration() // 配置信息
);
} catch (Exception e) {
throw new RuntimeException("Failed to create plugin resources", e);
}
}

关键点

  • addAssetPath 调用后,AssetManager 会将插件 APK 的资源索引(resources.arsc)纳入管理。
  • 新的 Resources 对象使用插件的 AssetManager,可以访问插件的资源。
  • 但宿主的 Resources 对象仍然只能访问宿主的资源——这是资源和类的双重隔离。

资源 ID 冲突问题:插件的资源 ID 可能与宿主冲突。编译时修改插件的 aapt 参数,给插件的资源 ID 设置一个不同的 package ID:

// AAPT 编译插件时指定 --package-id 0x7f → 0x7e(区别于宿主的 0x7f)
aapt package --package-id 0x7e --extra-packages com.plugin.lib ...

Android 8.0(API 26)后,ResourcesAssetManager 的实现发生了变化,一些反射方法不再可用。Google 在 API 30 正式暴露了 ResourcesProviderloadFromPath API,提供了官方的资源动态加载支持。

四、Activity 的插件化:代理模式

Activity 必须在 AndroidManifest.xml 中注册,这是插件化最大的难点。业界最成熟的方案是代理模式(Proxy Activity Pattern)——在宿主 Manifest 中预注册一个占位 Activity,运行时由它代理真正的插件 Activity。

4.1 代理 Activity 的生命周期转发

// 在宿主 Manifest 中预注册
<activity android:name=".plugin.ProxyActivity"
android:launchMode="standard"
android:configChanges="orientation|screenSize" />

// 运行时:宿主 ProxyActivity 持有插件 Activity 的引用
public class ProxyActivity extends Activity {
private PluginActivity mPluginActivity; // 通过反射创建

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String pluginActivityClassName = getIntent().getStringExtra("plugin_class");
// 用插件的 ClassLoader 加载插件 Activity
Class<?> pluginClass = PluginManager.getInstance()
.getPluginClassLoader().loadClass(pluginActivityClassName);
// 反射创建实例
mPluginActivity = (PluginActivity) pluginClass.newInstance();
// 设置宿主环境
mPluginActivity.setProxy(this);
// 调用插件的生命周期
mPluginActivity.onCreate(savedInstanceState);
}

@Override
protected void onResume() {
super.onResume();
mPluginActivity.onResume();
}

// ... 所有生命周期方法都按相同模式转发
}

4.2 插件 Activity 的实现

插件 Activity 不是一个真正的 android.app.Activity,而是一个普通类(继承自框架提供的 PluginActivity 基类),所有 Android API 调用都必须通过宿主 Activity 的代理:

public class PluginActivity {
private Activity mProxyActivity;
private Resources mPluginResources;

public void setProxy(Activity proxy) {
this.mProxyActivity = proxy;
}

// 设置布局:使用插件的 Resources 加载布局
protected void setContentView(int layoutResId) {
// 通过插件的 Resources 加载布局
XmlResourceParser parser = mPluginResources.getLayout(layoutResId);
View view = LayoutInflater.from(mProxyActivity)
.inflate(parser, null);
mProxyActivity.setContentView(view);
}

// 获取字符串:使用插件的 Resources
protected String getString(int resId) {
return mPluginResources.getString(resId);
}

// 启动其他插件 Activity:发送 Intent 给 ProxyActivity
protected void startPluginActivity(String className) {
Intent intent = new Intent(mProxyActivity, ProxyActivity.class);
intent.putExtra("plugin_class", className);
mProxyActivity.startActivity(intent);
}
}

五、主流插件化框架对比

5.1 RePlugin(360 开源)

RePlugin 是 360 团队开源的插件化框架(https://github.com/Qihoo360/RePlugin),核心特色是坑位(Pit)机制——在宿主 Manifest 中预注册一系列占位 Activity,每个坑位预设了不同的 launchMode 和主题。插件 Activity 运行时被分配到对应的坑位。

坑位分配逻辑:

// RePlugin 的坑位分配
// 根据插件的 launchMode、theme、configChanges 等属性匹配最合适的坑位
String pitAlias = PluginContainers.alloc(containerType,
pluginLaunchMode, pluginTheme, pluginConfigChanges);
// 启动匹配的坑位 Activity,同时传递真实的插件 Activity 类名

5.2 VirtualApp(商业级沙箱)

VirtualApp(https://github.com/asLody/VirtualApp)采用更高阶的思路——完全在用户态模拟 Android Framework 的行为。它 Hook 了大量系统服务(ActivityManagerService、PackageManagerService 等),在单进程中运行多个”虚拟应用”。这是一个重量级方案,主要用于应用多开。

5.3 Shadow(腾讯开源)

腾讯的 Shadow(https://github.com/Tencent/Shadow)是较新的插件化框架,特点是将框架本身也作为插件加载,避免插件框架代码与宿主代码的耦合。其核心是”零反射”设计——使用 Transform API 在编译期注入代码。

六、Android App Bundle 与插件化的关系

Google 于 2018 年推出 Android App Bundle(AAB),随后推出了 Play Feature Delivery 和 Dynamic Asset Delivery:

  • AAB:将 APP 拆分为 base + configuration splits + dynamic feature modules。
  • Dynamic Delivery:按需下载功能模块(dynamic feature),用户首次安装时不下载,需要时才拉取。
  • In-App Update API:Google Play 提供的应用内更新机制。

Google 的 Dynamic Delivery 在某种程度上替代了插件化的需求——它允许应用在不重新安装的情况下获取新功能。但这与插件化的哲学不同:

  1. 动态功能模块仍需通过 Google Play 审核和签名,更新周期受平台控制。插件化可以完全自主控制更新节奏。
  2. 动态模块必须经过 Google Play Console,不能从自有服务器下发。中国市场的应用无法使用。
  3. 动态模块之间的隔离是由系统保证的(不同 ClassLoader),比插件化更安全。

七、面试常问题目

Q1: 插件化框架如何解决 Activity 的 Manifest 注册问题?

两种主流方案:(1) 代理模式——在宿主 Manifest 中预注册一个或多个占位 Activity,运行时通过该 Activity 转发所有生命周期方法给插件 Activity,插件 Activity 本身不继承 android.app.Activity,而是继承框架提供的 PluginActivity 基类。(2) Hook 模式——Hook AMS(ActivityManagerService)的 startActivity 方法,在调用系统 process 之前将目标 Activity 替换为预注册的占位 Activity,系统创建 Activity 实例后,再 Hook ActivityThread 的 mH(Handler),将占位 Activity 替换回真实 Activity。

Q2: addAssetPath 为什么可以加载插件的资源?

Android 的资源加载链路是 Resources → AssetManager → resources.arsc + res 目录。AssetManager 是 C++ 层的对象,通过 JNI 与 Java 层通信。addAssetPath 方法将新的 APK 路径注册到 AssetManager 的 AssetPath 列表中,使得后续的资源查询(getString、getDrawable 等)可以找到插件 APK 中的资源。每个 APK 的资源由 packageId(即资源 ID 的高 8 位,通常是 0x7f)区分,因此需要给不同插件分配不同的 packageId。

Q3: 插件化框架为什么需要进程隔离?

插件代码是不可信任的(可能来自第三方或包含未知 Bug)。如果插件和宿主在一个进程中运行,插件的内存错误(如 OOM、死循环)会直接影响宿主的稳定性。更严重的是,插件可能在同一个进程中调用 System.exit() 或 Runtime.getRuntime().halt(),导致整个应用退出。多进程隔离(将插件运行在独立进程)可有效防止这些风险。

Q4: Google 为什么”放弃”了插件化?

Google 并未直接放弃,而是通过 AAB + Dynamic Delivery 提供了替代方案。核心原因:(1) 安全——绕过 Google Play 的代码审查机制,可能引入恶意代码;(2) 兼容性——Android 每个版本都对内部 API 有所改动,Hook 方案极易在新系统上失效;(3) 碎片化——自定义 ClassLoader 和资源的 Hack 方案在不同 ROM 上的表现不一致。Google 倾向于通过平台能力(而非开发者 Hack)来解决需求。


参考源码路径:

  • DexClassLoader:libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
  • BaseDexClassLoader:libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
  • AssetManager:frameworks/base/core/java/android/content/res/AssetManager.java
  • Resources:frameworks/base/core/java/android/content/res/Resources.java
  • RePlugin:https://github.com/Qihoo360/RePlugin
  • VirtualApp:https://github.com/asLody/VirtualApp
  • Shadow:https://github.com/Tencent/Shadow
  • Android App Bundle:https://developer.android.com/guide/app-bundle
打赏
  • 微信
  • 支付宝

评论