一、插件化的核心挑战 插件化(Pluginization)是比组件化更激进的技术方案:业务模块在编译期完全独立,运行时从服务器下载并动态加载到宿主 App 中 。这需要攻克 Android 系统设计的多个壁垒——Android 在设计之初并不支持动态加载 APK。
插件化需要解决四大问题:
类的加载 :插件 APK 中的类如何被宿主 ClassLoader 找到并加载?
资源的加载 :插件 APK 中的图片、布局、字符串等资源如何被访问?
四大组件的加载 :插件中的 Activity/Service/BroadcastReceiver/ContentProvider 如何正常工作?这些组件必须在 AndroidManifest.xml 中注册,而插件的 Manifest 没有被系统解析。
进程隔离与安全保障 :如何防止插件代码影响宿主应用的稳定性?
1.1 插件化的技术演进 2013-2015: 萌芽期 - 基于反射的动态加载(dynamic-load-apk) - 基本的 ClassLoader 替换 2015-2017: 爆发期 - DroidPlugin(360):代理模式 + Hook AMS - RePlugin(360):坑位机制 - VirtualAPK(滴滴):多方案融合 - Atlas(阿里):组件化 + 动态部署 - Small(林光宇):轻量级插件化 2017-2020: 成熟期 - VirtualApp:应用沙箱/多开 - Shadow(腾讯):零反射 + Transform API - RePlugin 2.0:稳定方案 2020-至今: 官方替代 - Android App Bundle (AAB) - Play Feature Delivery - 插件化逐渐被 AAB + In-App Update 替代
二、类的加载:DexClassLoader 机制 2.1 Android ClassLoader 体系 Android 的 ClassLoader 继承关系:
java.lang.ClassLoader │ └── dalvik.system.BaseDexClassLoader (API 26+) │ ├── dalvik.system.PathClassLoader │ - 用于加载已安装 APK 中的类 │ - parent 是 BootClassLoader │ └── dalvik.system.DexClassLoader - 用于加载任意路径的 DEX/JAR/APK - 可指定 parent - 插件化的核心
Android 的 ClassLoader 体系中,DexClassLoader 可以加载指定路径的 DEX/JAR/APK 文件:
DexClassLoader pluginClassLoader = new DexClassLoader ( pluginApkPath, optimizedDirectory, null , hostClassLoader );
2.2 Android 类加载流程 final class DexPathList { private Element[] dexElements; public Class<?> findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { Class<?> clazz = element.findClass(name, definingContext, suppressed); if (clazz != null ) { return clazz; } } return null ; } static class Element { private final DexFile dexFile; private final File path; public Class<?> findClass(String name, ClassLoader definingContext, List<Throwable> suppressed) { return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null ; } } } protected Class<?> loadClass(String name, boolean resolve) { Class<?> c = findLoadedClass(name); if (c == null ) { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } if (c == null ) { c = findClass(name); } } return c; }
2.3 插件化 ClassLoader 的层级设计 关键设计——parent 设置为宿主 ClassLoader :
BootClassLoader (Framework 类) ↑ Host PathClassLoader (宿主 APK) ↑ ↑ Plugin DexClassLoader Plugin DexClassLoader (插件A) (插件B)
这种设计有两个效果:
插件可以访问宿主的类和 Framework 的类(因为插件 ClassLoader 的 parent 是宿主 ClassLoader)。
宿主的 ClassLoader 找不到插件的类(宿主 ClassLoader 中不包含插件 DEX 的路径),这意味着宿主和插件之间的代码必须通过反射或接口通信。
如果需要宿主调用插件的类 ,可以在宿主 ClassLoader 的 DexPathList 中插入插件的 DEX 路径(通过反射修改 dexElements),但这会打破隔离性。
2.4 DexElements 合并技术(热修复也用到的核心技术) public static void injectPluginDex (ClassLoader hostClassLoader, ClassLoader pluginClassLoader) { try { Field hostPathListField = BaseDexClassLoader.class.getDeclaredField("pathList" ); hostPathListField.setAccessible(true ); Object hostPathList = hostPathListField.get(hostClassLoader); Field hostElementsField = DexPathList.class.getDeclaredField("dexElements" ); hostElementsField.setAccessible(true ); Object[] hostElements = (Object[]) hostElementsField.get(hostPathList); Field pluginPathListField = BaseDexClassLoader.class.getDeclaredField("pathList" ); pluginPathListField.setAccessible(true ); Object pluginPathList = pluginPathListField.get(pluginClassLoader); Field pluginElementsField = DexPathList.class.getDeclaredField("dexElements" ); pluginElementsField.setAccessible(true ); Object[] pluginElements = (Object[]) pluginElementsField.get(pluginPathList); Object[] mergedElements = (Object[]) Array.newInstance( hostElements.getClass().getComponentType(), pluginElements.length + hostElements.length ); System.arraycopy(pluginElements, 0 , mergedElements, 0 , pluginElements.length); System.arraycopy(hostElements, 0 , mergedElements, pluginElements.length, hostElements.length); hostElementsField.set(hostPathList, mergedElements); } catch (Exception e) { throw new RuntimeException (e); } }
2.5 类的隔离与共享 实际生产环境中,插件化框架通常支持两种类加载策略:
// 共享类加载器(Shared ClassLoader) // 放在宿主 APK 中,所有插件共享(如公共库、协议类) // 通过设置 parent 或 sharedLibrary 实现 // 隔离类加载器(Isolated ClassLoader) // 每个插件独立加载,避免类冲突 // 每个插件有自己的 DexClassLoader,不同的 parent 链
三、资源的加载:AssetManager 与 addAssetPath Android 的资源框架基于 Resources 和 AssetManager。每个 APK 有自己的 resources.arsc 文件(资源索引表)和 res 目录。
3.1 Resources 加载链路 Resources │ ▼ AssetManager (Java) │ JNI ▼ AssetManager (C++) —— android_content_res_AssetManager.cpp │ ├── resources.arsc 解析 ├── ResTable —— resourceId -> value 的查找表 └── AssetPath 列表 —— 所有已加载的 APK 的资源路径
3.2 创建插件 Resources 要让宿主访问插件的资源,需要创建独立的 Resources 对象:
public Resources createPluginResources (String pluginApkPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = AssetManager.class.getDeclaredMethod( "addAssetPath" , String.class); addAssetPath.invoke(assetManager, pluginApkPath); Resources hostResources = context.getResources(); return new Resources ( 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 对象仍然只能访问宿主的资源——这是资源和类的双重隔离。
3.3 资源 ID 冲突解决方案 资源 ID 冲突 问题:插件的资源 ID 可能与宿主冲突。编译时修改插件的 aapt 参数,给插件的资源 ID 设置一个不同的 package ID:
// AAPT 编译插件时指定 --package-id 0x7f -> 0x7e(区别于宿主的 0x7f) aapt package --package-id 0x7e --extra-packages com.plugin.lib ...
资源 ID 的结构:
0xPP TT EEEE │ │ └── Entry ID (16bit): 资源在类型中的序号 │ └────── Type ID (8bit): 资源类型 (layout=0x03, string=0x01, drawable=0x02) └────────── Package ID (8bit): APK 包标识 (系统=0x01, 应用=0x7f, 插件=0x7e)
3.4 Android 8.0+ 的资源加载变化 Android 8.0(API 26)后,Resources 和 AssetManager 的实现发生了变化,一些反射方法不再可用。Google 在 API 30 正式暴露了 ResourcesProvider 和 loadFromPath API,提供了官方的资源动态加载支持:
ResourcesProvider provider = ResourcesProvider.loadFromPath(pluginApkPath);Resources pluginResources = new Resources (provider);
四、Activity 的插件化:代理模式 vs Hook 模式 Activity 必须在 AndroidManifest.xml 中注册,这是插件化最大的难点。业界有两种主流方案:
4.1 代理模式(Proxy Activity Pattern) 在宿主 Manifest 中预注册一个占位 Activity,运行时由它代理真正的插件 Activity。
宿主 Manifest 预注册 :
<activity android:name =".plugin.ProxyActivity" android:launchMode ="standard" android:configChanges ="orientation|screenSize" />
代理 Activity 的生命周期转发 :
public class ProxyActivity extends Activity { private PluginActivity mPluginActivity; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); String pluginActivityClassName = getIntent().getStringExtra("plugin_class" ); 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(); } }
插件 Activity 的实现 :
public class PluginActivity { private Activity mProxyActivity; private Resources mPluginResources; public void setProxy (Activity proxy) { this .mProxyActivity = proxy; } protected void setContentView (int layoutResId) { XmlResourceParser parser = mPluginResources.getLayout(layoutResId); View view = LayoutInflater.from(mProxyActivity) .inflate(parser, null ); mProxyActivity.setContentView(view); } protected String getString (int resId) { return mPluginResources.getString(resId); } protected void startPluginActivity (String className) { Intent intent = new Intent (mProxyActivity, ProxyActivity.class); intent.putExtra("plugin_class" , className); mProxyActivity.startActivity(intent); } }
4.2 Hook 模式(AMS Hook) Hook 模式的思路更底层:拦截系统调用,在启动 Activity 的流程中偷梁换柱。
Activity 启动流程中的 Hook 点 :
App进程 System Server 进程 │ │ │ startActivity(intent) │ │ │ │ │ ▼ │ │ Activity.startActivityForResult() │ │ │ │ │ ▼ │ │ Instrumentation.execStartActivity() │ │ ← Hook点1: 替换 Intent 中的目标 Activity 为占坑 Activity │ │ │ │ ├── Binder ──────────────────►│ │ │ │ │ │ AMS.startActivity() │ │ 验证 Manifest (通过! 因为是占坑Activity) │ │ 创建进程/安排任务 │ │ │ │ │◄────────────────────────────┤ │ │ │ │ ▼ │ │ ApplicationThread.scheduleLaunchActivity() │ ← Hook点2: 将占坑 Activity 替换回真正的插件 Activity │ │ │ ▼ │ ActivityThread.performLaunchActivity() │ 创建 Activity 实例 (插件 Activity) │ 调用 attach, onCreate 等生命周期
Hook 点1 实现(Hook Instrumentation) :
public class PluginInstrumentation extends Instrumentation { private Instrumentation mBase; @Override public ActivityResult execStartActivity (Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { String pluginActivity = intent.getComponent().getClassName(); intent.setClassName(who, "com.host.plugin.ProxyActivity" ); intent.putExtra("real_activity" , pluginActivity); return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode, options); } } public static void hookInstrumentation () { ActivityThread activityThread = ActivityThread.currentActivityThread(); Field instrumentationField = ActivityThread.class.getDeclaredField("mInstrumentation" ); instrumentationField.setAccessible(true ); Instrumentation instrumentation = (Instrumentation) instrumentationField.get(activityThread); PluginInstrumentation pluginInstrumentation = new PluginInstrumentation (); pluginInstrumentation.setBase(instrumentation); instrumentationField.set(activityThread, pluginInstrumentation); }
Hook 点2 实现(Hook H.Callback) :
public static void hookHCallback () { ActivityThread activityThread = ActivityThread.currentActivityThread(); Field hField = ActivityThread.class.getDeclaredField("mH" ); hField.setAccessible(true ); Handler mH = (Handler) hField.get(activityThread); Field callbackField = Handler.class.getDeclaredField("mCallback" ); callbackField.setAccessible(true ); callbackField.set(mH, new Handler .Callback() { @Override public boolean handleMessage (Message msg) { if (msg.what == LAUNCH_ACTIVITY) { Object r = msg.obj; Field intentField = r.getClass().getDeclaredField("intent" ); Intent intent = (Intent) intentField.get(r); String realActivity = intent.getStringExtra("real_activity" ); intent.setClassName(who, realActivity); } return false ; } }); }
4.3 代理模式 vs Hook 模式对比
维度
代理模式
Hook 模式
复杂度
低(转发生命周期即可)
高(需要理解系统内部流程)
兼容性
较好(不依赖 @hide API)
差(每个 Android 版本可能需要调整 Hook 点)
插件 Activity 限制
不继承 android.app.Activity
可以继承真正的 Activity
系统特性支持
有限(theme, launchMode 等需手动处理)
完整(因为系统看到的仍是”合规的”Activity)
维护成本
低
高
代表框架
RePlugin, VirtualAPK
DroidPlugin
五、Service 和 BroadcastReceiver 的插件化 5.1 Service Service 的插件化相对简单,因为 Service 可以在运行时动态注册:
public class ProxyService extends Service { private PluginService mPluginService; @Override public void onCreate () { super .onCreate(); String pluginClass = getIntent().getStringExtra("plugin_class" ); mPluginService = (PluginService) PluginManager.getInstance() .getPluginClassLoader().loadClass(pluginClass).newInstance(); mPluginService.attach(this ); mPluginService.onCreate(); } @Override public int onStartCommand (Intent intent, int flags, int startId) { return mPluginService.onStartCommand(intent, flags, startId); } }
5.2 BroadcastReceiver BroadcastReceiver 可以完全通过 registerReceiver() 动态注册,无需在 Manifest 中声明(除非是需要静态注册的系统广播)。
Class<?> receiverClass = pluginClassLoader.loadClass(receiverClassName); BroadcastReceiver receiver = (BroadcastReceiver) receiverClass.newInstance();IntentFilter filter = new IntentFilter (action);registerReceiver(receiver, filter);
5.3 ContentProvider ContentProvider 是最难插件化的组件,因为它必须声明在 Manifest 中,且初始化时机非常早(在 Application.attachBaseContext 之前)。主流的做法是在宿主 Manifest 中预注册一个代理 ContentProvider,通过 URI 参数来路由到不同的插件 Provider。
六、主流插件化框架深度对比 6.1 RePlugin(360 开源) RePlugin 是 360 团队开源的插件化框架(https://github.com/Qihoo360/RePlugin),核心特色是坑位(Pit)机制 ——在宿主 Manifest 中预注册一系列占位 Activity,每个坑位预设了不同的 launchMode 和主题。插件 Activity 运行时被分配到对应的坑位。
RePlugin 架构:
┌──────────────────────────────────────┐ │ RePlugin │ │ │ │ ┌──────────┐ ┌──────────────────┐ │ │ │ Plugin │ │ Plugin Manager │ │ │ │ Loader │ │ (安装/卸载/升级) │ │ │ └──────────┘ └──────────────────┘ │ │ │ │ ┌────────────────────────────────┐ │ │ │ Pit Manager │ │ │ │ (坑位分配与管理) │ │ │ │ - 根据 launchMode 匹配坑位 │ │ │ │ - 管理坑位的使用状态 │ │ │ └────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────┐ │ │ │ IPC Binder Bridge │ │ │ │ (宿主 ↔ 插件进程通信) │ │ │ └────────────────────────────────┘ │ └──────────────────────────────────────┘
坑位分配逻辑:
String pitAlias = PluginContainers.alloc(containerType, pluginLaunchMode, pluginTheme, pluginConfigChanges);
RePlugin 的坑位预设示例(在宿主 Manifest 中):
<activity android:name =".pit.PitStandard1" android:launchMode ="standard" /> <activity android:name =".pit.PitStandard2" android:launchMode ="standard" /> <activity android:name =".pit.PitSingleTop1" android:launchMode ="singleTop" /> <activity android:name =".pit.PitSingleTask1" android:launchMode ="singleTask" /> <activity android:name =".pit.PitSingleInstance1" android:launchMode ="singleInstance" /> <activity android:name =".pit.PitTransparent1" android:theme ="@android:style/Theme.Translucent" />
6.2 VirtualApp(商业级沙箱) VirtualApp(https://github.com/asLody/VirtualApp)采用更高阶的思路——完全在用户态模拟 Android Framework 的行为 。它 Hook 了大量系统服务(ActivityManagerService、PackageManagerService 等),在单进程中运行多个”虚拟应用”。这是一个重量级方案,主要用于应用多开。
核心 Hook 点:
VirtualApp Hook 的系统服务: ├── ActivityManagerService │ ├── startActivity │ ├── startService │ └── bindService ├── PackageManagerService │ ├── getPackageInfo │ ├── queryIntentActivities │ └── resolveIntent ├── WindowManagerService │ └── addWindow ├── NotificationManagerService │ └── enqueueNotificationWithTag ├── AlarmManagerService │ └── set └── ContentService └── notifyChange
VirtualApp 的运行模型:
Host Process (宿主应用) │ └── VirtualApp Core (虚拟引擎) │ ├── Virtual App 1 (运行在独立进程中) │ ├── AMS Hook │ ├── PMS Hook │ └── Virtual Context │ ├── Virtual App 2 │ └── ... │ └── Virtual App 3 └── ...
6.3 Shadow(腾讯开源) 腾讯的 Shadow(https://github.com/Tencent/Shadow)是较新的插件化框架,特点是将框架本身也作为插件加载,避免插件框架代码与宿主代码的耦合。其核心是”零反射”设计——使用 Transform API 在编译期注入代码。
Shadow 的核心理念:
传统插件化: Host ──────► Plugin (通过反射/接口) Shadow: Host ──► Loader Plugin ──► Business Plugin (框架代码) (业务代码)
Shadow 的关键技术:
LoadParameters :通过 compileOnly 依赖接口,编译期确定接口签名,运行时无需反射。
PluginManager 动态化 :框架代码作为插件的一部分分发,宿主只需极小量的接入代码(约 100 行)。
Transform API :编译期修改字节码,将插件中对 Context、Resources 的引用替换为 Shadow 提供的代理。
6.4 各框架对比总结
框架
开发者
核心方案
复杂度
兼容性
适用场景
DroidPlugin
360
Hook AMS + 代理
高
低
学习参考
RePlugin
360
坑位 + 代理
中
高
大型 App 插件化
VirtualAPK
滴滴
多方案融合
中
中
中小型 App
Atlas
阿里
组件化 + 动态部署
高
中
淘宝级应用
VirtualApp
Lody
虚拟 Framework
极高
中
应用多开
Shadow
腾讯
零反射 + Transform
高
高
新一代插件化
七、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 在某种程度上替代了插件化的需求——它允许应用在不重新安装的情况下获取新功能。但这与插件化的哲学不同:
动态功能模块仍需通过 Google Play 审核和签名,更新周期受平台控制。插件化可以完全自主控制更新节奏。
动态模块必须经过 Google Play Console,不能从自有服务器下发。中国市场的应用无法使用。
动态模块之间的隔离是由系统保证的(不同 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)来解决需求。
Q5: 插件化中如何处理插件和宿主的 Context 差异?
插件中的 Context 不能直接使用宿主的 Context,因为:
宿主 Context 无法访问插件的资源(AssetManager 中没有插件的 resource path)。
宿主 Context 的包名、应用信息与插件不同。
宿主 Context 的 ClassLoader 可能加载不到插件的类。
解决方案是创建插件的专用 Context:
public Context createPluginContext (Context hostContext, String pluginApkPath, ClassLoader pluginClassLoader) { AssetManager am = AssetManager.class.newInstance(); am.getClass().getMethod("addAssetPath" , String.class).invoke(am, pluginApkPath); Resources pluginResources = new Resources (am, hostContext.getResources().getDisplayMetrics(), hostContext.getResources().getConfiguration()); Context pluginContext = new ContextWrapper (hostContext) { @Override public Resources getResources () { return pluginResources; } @Override public AssetManager getAssets () { return am; } @Override public ClassLoader getClassLoader () { return pluginClassLoader; } @Override public String getPackageName () { return pluginPackageName; } }; return pluginContext; }
这个插件 Context 在所有插件代码中使用(通过全局替换),确保插件所有资源访问和类加载都正确路由。
核心参考源码路径:
DexClassLoader:libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
BaseDexClassLoader:libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
DexPathList:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
ClassLoader:libcore/libart/src/main/java/java/lang/ClassLoader.java
AssetManager:frameworks/base/core/java/android/content/res/AssetManager.java
Resources:frameworks/base/core/java/android/content/res/Resources.java
ActivityThread:frameworks/base/core/java/android/app/ActivityThread.java
Instrumentation:frameworks/base/core/java/android/app/Instrumentation.java
AMS:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.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