摘要
腾讯开源的 Shadow 是一个无 Hook、零反射、支持四大组件 的工业级 Android 插件化框架,已在 QQ 等亿级应用中稳定运行多年。
本文基于官方
Maven 二进制依赖方式,手把手带你 3 分钟跑通官方 Demo ,深入解析 宿主 × 管理器 × 插件 三大工程的职责、协作机制与源码原理,助你快速迈向生产级插件化实践。

一、目标与背景
✅ 本文目标
- 完全通过 Maven Central 依赖(非源码依赖)构建;
- 成功运行官方
projects/sample/maven示例; - 理解 Shadow 的 三工程架构设计 及其生产意义。
🔗 官方资源
- GitHub 地址:github.com/Tencent/Sha...
- Sample 文档:projects/sample/README.md
⚠️ 重要提示(来自官方文档):
"
maven目录下的 3 个工程在实际业务中大概率是 3 个独立代码库 。它们之间没有 Gradle 依赖关系 ,甚至 Shadow 版本号都是各自独立配置的。"因此,请务必 用 Android Studio 分别打开这三个目录!
二、三大工程角色说明
表格
| 工程 | 角色 | 是否可独立运行 | 输出产物 |
|---|---|---|---|
host-project |
宿主 App | ✅ 有 Launcher 图标 | APK |
manager-project |
插件管理器 | ❌ 无图标(纯 Library) | AAR |
plugin-project |
插件 App | ❌ 无图标(被动态加载) | APK |



