2.Android 3分钟跑通Shadow官方插件化Demo(Maven版):宿主/管理器/插件三工程(实战)

摘要

腾讯开源的 Shadow 是一个无 Hook、零反射、支持四大组件 的工业级 Android 插件化框架,已在 QQ 等亿级应用中稳定运行多年。

本文基于官方 Maven 二进制依赖 方式,手把手带你 3 分钟跑通官方 Demo ,深入解析 宿主 × 管理器 × 插件 三大工程的职责、协作机制与源码原理,助你快速迈向生产级插件化实践。


一、目标与背景

✅ 本文目标

  • 完全通过 Maven Central 依赖(非源码依赖)构建;
  • 成功运行官方 projects/sample/maven 示例;
  • 理解 Shadow 的 三工程架构设计 及其生产意义。

🔗 官方资源

⚠️ 重要提示(来自官方文档):

"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,会报错:

    csharp 复制代码
    Caused 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-loadersample-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 :自定义运行时实现,提供 ContextResourcesInstrumentation 等关键对象的代理,确保插件能正确访问资源并响应系统调用。
构建机制
  • 使用 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 依赖:

    arduino 复制代码
    implementation '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 效果

  1. 宿主 App:显示 "Load Plugin" 按钮;
  2. 点击后 :成功跳转至插件中的 PluginMainActivity
  3. 插件功能:可正常启动 Activity、Service,访问资源,完整运行。

八、三大工程协作关系

8.1 架构图:

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

8.2 工作原理的流程图

这个流程图简要地展示了从用户与宿主应用交互开始,直到插件中的 Activity 被成功加载和运行的过程。每个步骤都对应了 Shadow 框架内部的重要操作:

  1. 用户点击宿主应用中的按钮:这是整个流程的起点,用户与宿主应用进行交互。
  2. 宿主调用 PluginManager 的 enter 方法:宿主应用通过 PluginManager 提供的接口发起对插件的操作请求。
  3. PluginManager 解析 fromId 并选择对应的插件 :根据传入的 fromId 来确定需要加载哪一个插件以及执行什么操作。
  4. 加载并验证插件 APK 文件:确保指定的插件 APK 存在且合法。
  5. 创建 PluginLoader 实例:为即将加载的插件准备一个加载器实例。
  6. 通过 PluginLoader 创建 ClassLoader:为插件创建一个独立的类加载器,以便于隔离不同插件间的类空间。
  7. 启动目标 Activity(由插件提供) :利用前面准备好的环境启动插件中的 Activity。
  8. Activity 生命周期由宿主代理并回调给插件:宿主代理插件 Activity 的生命周期事件,并将这些事件转发给插件处理。
  9. 插件 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 的距离!

相关推荐
C_心欲无痕2 小时前
为什么前端项目部署需要 nginx 或 Apache?
前端·nginx·apache
华如锦2 小时前
MongoDB作为小型 AI智能化系统的数据库
java·前端·人工智能·算法
bug总结2 小时前
单点登录总结速通
前端
tianxinw2 小时前
uniapp x + vue3 实现echarts图表
前端·uni-app·vue·echarts
EricLee2 小时前
2025 年终总结 - Agent 元年
前端·人工智能·后端
熏鱼的小迷弟Liu2 小时前
【消息队列】如何在RabbitMQ中处理消息的重复消费问题?
面试·消息队列·rabbitmq
xuyuan19982 小时前
超越Selenium:自动化测试框架Cypress在现代前端测试中的卓越实践(windows版本)环境搭建
前端·windows·cypress
韩zj2 小时前
app打包成apk后,在android 26上的手机,无法安装的排查
android·智能手机
高山上有一只小老虎3 小时前
SpringBoot项目集成thymeleaf实现web
前端·spring boot·后端