💡 设计哲学:三者完全解耦,模拟真实微服务化开发场景------宿主不关心插件实现,插件不依赖宿主代码。
三、环境配置:解决编译问题(关键!)
原始 Sample 基于旧版工具链,需升级以下三项以兼容 JDK 17 + AGP 7.4+ :
1. 升级 Gradle Wrapper
修改每个项目的 gradle/wrapper/gradle-wrapper.properties:
ini
distributionUrl=https://mirrors.tencent.com/gradle/gradle-7.5-bin.zip
2. 使用 JDK 17
-
Android Studio Flamingo / Giraffe 默认使用 JDK 17;
-
若使用 JDK 8/11,会报错:
csharpCaused by: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed
3. 升级 Android Gradle Plugin (AGP)
在每个项目的 Project 级 build.gradle 中:
是7.0.2 不是7.4.2 否则编译报错
arduino
classpath 'com.android.tools.build:gradle:7.0.2'
否则会遇到模块访问限制错误:
arduino
Unable to make field private final java.lang.String java.io.File.path accessible
✅ 验证路径:
File > Project Structure > SDK Location > JDK location
四、分步构建与集成
4.1 构建插件工程(plugin-project)
项目结构
bash
plugin-project/
├── plugin-app/ # 插件业务模块(含 Activity)
├── sample-loader/ # 插件加载器
└── sample-runtime/ # 插件运行时环境
核心职责
plugin-app:编写插件业务逻辑,必须使用独立 applicationId;sample-loader:负责加载插件 DEX,创建 PluginClassLoader;sample-runtime:提供 Context、Resource、Instrumentation 等代理实现。
构建命令
bash
cd plugin-project
./gradlew packageDebugPlugin
输出文件
这个目录下生成的: plugin-project\build\
lua
F:\shadow0907\Shadow-master\projects\sample\maven\plugin-project\build\plugin-debug.zip
然后执行
bash
adb push build/plugin-debug.zip /data/local/tmp
⚠️ 重命名 :必须改为
sample-plugin-debug.apk(宿主代码中写死此名称!)
4.2 构建管理器工程(manager-project)
核心职责
- 实现
PluginManager接口; - 管理插件生命周期;
- 通过
fromId区分不同插件入口。
关键代码(SamplePluginManager.java)
java
@Override
public void enter(Context context, long fromId, Bundle bundle, EnterCallback callback) {
if (fromId == FROM_ID_START_ACTIVITY) {
startPluginActivity(context, bundle, callback);
} else {
throw new IllegalArgumentException("不认识的fromId==" + fromId); // ← 常见崩溃点!
}
}
💡 注意 :确保
FROM_ID_START_ACTIVITY = 1001与宿主调用一致!
构建命令
bash
cd manager-project
./gradlew assembleDebug
输出 F:\shadow0907\Shadow-master\projects\sample\maven\manager-project\sample-manager\build\outputs\apk\debug\sample-manager-debug.apk
然后执行
lua
adb push sample-manager/build/outputs/apk/debug/sample-manager-debug.apk /data/local/tmp
4.3 配置宿主工程(host-project)
直接运行,就可以了
启动插件
kotlin
// MainActivity.java
mPluginManager.enter(this, 1001, "com.tencent.shadow.sample.plugin_main.MainActivity");
✅
1001必须与SamplePluginManager中处理的fromId一致!
五、常见问题排查
❌ 崩溃:IllegalArgumentException: 不认识的fromId==1001
是源码sample-manager-debug.apk,不匹配! 源码已经推送了一个sample-manager-debug.apk,是不匹配的
原因 :SamplePluginManager.enter() 未处理 fromId=1001。
解决 :检查 manager-project 中是否定义了对应常量,或统一 ID。
❌ 插件无法加载 / 黑屏
- 检查 APK 是否放入
assets且命名正确; - 检查插件是否包含
sample-loader和sample-runtime; - 强烈建议三工程使用相同 Shadow 版本 (如
2.0.19)。
SamplePluginManager,在源码里面,需要改源码才行!
SamplePluginManager是在manager里面的
看下manager是哪个apk
php
if (isProcess(application, application.getPackageName())) {
FixedPathPmUpdater fixedPathPmUpdater
= new FixedPathPmUpdater(new File("/data/local/tmp/sample-manager-debug.apk"));
boolean needWaitingUpdate
= fixedPathPmUpdater.wasUpdating()//之前正在更新中,暗示更新出错了,应该放弃之前的缓存
|| fixedPathPmUpdater.getLatest() == null;//没有本地缓存
Future<File> update = fixedPathPmUpdater.update();
if (needWaitingUpdate) {
try {
update.get();//这里是阻塞的,需要业务自行保证更新Manager足够快。
} catch (Exception e) {
throw new RuntimeException("Sample程序不容错", e);
}
}
sPluginManager = new DynamicPluginManager(fixedPathPmUpdater);
出现了这个错误:加载的插件不匹配,源码和maven的插件搞混了!
php
encent.shadow.sample.host E FATAL EXCEPTION: main
Process: com.tencent.shadow.sample.host:plugin, PID: 24952
java.lang.RuntimeException: Unable to create service com.tencent.shadow.sample.introduce_shadow_lib.MainPluginProcessService: java.lang.IllegalStateException: PPS出现多实例
at android.app.ActivityThread.handleCreateService(ActivityThread.java:6025)
at android.app.ActivityThread.-$$Nest$mhandleCreateService(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:3080)
at android.os.Handler.dispatchMessage(Handler.java:114)
at android.os.Looper.loopOnce(Looper.java:266)
at android.os.Looper.loop(Looper.java:361)
at android.app.ActivityThread.main(ActivityThread.java:10320)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:675)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1002)
Caused by: java.lang.IllegalStateException: PPS出现多实例
at com.tencent.shadow.dynamic.host.BasePluginProcessService.onCreate(BasePluginProcessService.java:32)
at android.app.ActivityThread.handleCreateService(ActivityThread.java:6012)
at android.app.ActivityThread.-$$Nest$mhandleCreateService(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:3080)
at android.os.Handler.dispatchMessage(Handler.java:114)
at android.os.Looper.loopOnce(Looper.java:266)
at android.os.Looper.loop(Looper.java:361)
at android.app.ActivityThread.main(ActivityThread.java:10320)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:675)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1002)
六、核心源码简析
Shadow 的设计高度模块化,三大工程各司其职。下面分别从源码层面解析其关键类与运行原理。
6.1 plugin-project ------ 插件应用工程
核心职责
将业务代码打包为可被动态加载的插件 APK,并在运行时适配宿主环境。
关键类说明
PluginApplication:插件的 Application 基类,替代原生Application,用于初始化插件运行时上下文。PluginMainActivity:插件的主 Activity,继承自PluginActivity,通过 Shadow 代理机制实现生命周期回调。SampleRuntime:自定义运行时实现,提供Context、Resources、Instrumentation等关键对象的代理,确保插件能正确访问资源并响应系统调用。
构建机制
- 使用 Shadow 提供的 Gradle 插件
shadow.pluginDSL进行打包; - 生成的是标准 APK,但内部类字节码会被重写(如 Activity 继承关系替换),以兼容宿主的 ClassLoader 和 Context 体系;
- 最终插件 APK 不依赖宿主代码,实现完全解耦。
工程结构与分层
php
include ':plugin-app' // 插件业务模块(含 Activity、Service)
include ':sample-runtime' // 插件运行时环境(Context/Resource 代理)
include ':sample-loader' // 插件加载器(负责 Dex 加载与 ClassLoader 构建)
✅ 三者协同工作:
loader加载 →runtime代理 →app执行业务。
6.2 manager-project ------ 插件管理器工程
⚠️ 注意 :Maven 依赖版本与 GitHub 源码可能存在差异,建议统一使用相同版本(如
2.0.19)以避免兼容性问题。
核心职责
作为宿主与插件之间的桥梁,负责:
- 插件 APK 的加载与校验;
- 创建
PluginLoader实例; - 启动插件组件(Activity / Service);
- 支持多插件、多入口场景(通过
fromId区分)。
关键类说明
SamplePluginManager:继承FastPluginManager,实现enter()方法,根据fromId路由到不同插件功能;SampleComponentManager:管理插件中四大组件的注册与查找;SamplePluginManagerUpdater:插件更新策略实现(如版本比对、热更新触发)。
典型流程
ini
宿主调用 enter(fromId=1001)
→ Manager 解析 fromId
→ 加载 plugin-app-debug.apk
→ 创建 PluginLoader
→ 启动目标 Activity
6.3 host-project ------ 宿主应用工程
核心职责
提供插件运行的容器环境,并触发插件加载流程。
依赖关系
-
仅需引入 Maven 依赖:
arduinoimplementation 'com.tencent.shadow.dynamic:dynamic-host:2.0.19' -
无需显式依赖
introduce-shadow-lib:该库已作为传递依赖由dynamic-host自动引入(见其 POM 文件声明)。
关键类说明
MainActivity:宿主主界面,点击按钮后调用插件管理器;DynamicPluginManager:宿主侧插件管理接口,封装了与 Manager 工程的交互逻辑;Shadow:框架全局入口类,用于初始化或获取插件上下文(高级用法)。
启动插件示例
kotlin
// MainActivity.java
mPluginManager.enter(
this,
1001, // fromId,需与 Manager 中处理逻辑一致
"com.tencent.shadow.sample.plugin_main.MainActivity"
);
✅ 整个过程对宿主透明:宿主不感知插件具体实现,仅通过标准接口通信。
七、Demo 效果

- 宿主 App:显示 "Load Plugin" 按钮;
- 点击后 :成功跳转至插件中的
PluginMainActivity; - 插件功能:可正常启动 Activity、Service,访问资源,完整运行。
八、三大工程协作关系
8.1 架构图:

- 宿主 :仅依赖
dynamic-host,通过接口调用管理器; - 管理器:桥接宿主与插件,控制加载策略;
- 插件:标准 APK,但通过 Shadow DSL 打包,类被重写以适配宿主环境。
8.2 工作原理的流程图

这个流程图简要地展示了从用户与宿主应用交互开始,直到插件中的 Activity 被成功加载和运行的过程。每个步骤都对应了 Shadow 框架内部的重要操作:
- 用户点击宿主应用中的按钮:这是整个流程的起点,用户与宿主应用进行交互。
- 宿主调用 PluginManager 的 enter 方法:宿主应用通过 PluginManager 提供的接口发起对插件的操作请求。
- PluginManager 解析 fromId 并选择对应的插件 :根据传入的
fromId来确定需要加载哪一个插件以及执行什么操作。 - 加载并验证插件 APK 文件:确保指定的插件 APK 存在且合法。
- 创建 PluginLoader 实例:为即将加载的插件准备一个加载器实例。
- 通过 PluginLoader 创建 ClassLoader:为插件创建一个独立的类加载器,以便于隔离不同插件间的类空间。
- 启动目标 Activity(由插件提供) :利用前面准备好的环境启动插件中的 Activity。
- Activity 生命周期由宿主代理并回调给插件:宿主代理插件 Activity 的生命周期事件,并将这些事件转发给插件处理。
- 插件 Activity 正常运行,并可以访问其资源:最终,插件中的 Activity 可以像普通应用一样正常运行,并能够访问自身的资源文件。
8.3 demo中的代码示例:
宿主中调用
java
PluginManager pluginManager = InitApplication.getPluginManager();
pluginManager.enter(MainActivity.this, FROM_ID_START_ACTIVITY, new Bundle(), new EnterCallback() {
@Override
public void onShowLoadingView(View view) {
MainActivity.this.setContentView(view);//显示Manager传来的Loading页面
}
@Override
public void onCloseLoadingView() {
MainActivity.this.setContentView(linearLayout);
}
@Override
public void onEnterComplete() {
v.setEnabled(true);
}
});
manager-project中调用
java
SamplePluginManager
public class SamplePluginManager extends FastPluginManager {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private Context mCurrentContext;
public SamplePluginManager(Context context) {
super(context);
mCurrentContext = context;
}
/**
* @return PluginManager实现的别名,用于区分不同PluginManager实现的数据存储路径
*/
@Override
protected String getName() {
return "sample-manager";
}
/**
* @return 宿主中注册的PluginProcessService实现的类名
*/
@Override
protected String getPluginProcessServiceName() {
return "com.tencent.shadow.sample.introduce_shadow_lib.MainPluginProcessService";
}
@Override
public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {
if (fromId == Constant.FROM_ID_START_ACTIVITY) {
bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH, "/data/local/tmp/plugin-debug.zip");
bundle.putString(Constant.KEY_PLUGIN_PART_KEY, "sample-plugin");
bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME, "com.tencent.shadow.sample.plugin.MainActivity");
Log.e("tengxun","enter333333333"+fromId);
onStartActivity(context, bundle, callback);
} else if (fromId == Constant.FROM_ID_CALL_SERVICE) {
Log.e("tengxun","enter22222"+fromId);
callPluginService(context);
} else {
Log.e("tengxun","enter"+fromId);
throw new IllegalArgumentException("========不认识的fromId==" + fromId);
}
}
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
Log.e("tengxun","onStartActivity");
final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
Log.e("tengxun","pluginZipPath:"+pluginZipPath);
Log.e("tengxun","partKey:"+partKey);
Log.e("tengxun","className:"+className);
if (className == null) {
throw new NullPointerException("className == null");
}
final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
if (callback != null) {
final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
callback.onShowLoadingView(view);
}
executorService.execute(new Runnable() {
@Override
public void run() {
try {
InstalledPlugin installedPlugin
= installPlugin(pluginZipPath, null, true);//这个调用是阻塞的
Intent pluginIntent = new Intent();
pluginIntent.setClassName(
context.getPackageName(),
className
);
if (extras != null) {
pluginIntent.replaceExtras(extras);
}
startPluginActivity(context, installedPlugin, partKey, pluginIntent);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (callback != null) {
Handler uiHandler = new Handler(Looper.getMainLooper());
uiHandler.post(new Runnable() {
@Override
public void run() {
callback.onCloseLoadingView();
callback.onEnterComplete();
}
});
}
}
});
}
private void callPluginService(final Context context) {
final String pluginZipPath = "/data/local/tmp/plugin-debug.zip";
final String partKey = "sample-plugin";
final String className = "com.tencent.shadow.sample.plugin.MyService";
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
executorService.execute(new Runnable() {
@Override
public void run() {
try {
InstalledPlugin installedPlugin
= installPlugin(pluginZipPath, null, true);//这个调用是阻塞的
loadPlugin(installedPlugin.UUID, partKey);
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
boolean callSuccess = mPluginLoader.bindPluginService(pluginIntent, new PluginServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder);
try {
String s = iMyAidlInterface.basicTypes(1, 2, true, 4.0f, 5.0, "6");
Log.i("SamplePluginManager", "iMyAidlInterface.basicTypes : " + s);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
throw new RuntimeException("onServiceDisconnected");
}
}, Service.BIND_AUTO_CREATE);
if (!callSuccess) {
throw new RuntimeException("bind service失败 className==" + className);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
8.4 核心类
FastPluginManager:Shadow 提供的插件管理器基类,封装了插件加载、ClassLoader 创建等通用逻辑。SamplePluginManager:继承FastPluginManager的具体实现,根据 fromId 路由并加载指定插件 APK。InitApplication:宿主 Application 子类,用于在应用启动时初始化插件管理器实例。FixedPathPmUpdater:固定路径插件更新器,从预设本地路径(如 assets)加载插件,适用于调试场景
| 类名 | 角色 | 一句话说明 |
|---|---|---|
FastPluginManager |
框架基类 | 提供插件加载通用能力 |
SamplePluginManager |
业务实现 | 决定"加载哪个插件、怎么加载" |
InitApplication |
宿主初始化 | 在宿主启动时准备好插件管理器 |
FixedPathPmUpdater |
更新策略 | 从固定路径(如 assets)读取插件 APK |
宿主中清单文件的注册:
ini
<service
android:name=".MainPluginProcessService"
android:process=":plugin" />
<!--container 注册
注意configChanges需要全注册
theme需要注册成透明
这些类不打包在host中,打包在runtime中,以便减少宿主方法数增量
-->
<activity
android:name="com.tencent.shadow.sample.runtime.PluginDefaultProxyActivity"
android:launchMode="standard"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:hardwareAccelerated="true"
android:theme="@style/PluginContainerActivity"
android:process=":plugin" />
<activity
android:name="com.tencent.shadow.sample.runtime.PluginSingleInstance1ProxyActivity"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:hardwareAccelerated="true"
android:theme="@style/PluginContainerActivity"
android:process=":plugin" />
<activity
android:name="com.tencent.shadow.sample.runtime.PluginSingleTask1ProxyActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:hardwareAccelerated="true"
android:theme="@style/PluginContainerActivity"
android:process=":plugin" />
<provider
android:authorities="com.tencent.shadow.contentprovider.authority.dynamic"
android:name="com.tencent.shadow.core.runtime.container.PluginContainerContentProvider" />
<!--container 注册 end -->
九、总结
通过本文,你已掌握:
✅ 环境配置 :Gradle、JDK、AGP 正确版本
✅ 三工程职责 :宿主(入口)、管理器(桥梁)、插件(业务)
✅ 构建流程 :插件 → 管理器 → 宿主集成
✅ 问题排查 :fromId 匹配、APK 命名、版本统一
✅ 架构理解:解耦设计、生产就绪模式
📦 下一步建议:
- 将插件 APK 改为网络下载 + 签名校验;
- 支持多插件动态切换;
- 结合 CI/CD 自动化构建插件包。
Shadow 以"无侵入、高兼容、全组件支持" 的特性,为 Android 插件化提供了工业级解决方案。现在,你离"模块化热更新"只差一个 Demo 的距离!