5.Android 如何用腾讯Shadow在双11电商场景的完整复盘(实战2年),实现热修复(全网最详细实战案例)

1 阅读54分钟

写在前面

Shadow是非常强,之前的博客也介绍了很多! 这次完整的实战

两年前,我们团队满怀激情地引入了腾讯 Shadow 插件化框架,信心满满地要在双11大促中大展拳脚。当时的想法很简单:活动页面上线再也不用等应用商店审核,老用户也能第一时间体验新功能,包体积也能控制住。

“Shadow 真香!”是我们当时的一致评价。

然而,经过两年真实业务场景的迭代,我们逐渐发现了一些当初文档里没写到、社区里没人提过的“坑”。多插件相互依赖像一团乱麻,代码调试如同大海捞针,插件化与组件化共存的架构让人头秃,宿主与插件之间、插件与插件之间的通信写接口写到怀疑人生。

今天,我就把这 2 年踩过的坑、总结的经验,以双11电商业务为背景,完整地分享给大家。希望能让后来者少走一些弯路。比如下面的:

1).多插件的相互依赖

2).代码的调试

3).插件化和组件化的一起使用,架构的修改

4).复杂的业务,插件和宿主,宿主和插件的通信,要写很多接口,需要把协议转义


1. 业务背景

一个完整的电商双11大促插件化系统:

业务场景:双11当天0点,需要上线全新的「限时秒杀」活动页面,包含倒计时、库存秒杀、弹幕互动等复杂功能。传统发版需等待应用商店审核,而插件化可以实现:

  • 11月10日23:50 提前预加载秒杀插件
  • 11月11日0:00 立即激活秒杀页面
为啥我的电商项目用插件化?
  1. 常规发版需要7-15天审核周期,错过营销时机 经常有很多活动
  2. 老用户无法体验最新的功能,必须发版本,强制升级才行! 但有的用户不喜欢强制升级!
  3. 包体积膨胀:每次新增功能都增加APK体积,影响下载转化率
  4. 灵活性差:无法针对不同用户群体展示不同购物流程

2. 详细的双11电商需求 (Shadow使用的10大核心需求)

2.1. 项目效果图

1000031560_免费视频转GIF_20260325_132113.gif

2.2. 详细的功能清单列表
需求业务场景技术难点与解决方案
1. 加载双11插件点击Tab或App启动时加载活动页面核心:通过 DynamicPluginManager.enter() 动态加载
2. 插件崩溃处理活动插件崩溃,不能影响主App核心:进程隔离 + 自动回退/降级策略,实现用户级别灰度
3. 卸载插件活动结束,释放本地磁盘空间核心:PPSController.exit() + 删除文件 + 清理数据库记录
4. 加载Activity点击商品,跳转插件内的详情页核心:插件内Activity继承 PluginContainerActivity,宿主注册容器
5. 加载Fragment首页Tab嵌入活动页核心:宿主通过插件的ClassLoader加载Fragment实例并注入
6. 宿主↔插件通信获取用户信息、同步购物车核心:定义公共接口,通过Bundle传递Binder或直接通过ClassLoader调用
7. 插件↔插件通信双11插件添加商品,通知购物车插件核心:通过Loader作为服务注册中心,利用Binder跨ClassLoader调用
8. 热更新策略多插件版本管理、增量更新核心:Manager负责版本检测、依赖排序、下载安装,实现全动态化
9. 热修复类/资源修复优惠券计算逻辑、替换侵权图片核心:本质是插件全量更新,Manager下载新版插件包替换旧版
10. 热修复SO库替换有崩溃的图片加载SO库核心:同样是插件全量更新,重启插件进程后加载新SO库

3. 电商App的架构

双11,新增了一个双11tab的入口

架构对比:常规 vs. 双11

常规架构(发版固定)

首页  ——▶ 分类  ——▶ 购物车 ——▶ 我的
(Native) (Native)  (Native) (Native)

双11架构(动态插入)

       【新增插件化入口】
              ↑
首页  ——▶ 双11 ——▶ 分类 ——▶ 购物车 ——▶ 我的
(Native) (插件)    (Native)  (Native)   (Native)
 插件化实现
3.1. 模块划分图

宿主:Host 插件: 首页, 分类,购物,我的,新增双11主会场

电商主APP (宿主)
├── 插件管理层 (插件Manager)
├── Shadow引擎层(宿主)
└── 核心业务插件 (插件)
    ├── 首页
    ├── 分类  
    ├── 购物车 
    ├── 我的
    └── 双11插件(shadow)
3.2. 整体的架构是怎么样的?分层 (需要修改插件)
Root project 'ATaoDuoduoShadow'
+--- Project ':app'                                # 宿主应用
+--- Project ':introduce-shadow-lib'               # 宿主的Shadow核心库
+--- Project ':plugin-app'                         # 双11插件应用(包含秒杀、弹幕等复杂功能)
+--- Project ':sample-loader'                      # Shadow插件加载器(负责加载插件APK)
+--- Project ':sample-manager'                     # Shadow插件管理器(管理插件生命周期、版本)
+--- Project ':sample-runtime'                     # Shadow运行时环境(提供插件运行所需组件)
\--- Project ':shadow_common'                      # shadow 公共库(宿主与插件共享的接口)
3.3. 整体业务流程图

宿主加载插件,对于上面流程图的调用

shadow的流程.png


4. 项目搭建过程:集成shadow

考虑下面2个问题

Q1: shadow框架中,插件 能不能依赖host,如果不能依赖,那么很多插件之间使用到的公共库要各自自己引入吗? 插件一般是依赖宿主的

shadow插件化Maven的封装思想:(比较科学) 用这个插件和宿主,公共的模块怎么共享 依赖关系不要错了: 插件依赖宿主工程!

Q2: shadow插件化源码的封装思想:(太复杂了,代码量大的惊人)

项目中通过maven集成shadow的步骤

4.1 宿主工程搭建---APP-project:

用的1.8的jdk进行编译的!

用17编译,然后项目的的gradle版本是这个!

classpath 'com.android.tools.build:gradle:7.0.2' 

1.已经有了一个项目 2.集成host工程 拷贝introduce-shadow-lib

在host工程中添加依赖:

implementation project(':introduce-shadow-lib')
//如果introduce-shadow-lib发布到Maven,在pom中写明此依赖,宿主就不用写这个依赖了。
implementation "com.tencent.shadow.dynamic:host:$shadow_version"

把类拷贝过来:MyApplication

4.2 主工程集成插件:插件集成shadow

插件工程创建,和宿主放在一起,然后插件中依赖shadow 插件里面的这2个类是干嘛的? runtime和loader

include ':sample-runtime'
include ':sample-loader'

用到的是application,而不是libray

applicationId "com.tencent.shadow.sample.loader"//applicationId不重要

这3个apk是的关系是怎么样的? 如何相互工作,相互调用的

1).创建插件的主工程,在一个项目中

编译项目: 插件:

./gradlew packageDebugPlugin

4.3 主工程集成Manager插件,Manager集成shadow

manager:

cd manager-project ./gradlew assembleDebug

4.4 运行主工程,加载插件逻辑

这个流程和之前博客写的是一样的,push apk,安装apk

主宿中用插件一样的包名,否则报一下的错! 出现多实例,出现包名不匹配的提示!

image.png

image.png


5. 具体完整的10项需求的源码和实战方案

5.1. 如何加载一个双11活动插件,双11限时秒杀活动

业务场景:双11当天0点,需要上线全新的「限时秒杀」活动页面,包含倒计时、库存秒杀、弹幕互动等复杂功能

核心原理:宿主不直接加载业务插件,而是加载 ManagerManager 通过 DexClassLoader 反射调用 ManagerFactoryImpl,获取 PluginManager 实例,再由该实例去加载具体的业务插件。

5.1.1 打包manager.zip

这个manger是在apk中,那怎么改变manger

DynamicPluginManager SamplePluginManager: 是怎么加载的?

Shadow 官方推荐使用 PluginContainerActivity 的方式:

关键的问题: 1.怎么使用manager中的SamplePluginManager!!! (搞定)

关键原理:宿主如何“调用” SamplePluginManager?

宿主 并不直接引用 SamplePluginManager,而是:

  1. DynamicPluginManager 读取 ZIP 中的 manager.apk
  2. 用 DexClassLoader 加载 manager.apk
  3. 反射调用
Class<?> factoryClass = classLoader.loadClass("com.tencent.shadow.dynamic.impl.ManagerFactoryImpl");
ManagerFactory factory = (ManagerFactory) factoryClass.newInstance();
PluginManagerImpl manager = factory.buildManager(context);

DynamicPluginManager,里面调用了实现

public void enter(Context context, long fromId, Bundle bundle, EnterCallback callback) {
    if (mLogger.isInfoEnabled()) {
        mLogger.info("enter fromId:" + fromId + " callback:" + callback);
    }
    updateManagerImpl(context);
    mManagerImpl.enter(context, fromId, bundle, callback);
    mUpdater.update();
}

FastPluginManager的源码需要多看看!

5.1.2 打包双11的APK

打包插件

# 编译并打包插件
./gradlew :plugin-double11:packageDebugPlugin

# 输出文件位置
# plugin-double11/build/outputs/plugin/debug/plugin-double11-debug.zip
5.1.3 插件配置
// plugin-double11/src/main/assets/config.json
{
  "partKey": "double11",
  "version": 1,
  "uuid": "double11-group",
  "hostWhiteList": [
    "com.double11.app.MainActivity",
    "com.double11.app.CartActivity"
  ],
  "dependencies": []
}
5.1.4 宿主中的加载插件的核心逻辑
// 宿主:Double11PluginLoader.java
public class Double11PluginLoader {
    private DynamicPluginManager mPluginManager;
    
    public void loadDouble11Plugin() {
        // 1. 获取插件管理器
        mPluginManager = (DynamicPluginManager) 
            Shadow.getPluginManager();
        
        // 2. 构建加载参数
        Bundle bundle = new Bundle();
        bundle.putString("activityClassName", 
            "com.double11.plugin.double11.FlashSaleActivity");
        bundle.putString("pageTitle", "双11限时秒杀");
        
        // 3. 加载并启动插件
        mPluginManager.enter(
            getApplicationContext(),
            1001,  // fromId
            bundle,
            new EnterCallback() {
                @Override
                public void onSuccess() {
                    Log.d("Double11", "插件加载成功");
                }
                
                @Override
                public void onError(String msg) {
                    Log.e("Double11", "插件加载失败: " + msg);
                    // 降级到H5页面
                    loadH5Fallback();
                }
            }
        );
    }
    
    private void loadH5Fallback() {
        Intent intent = new Intent(this, WebViewActivity.class);
        intent.putExtra("url", "https://m.double11.com/flashsale");
        startActivity(intent);
    }
}

5.2. 插件奔溃,怎么处理,双11活动插件有奔溃问题

业务场景:双11活动插件出现崩溃,不能影响主 App 使用。

核心原则:插件崩溃不影响宿主,并支持自动恢复或降级。

核心设计原则

  1. 插件崩溃不影响宿主
  2. 版本更新(插件升级)
  3. 支持用户级别降级(插件降级)
  4. 异常自动上报,方便定位问题

说白了就是:强制更新插件和强制回退插件

崩溃处理与插件更新的关系图

┌─────────────────────────────────────────────────────────────────┐
│                        用户使用插件                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │   发生崩溃       │
                    └─────────────────┘
                              │
                              ▼
              ┌───────────────────────────────┐
              │     PluginCrashHandler        │
              │     记录崩溃信息并上报          │
              └───────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌───────────────┐   ┌───────────────┐   ┌───────────────┐
│  偶发崩溃      │   │  版本Bug      │   │  严重问题      │
│  (1-2次)      │   │  (3次以上)    │   │  (崩溃率>10%) │
└───────────────┘   └───────────────┘   └───────────────┘
        │                     │                     │
        ▼                     ▼                     ▼
┌───────────────┐   ┌───────────────┐   ┌───────────────┐
│  重启插件      │   │  检查更新      │   │  回退版本      │
│  清理现场      │   │  下载新版本    │   │  使用旧版本    │
└───────────────┘   └───────────────┘   └───────────────┘
        │                     │                     │
        └─────────────────────┼─────────────────────┘
                              ▼
                    ┌─────────────────┐
                    │   继续使用       │
                    └─────────────────┘

5.3. 如何卸载插件?

业务场景:11月12日00:00活动结束:

空间回收:删除临时插件,释放50-100MB存储空间,双11活动结束,插件在本地暂用磁盘

插件卸载 - 核心实现

流程图

┌─────────────────────────────────────────────────────────────┐
│                     双11活动结束                              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
              ┌───────────────────────────────┐
              │   markForUninstall("double11") │
              │   标记待卸载 + 延迟7天          │
              └───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    7天后自动触发                             │
└─────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌───────────────┐   ┌───────────────┐   ┌───────────────┐
│ 1.停止插件     │   │ 2.删除文件     │   │ 3.清理记录     │
│ exit()        │──▶│ deleteRecursive│──▶│ clearDB()     │
└───────────────┘   └───────────────┘   └───────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │   释放空间       │
                    │   ~50-100MB     │
                    └─────────────────┘

核心代码

// PluginUninstallManager.java - 核心卸载逻辑
public class PluginUninstallManager {
    
    /**
     * 卸载插件(核心3步)
     */
    public static void uninstall(String partKey) {
        // 1. 停止插件进程
        Shadow.getPPSController().exit(partKey);
        
        // 2. 删除插件文件
        File pluginDir = new File(context.getFilesDir(), "ShadowPlugin/" + partKey);
        deleteFile(pluginDir);
        
        // 3. 清理数据库记录
        Shadow.getInstalledDao().deleteByPartKey(partKey);
        
        Log.d("Uninstall", "插件已卸载: " + partKey);
    }
    
    // 递归删除文件/文件夹
    private static void deleteFile(File file) {
        if (file == null || !file.exists()) return;
        
        if (file.isDirectory()) {
            File[] children = file.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteFile(child);
                }
            }
        }
        file.delete();
    }
    
    /**
     * 延迟卸载(活动结束7天后)
     */
    public static void scheduleUninstall(String partKey, long endTime) {
        long delay = endTime + 7 * 24 * 3600 * 1000 - System.currentTimeMillis();
        
        if (delay <= 0) {
            // 已过期,立即卸载
            uninstall(partKey);
        } else {
            // 延迟卸载
            new Handler().postDelayed(() -> uninstall(partKey), delay);
        }
    }
}

5.4. 如何加载插件中新增一个Activity,双11活动插件

业务场景:点击双11tab,也就是fragment页面,里面的案例,进入到双11的商品详细页面

核心原理:Shadow通过 PluginContainerActivity 作为“占坑”Activity,在运行时将插件Activity的代码“注入”进来。

实现步骤

  1. 插件内定义Activity:继承 PluginContainerActivity
  2. 宿主Manifest注册:注册 com.tencent.shadow.core.runtime.PluginContainerActivity
  3. 跳转:在插件内使用标准的 Intent 进行跳转。

插件 Activity 加载 - 核心代码

流程图

用户点击商品
    │
    ▼
┌─────────────────────────────────────────┐
│  宿主 Fragment 中点击商品                │
│  (双11插件内的 Fragment)                 │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│  跳转到插件的 Activity                   │
│  Intent(宿主包名, 插件Activity类名)      │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│  Shadow 自动处理                         │
│  Activity → PluginContainerActivity     │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│  显示商品详情页                          │
│  (插件内的 Activity)                     │
└─────────────────────────────────────────┘

核心代码

5.4.1 插件中定义 Activity
// 插件工程:ProductDetailActivity.java
package com.double11.plugin;

public class ProductDetailActivity extends PluginContainerActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_product_detail);
        
        // 获取商品ID
        String productId = getIntent().getStringExtra("product_id");
        
        // 加载商品详情
        loadProductDetail(productId);
    }
    
    private void loadProductDetail(String productId) {
        // 插件内的业务逻辑
        TextView title = findViewById(R.id.tv_title);
        title.setText("双11秒杀商品: " + productId);
    }
}
5.4.2 插件中跳转 Activity
// 插件工程:FlashSaleFragment.java
public class FlashSaleFragment extends Fragment {
    
    private void onProductClick(String productId) {
        // 方法1:标准 Intent 跳转(推荐)
        Intent intent = new Intent(getActivity(), ProductDetailActivity.class);
        intent.putExtra("product_id", productId);
        startActivity(intent);
        
        // 方法2:通过类名跳转
        // Intent intent = new Intent();
        // intent.setClassName(getActivity(), 
        //     "com.double11.plugin.ProductDetailActivity");
        // intent.putExtra("product_id", productId);
        // startActivity(intent);
    }
}
5.4.3 宿主动态加载插件 Fragment
// 宿主工程:MainActivity.java
public class MainActivity extends AppCompatActivity {
    
    // 点击双11 Tab,加载插件的 Fragment
    private void loadDouble11Fragment() {
        try {
            // 1. 获取插件 ClassLoader
            PluginParts parts = Shadow.getPluginParts("double11");
            ClassLoader classLoader = parts.getClassLoader();
            
            // 2. 加载 Fragment 类
            Class<?> fragmentClass = classLoader.loadClass(
                "com.double11.plugin.FlashSaleFragment"
            );
            
            // 3. 创建实例
            Fragment fragment = (Fragment) fragmentClass.newInstance();
            
            // 4. 添加到宿主 Activity
            getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.container, fragment)
                .commit();
                
        } catch (Exception e) {
            e.printStackTrace();
            // 降级到 H5
            loadH5Page();
        }
    }
}

AndroidManifest 配置

5.4.4 插件 Manifest(无需注册 Activity)
<!-- 插件工程:AndroidManifest.xml -->
<manifest package="com.double11.plugin">
    
    <application>
        <!-- Shadow 会自动处理 Activity,无需手动注册 -->
        <!-- ProductDetailActivity 不需要在这里声明 -->
    </application>
    
</manifest>
5.4.5 宿主 Manifest(注册插件容器)
<!-- 宿主工程:AndroidManifest.xml -->
<manifest package="com.double11.app">
    
    <application>
        <!-- 注册 Shadow 容器 Activity(必须) -->
        <activity android:name="com.tencent.shadow.core.runtime.PluginContainerActivity"
            android:configChanges="orientation|screenSize"
            android:theme="@style/PluginTheme" />
    </application>
    
</manifest>

关键点总结

要点说明代码
Activity 定义继承 PluginContainerActivityextends PluginContainerActivity
无需注册插件 Manifest 不需要注册 Activity自动处理
跳转方式标准 Intentnew Intent(context, TargetActivity.class)
宿主配置注册容器 ActivityPluginContainerActivity

5.5. 如何加载新增的Fragment,双11活动插件?(非常重要)

核心挑战:Fragment不是Shadow的一等公民,需要宿主主动获取 解决方案

  1. 获取插件的ClassLoader:通过 Shadow.getPluginParts(partKey).getClassLoader() 获取。
  2. 反射创建Fragmentclazz.newInstance()
  3. 将Fragment添加到宿主Activity:使用 FragmentManager 的 replace 方法

页面就是:DoubleElevenFragment

具体方案有如下3种:

  1. 通过插件提供的 Activity

  2. 如果插件没用Activity,宿主怎么启动插件的fragment 通过插件暴露的“工厂接口”或“服务”来获取 Fragment 实例

    第一步:定义公共接口(放在宿主和插件都能引用的模块)

  3. 宿主通过插件ClassLoader加载Fragment类,创建实例,然后将插件的Context注入到Fragment中,最后将Fragment添加到宿主Activity的Fragment容器里。

    • Fragment 并不是 Shadow 的一等公民,没有像 Activity 那样的自动代理和生命周期接管。
    • 因此,即使你手动加载 Fragment,它的 getActivity()getContext()、资源访问等行为都可能出错。

我们采用第三种方案:

5.5.1. 宿主怎么获取插件的ClassLoader,加载插件的fragment(重点)

需要自定义shadow,修改它的源码,然后发布到自己的maven! 提供一个共同的方法,插件的ClassLoader的获取

模仿的Shadow源码中的SamplePluginLoader!

先得到PluginParts

PluginParts pluginParts = getPluginParts(partKey);
String packageName = pluginParts.getApplication().getPackageName();
ApplicationInfo applicationInfo = pluginParts.getPluginPackageManager().getApplicationInfo(packageName, GET_META_DATA);
PluginClassLoader classLoader = pluginParts.getClassLoader();
Resources resources = pluginParts.getResources();
5.5.2. 通过宿主获取插件的ClassLoader,加载插件的fragment
// 获取插件的 ClassLoader
public ClassLoader getPluginClassLoader(String partKey) {
    try {
        // 1. 获取 PluginParts
        PluginParts pluginParts = Shadow.getPluginParts(partKey);
        
        // 2. 获取 ClassLoader
        ClassLoader classLoader = pluginParts.getClassLoader();
        
        return classLoader;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

// 使用示例
ClassLoader loader = getPluginClassLoader("double11");
Class<?> clazz = loader.loadClass("com.double11.plugin.FlashSaleFragment");
Fragment fragment = (Fragment) clazz.newInstance();
5.5.3 更可行的方案:宿主与插件共享同一个进程(关键!)

在 宿主与插件运行于同一个进程 的前提下(即 Shadow 配置为同进程加载),

pluginCl 必须使用插件的 ClassLoader,而不是宿主的 ClassLoader

自定义 PluginLoader 获取 ClassLoader (重要)

如果你确实需要在宿主中获取插件 ClassLoader,需要自定义 PluginLoader:

方法一:反射

如果是用fragment , 用Activity跳转,那么会有很大的问题!

用腾讯shadow插件化加载fragment, 一个宿主,主Activity,4个tab页面,3个页面是fragment。另外一个tab,点击也是加载fragment,放在插件里面,那么这个宿主如何加载这个fragment,

定义宿主 Fragment创建一个空的 PlaceholderFragment,用于占位。

宿主和插件依赖问题,他们都有相同的依赖,会不会加载有问题!

Android shadow用maven方式,有host-project, plugin-project, 现在我要在host-project中直接加载plugin-project的fragment,通过接口在common模块,宿主和插件都依赖它,IPluginUiProvider! 具体怎么做?host-project和plugin-project是2个独立的工程

1.这是最标准的做法,将 common 模块发布到 Maven 仓库,两个工程都从 Maven 依赖。

  1. 配置就会导致能看到它上面的一层结构!
include ':common'
project(':common').projectDir = new File('../common')

Q1: 不知道其他人有没有更好的办法获取ClassLoader!


5.6. 宿主与插件之间如何高效通信?

插件与宿主通信:同一个进程,通过预定义接口(Service、Callback)实现双向调用

5.6.1. 插件获取宿主信息:(用户信息:插件需要获取登录状态、收货地址)

方案一:通过插件加载时的Bundle参数传递

在加载插件时,通过Bundle传递宿主API的Binder。

// 宿主工程: PluginManager.java
public class PluginManager {
    
    public void loadPluginWithHostApi(String partKey, IHostApi hostApi) {
        Bundle extras = new Bundle();
        extras.putBinder("host_api_binder", hostApi.asBinder());
        
        // Shadow加载插件时传入extras
        mPluginLoader.loadPlugin(partKey, extras);
    }
}

// Loader插件工程: SamplePluginLoader.java
public class SamplePluginLoader implements PluginLoader {
    
    @Override
    public void loadPlugin(String partKey, Bundle extras) {
        // 保存extras中的host_api_binder
        if (extras != null) {
            IBinder hostApiBinder = extras.getBinder("host_api_binder");
            if (hostApiBinder != null) {
                setHostApi(partKey, hostApiBinder);
            }
        }
        // 继续正常的加载流程
    }
}

方案二:官方推荐的标准模式是:“接口回调”(Callback / Dependency Injection)。

核心思路

  1. 定义接口:在共享模块(Host 和 Plugin 都依赖的 module)中定义一个接口,描述宿主需要提供的能力。
  2. 宿主实现:宿主工程实现这个接口,提供具体逻辑。
  3. 传递接口:宿主在加载插件或初始化插件时,将实现类的实例传递给插件。
  4. 插件调用:插件持有该接口引用,直接调用方法。

方案:接口回调模式(官方推荐)

通过公共模块定义接口,宿主实现并传递给插件。

核心代码

5.6.1.1 公共模块定义接口 (shadow-common)
// IHostInfoProvider.java
public interface IHostInfoProvider {
    String getLoginToken();
    UserInfo getUserInfo();
    Address getDefaultAddress();
}

// 数据Bean
public class UserInfo implements Serializable {
    public String userId;
    public String nickName;
    public boolean isVip;
}

public class Address implements Serializable {
    public String receiverName;
    public String phone;
    public String fullAddress;
}
5.6.1.2 宿主实现接口
// HostInfoProviderImpl.java
public class HostInfoProviderImpl implements IHostInfoProvider {
    
    @Override
    public String getLoginToken() {
        return UserManager.getInstance().getToken();
    }
    
    @Override
    public UserInfo getUserInfo() {
        User user = UserManager.getInstance().getCurrentUser();
        UserInfo info = new UserInfo();
        info.userId = user.getId();
        info.nickName = user.getName();
        info.isVip = user.isVip();
        return info;
    }
    
    @Override
    public Address getDefaultAddress() {
        AddressEntity addr = AddressManager.getInstance().getDefaultAddress();
        Address address = new Address();
        address.receiverName = addr.getName();
        address.phone = addr.getPhone();
        address.fullAddress = addr.getFullAddress();
        return address;
    }
}
5.6.1.3 加载插件时传递接口实例
// 宿主:Double11PluginLoader.java
public void loadDouble11Plugin() {
    // 创建接口实例
    IHostInfoProvider hostApi = new HostInfoProviderImpl();
    
    // 通过Bundle传递(需实现Parcelable)
    Bundle bundle = new Bundle();
    bundle.putParcelable("host_api", (Parcelable) hostApi);
    
    mPluginManager.enter(context, 1001, bundle, callback);
}
5.6.1.4 插件接收并使用
// 插件:FlashSaleActivity.java
public class FlashSaleActivity extends PluginContainerActivity {
    
    private IHostInfoProvider mHostApi;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 获取宿主API
        Bundle bundle = getIntent().getExtras();
        if (bundle != null) {
            mHostApi = bundle.getParcelable("host_api");
        }
        
        // 使用宿主信息
        loadUserInfo();
    }
    
    private void loadUserInfo() {
        if (mHostApi == null) return;
        
        // 获取登录信息
        String token = mHostApi.getLoginToken();
        if (TextUtils.isEmpty(token)) {
            // 未登录,跳转登录页
            showLoginDialog();
            return;
        }
        
        // 获取用户信息
        UserInfo user = mHostApi.getUserInfo();
        tvNickName.setText(user.nickName);
        
        // 获取默认地址
        Address address = mHostApi.getDefaultAddress();
        tvAddress.setText(address.fullAddress);
    }
}
5.6.2. 宿主获取插件信息:(购物车同步:插件添加商品,宿主购物车实时更新)

这个就像是跳转Activity一样的原理!

宿主获取插件的方法

5.6.2.1 优化一:将自定义方法加入预定义的Loader接口

你之前的做法是通过反射去调用Loader中新增的getPluginApi方法。这在Java中是一种“侵入式”的调用,不够稳健。更好的做法是让Loader实现一个预定义的、通用的Binder接口。

  • 原理:Shadow的PluginLoader本身就是一个跨进程的Binder对象。你可以定义一个通用的Binder接口(例如IBinderLoader),并让Loader的实现类(如SamplePluginLoader)去实现它。
  • 好处:宿主可以通过BinderPluginLoader(Loader的Binder代理)直接查询这个通用接口,然后安全地转换成你的IPluginApi。这样就完全避免了反射,利用的是Binder自带的queryLocalInterface机制,代码更加健壮。
5.6.2.2 优化二:通过插件Service方式暴露接口(更推荐)

如果你的插件功能比较复杂,需要处理多个请求或长时间任务,那么将其设计为一个“插件Service”是Shadow官方思路中最标准的方式。

  • 原理:Shadow的Loader本身也是一个动态的Service。你可以在插件中创建一个自定义的Binder(继承自IPluginApi.Stub),然后在Loader中通过bindPluginService的方式将这个Binder返回给宿主。
  • 好处:这种方式最“Android化”,你的插件getString方法将作为一个标准的、跨进程的Service方法被调用。它拥有清晰的生命周期管理,能处理更复杂的交互,并且完全遵循Shadow通过Binder进行跨进程通信的设计核心。

具体代码:Loader作为中介 + Binder回调

购物车同步:插件添加商品,宿主购物车实时更新

插件将API注册到Loader,宿主通过Loader获取并调用。

5.6.2.3 公共模块定义接口 (shadow-common)
// IPluginCartApi.java - 插件提供的购物车接口
public interface IPluginCartApi extends IInterface {
    // AIDL 风格定义
    void addToCart(String productId, int count);
    void removeFromCart(String productId);
    List<CartItem> getCartItems();
    
    abstract class Stub extends Binder implements IPluginCartApi {
        public static IPluginCartApi asInterface(IBinder obj) {
            // 标准 AIDL 转换
        }
    }
}

// CartItem.java - 数据Bean
public class CartItem implements Parcelable {
    public String productId;
    public String name;
    public int count;
    public long price;
}
5.6.2.4 插件实现API并注册到Loader
// 插件工程:PluginCartApiImpl.java
public class PluginCartApiImpl extends IPluginCartApi.Stub {
    
    private CartManager cartManager = CartManager.getInstance();
    
    @Override
    public void addToCart(String productId, int count) {
        cartManager.add(productId, count);
        // 通知宿主购物车变化(通过Loader回调)
        notifyCartChanged();
    }
    
    @Override
    public void removeFromCart(String productId) {
        cartManager.remove(productId);
        notifyCartChanged();
    }
    
    @Override
    public List<CartItem> getCartItems() {
        return cartManager.getItems();
    }
}

// 插件加载时注册
public class Double11PluginLoader extends SamplePluginLoader {
    
    @Override
    public void loadPlugin(String partKey, Bundle extras) {
        super.loadPlugin(partKey, extras);
        
        // 将API注册到Manager
        IPluginCartApi cartApi = new PluginCartApiImpl();
        registerPluginApi(partKey, cartApi.asBinder());
    }
}
5.6.2.5 宿主获取并调用插件API
// 宿主工程:CartSyncManager.java
public class CartSyncManager {
    
    private IPluginCartApi mCartApi;
    
    // 获取插件API
    public void bindPluginCart(String partKey) {
        DynamicPluginManager manager = (DynamicPluginManager) Shadow.getPluginManager();
        
        // 通过Loader获取插件的Binder
        IBinder binder = manager.getPluginApi(partKey);
        if (binder != null) {
            mCartApi = IPluginCartApi.Stub.asInterface(binder);
        }
    }
    
    // 调用插件方法
    public void addToCart(String productId, int count) {
        if (mCartApi != null) {
            try {
                mCartApi.addToCart(productId, count);
                // 成功后同步到宿主购物车
                syncToHostCart();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }
    
    // 宿主购物车同步
    private void syncToHostCart() {
        try {
            List<CartItem> items = mCartApi.getCartItems();
            // 更新宿主购物车UI
            EventBus.getDefault().post(new CartUpdateEvent(items));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}
5.6.2.6 宿主接收购物车更新
// 宿主工程:CartActivity.java
public class CartActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 监听购物车更新事件
        EventBus.getDefault().register(this);
        
        // 同步双11插件的购物车
        CartSyncManager.getInstance().bindPluginCart("double11");
    }
    
    @Subscribe
    public void onCartUpdate(CartUpdateEvent event) {
        // 刷新宿主购物车列表
        adapter.setNewData(event.getItems());
        updateTotalPrice();
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }
}

事件总线方案(同进程场景)

如果插件和宿主运行在同一个进程,可以用更简单的事件总线:

// 插件添加商品后发送事件
public class Double11PluginActivity extends PluginContainerActivity {
    
    private void addToCart(String productId) {
        // 插件内部添加逻辑...
        
        // 发送全局事件
        EventBus.getDefault().post(new CartAddEvent(productId));
    }
}

// 宿主接收事件
public class CartActivity extends AppCompatActivity {
    
    @Subscribe
    public void onCartAdd(CartAddEvent event) {
        // 直接更新宿主购物车
        updateCartUI(event.getProductId());
    }
}
5.6.3. 插件和插件通信:(双11活动插件,添加商品到了购物车插件)

方案一:宿主中转模式(最推荐,解耦最彻底)

原理

  1. 宿主加载插件 B,获取其实例。
  2. 宿主将插件 B 的实例注册到共享模块的一个静态管理器中,或者通过 IHostService 接口提供给插件 A。
  3. 插件 A 通过共享模块的管理器,或者调用宿主提供的接口,间接拿到插件 B 的能力。

方案二:SPI 服务发现机制(适合多个插件提供同类服务)

如果插件 B 只是众多提供某种能力(如“支付能力”、“登录能力”)的插件之一,可以使用 Java SPI 思想。

  1. 共享模块定义 ServiceProvider 接口和 ServiceRegistry 注册表。
  2. 宿主遍历所有已加载插件,查找实现了该接口的类,注册到 ServiceRegistry
  3. 插件 A 从 ServiceRegistry 查找所需服务。

优点:插件 A 不需要知道插件 B 的具体类名,只需知道“我要找一个能支付的插件”。 缺点:实现稍复杂,需要宿主维护加载顺序。

方案三:通过Loader作为中介(最推荐)

利用Loader作为桥梁,通过Binder接口实现插件间通信。

这个方案的核心思想是:让Loader扮演"路由器"或"服务注册中心"的角色,插件B将自己的API注册到Loader,插件A通过Loader查询并获取插件B的API,然后直接调用。

核心原理图解

┌─────────────────────────────────────────────────────────────┐
│                        宿主进程                              │
│                                                              │
│  ┌──────────────┐          ┌──────────────┐                │
│  │   插件A       │          │   插件B       │                │
│  │              │          │              │                │
│  │ 需要调用B的方法│◄────────►│ 提供API给A   │                │
│  └──────┬───────┘          └──────┬───────┘                │
│         │                          │                         │
│         │ 3. 获取B的API            │ 1. 注册自己的API         │
│         ▼                          ▼                         │
│  ┌───────────────────────────────────────────────────┐     │
│  │                    Loader                          │     │
│  │  ┌─────────────────────────────────────────────┐   │     │
│  │  │      插件API注册中心 (Map<String, IBinder>)  │   │     │
│  │  │  - plugin_b -> Binder(插件B的API)            │   │     │
│  │  └─────────────────────────────────────────────┘   │     │
│  └───────────────────────────────────────────────────┘     │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Binder通信机制 (跨ClassLoader调用)                   │   │
│  │  插件A → Loader → 插件B 的调用链路                     │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

为什么能跨插件调用?

关键因素有两个:

  1. Binder的跨ClassLoader能力
    • Binder是Android的跨进程通信机制,但它同样可以在同一个进程内跨ClassLoader通信
    • Binder对象在传输过程中不依赖于ClassLoader,它是二进制安全的
    • 当插件A收到Binder对象时,可以通过Stub.asInterface()将其转换为接口,这个转换过程不关心目标实现在哪个ClassLoader中
  2. Loader作为统一命名空间
    • Loader运行在独立的ClassLoader中,但它可以同时访问所有插件的类
    • Loader维护的Map<String, IBinder>相当于一个跨插件的服务目录
    • 任何插件只要知道目标插件的"服务名"(partKey),就能通过Loader找到对应的Binder
具体代码:插件和插件通信(双11插件 → 购物车插件)

核心原理:Loader作为服务注册中心

Loader运行在独立ClassLoader中,可同时访问所有插件。通过维护Map<String, IBinder>服务注册表,实现插件间解耦通信。

为什么能跨插件调用?

  • Binder的跨ClassLoader能力:Binder是二进制安全的,不依赖ClassLoader,可在同进程内跨ClassLoader传输
  • Loader的统一命名空间:Loader维护全局服务目录,任何插件通过partKey即可找到对应服务

核心代码

5.6.3.1 公共模块定义接口 (shadow-common)
// ICartService.java - 购物车插件对外提供的服务
public interface ICartService extends IInterface {
    void addToCart(String productId, int count);
    void removeFromCart(String productId);
    List<CartItem> getCartItems();
    
    abstract class Stub extends Binder implements ICartService {
        public static ICartService asInterface(IBinder obj) {
            if (obj == null) return null;
            IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            return (iin instanceof ICartService) ? (ICartService) iin : new Proxy(obj);
        }
        
        private static class Proxy implements ICartService {
            private IBinder mRemote;
            Proxy(IBinder remote) { mRemote = remote; }
            
            @Override
            public void addToCart(String productId, int count) throws RemoteException {
                Parcel data = Parcel.obtain();
                data.writeInterfaceToken(DESCRIPTOR);
                data.writeString(productId);
                data.writeInt(count);
                mRemote.transact(TRANSACTION_addToCart, data, null, 0);
                data.recycle();
            }
            // ... 其他方法类似
        }
    }
}
5.6.3.2 购物车插件注册服务
// 购物车插件:CartPluginLoader.java
public class CartPluginLoader extends SamplePluginLoader {
    
    private ICartService mCartService;
    
    @Override
    public void loadPlugin(String partKey, Bundle extras) {
        super.loadPlugin(partKey, extras);
        
        // 创建服务实现
        mCartService = new CartServiceImpl();
        
        // 注册到Loader的服务中心
        registerPluginService("cart_plugin", mCartService.asBinder());
    }
    
    // 服务实现
    private class CartServiceImpl extends ICartService.Stub {
        @Override
        public void addToCart(String productId, int count) {
            CartDataManager.getInstance().add(productId, count);
        }
        
        @Override
        public List<CartItem> getCartItems() {
            return CartDataManager.getInstance().getItems();
        }
    }
}
5.6.3.3 双11插件调用购物车服务
// 双11插件:Double11PluginLoader.java
public class Double11PluginLoader extends SamplePluginLoader {
    
    private ICartService mCartService;
    
    // 获取购物车服务(在需要时调用)
    private void bindCartService() {
        if (mCartService != null) return;
        
        // 从Loader获取购物车插件的Binder
        IBinder binder = getPluginService("cart_plugin");
        if (binder != null) {
            mCartService = ICartService.Stub.asInterface(binder);
        }
    }
    
    // 双11秒杀添加商品
    public void onSeckillSuccess(String productId) {
        bindCartService();
        
        if (mCartService != null) {
            try {
                // 直接调用购物车插件的方法
                mCartService.addToCart(productId, 1);
                
                // 可选:发送通知刷新UI
                notifyCartUpdated();
                
            } catch (RemoteException e) {
                // 降级处理
                handleAddToCartFailed(productId);
            }
        }
    }
}
5.6.3.4 宿主中的Loader增强(支持服务注册)
// 宿主工程:EnhancedDynamicPluginManager.java
public class EnhancedDynamicPluginManager extends DynamicPluginManager {
    
    // 服务注册表
    private Map<String, IBinder> mServiceRegistry = new ConcurrentHashMap<>();
    
    // 插件注册服务
    public void registerPluginService(String serviceName, IBinder service) {
        mServiceRegistry.put(serviceName, service);
    }
    
    // 获取插件服务
    public IBinder getPluginService(String serviceName) {
        return mServiceRegistry.get(serviceName);
    }
    
    // 插件卸载时清理服务
    public void unregisterPluginService(String serviceName) {
        mServiceRegistry.remove(serviceName);
    }
}

流程图解

┌─────────────────────────────────────────────────────────────────┐
│                        宿主进程                                  │
│                                                                  │
│  ┌──────────────────────┐        ┌──────────────────────┐       │
│  │   双11插件            │        │   购物车插件          │       │
│  │                      │        │                      │       │
│  │  onSeckillSuccess()  │        │  CartServiceImpl     │       │
│  │         │            │        │      │               │       │
│  │         ▼            │        │      ▼               │       │
│  │  bindCartService()   │        │  registerService()  │       │
│  │         │            │        │      │               │       │
│  └─────────┼────────────┘        └──────┼───────────────┘       │
│            │                              │                      │
│            │ 2. getPluginService()        │ 1. registerService()│
│            ▼                              ▼                      │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    Loader (服务注册中心)                   │   │
│  │  ┌────────────────────────────────────────────────────┐ │   │
│  │  │  Map<String, IBinder> serviceRegistry              │ │   │
│  │  │  "cart_plugin" -> Binder(购物车插件API)            │ │   │
│  │  └────────────────────────────────────────────────────┘ │   │
│  └──────────────────────────────────────────────────────────┘   │
│            │                              │                      │
│            └──────────┬───────────────────┘                      │
│                       ▼                                          │
│            ┌──────────────────────┐                             │
│            │  Binder跨ClassLoader  │                             │
│            │  直接调用方法          │                             │
│            └──────────────────────┘                             │
└─────────────────────────────────────────────────────────────────┘

直接依赖方案(同UUID分组)

如果两个插件在同一UUID分组(共用Loader),可以更简单:

// 公共模块定义接口
public interface ICartProvider {
    void addToCart(String productId, int count);
}

// 购物车插件实现
public class CartPluginImpl implements ICartProvider {
    @Override
    public void addToCart(String productId, int count) {
        // 实现逻辑
    }
}

// 双11插件通过ClassLoader直接获取
public class Double11Plugin {
    private ICartProvider getCartProvider() {
        try {
            // 同一个ClassLoader下可以直接加载类
            Class<?> clazz = Class.forName("com.cart.plugin.CartProviderImpl");
            return (ICartProvider) clazz.newInstance();
        } catch (Exception e) {
            return null;
        }
    }
}

通信总结:

  • 插件获取宿主信息:在加载插件时,通过Bundle传递一个Binder(即宿主服务)给插件。
  • 宿主获取插件信息:通过 Manager 获取插件的Binder(即插件服务),然后调用。
  • 核心原理:利用Loader作为服务注册中心。插件B将自己的API(Binder)注册到Loader,插件A通过 Loader查询并获取插件B的Binder,然后直接调用其方法

5.7. Shadow如何更新:多插件管理、版本控制与热更新策略!宿主的版本和插件如果要更新,插件和插件之间依赖?(非常重要)

宿主与插件通信 Double11CommunicationBridge.java

宿主只负责加载 Manager 插件,这个后面是通过服务器加载,开始是本地加载

重点结论:
1).插件的加载和管理是通过manager

2).manager的加载和管理是通过自己检测自己的更新

插件动态更新策略,&& 插件动态化部署&& 多插件管理:同时加载多个插件的策略&& 插件生命周期管理

第一次版本,宿主A,包含2个插件,插件B 和插件C, 都是1.0版本
第一种场景: 过了7天,发现插件B有bug,热修复,插件B2.0版本
第二次发版:宿主A 3.0,插件B,C都有更新,同时新增插件D   都是3.0版本  

建立插件间的依赖关系图 管理插件与宿主的版本兼容关系

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  宿主A 1.0       │     │  插件热更新       │     │  宿主A 3.0       │
│  - 插件B 1.0     │────▶│  插件B 2.0       │────▶│  - 插件B 3.0     │
│  - 插件C 1.0     │     │  插件C 1.0       │     │  - 插件C 3.0     │
│                 │     │                 │     │  - 插件D 3.0     │
└─────────────────┘     └─────────────────┘     └─────────────────┘  

Double11PluginManager

5.7.1: 第一种场景:过了7天,发现插件B有bug,热修复,插件B是2.0版本

针对你提出的 Shadow 插件化框架中,宿主 A 包含插件 B 和 C,并需要动态热修复插件 B 的场景,结合 Shadow 的架构特性,提供一个详细的实现方案。

核心思路

Shadow 是一个全动态插件化框架,其核心设计是:宿主只负责加载 Manager 插件,Manager 负责下载、安装新插件,然后通过跨进程通信(IPC)拉起 Loader 和 Runtime 来运行具体的业务插件(如 B 和 C)。

要实现插件 B 的热修复(从 1.0 升级到 2.0),核心在于 Manager 模块需要具备版本检测和插件包替换的能力

第一阶段:初始版本(1.0)

此时的架构和职责如下:

  1. 宿主 A

    • 包含一个非常轻量的初始化代码,不包含任何业务逻辑。
    • 负责在启动时从 assets 目录或本地加载初始的 Manager 插件(管理插件)。
  2. 插件 Manager

    • 这是一个独立的插件,负责插件管理。
    • 它知道去哪里下载插件 B 和 C(比如 CDN 地址)。
    • 它负责将插件 B 和 C 的 1.0 版本 APK/ZIP 文件下载到本地,并存储到宿主应用的私有目录下(如 /data/data/包名/files/ShadowPluginManager/)。
  3. 插件 B 和 C

    • 标准的 Shadow 业务插件,打包成包含 config.json 的 ZIP 包。
    • ZIP 包内包含插件本身的 APK 以及必要的 config.json 描述文件(记录版本号、UUID、依赖等)。

初始加载流程
宿主 A -> 加载 Manager -> Manager 检查本地插件 -> 发现无版本或版本低 -> 下载插件 B 1.0 -> 安装(解压、odex、存入数据库) -> 启动插件 B。

第二阶段:热修复(插件 B 升级到 2.0)

假设 7 天后,你需要修复插件 B 的 Bug。由于 Manager 本身也是一个插件,你可以选择不更新宿主和 Manager,只更新插件 B。

5.7.1.1 插件打包与下发

你需要重新打包插件 B,生成 2.0 版本的 ZIP 包。在打包配置中(shadow 闭包),你需要修改 version 字段

shadow {
    packagePlugin {
        pluginTypes {
            release {
                pluginApks {
                    pluginB {
                        // ... 其他配置
                        version = 2  // 关键:版本号从1改为2
                        // 或者通过 version = 2 和 uuidNickName = "2.0.0" 组合控制
                    }
                }
            }
        }
        // 如果共用Loader/Runtime,UUID必须保持一致
        uuid = "your-unique-uuid-for-bc" 
    }
}

打包完成后,将这个新的 plugin_b_2.0.zip 上传到你的服务器(CDN),并更新下发策略。

5.7.1.2 Manager 的版本检测机制(关键)

这是实现热修复的核心逻辑。你需要在你自定义的 Manager 插件中实现以下逻辑:

  • 版本对比:Manager 不能只判断插件是否存在,必须判断版本号。通常通过解析本地数据库或 config.json 中的 version 字段与服务器下发的版本信息(如接口返回的 latestVersion)进行对比。
  • 按需下载:当用户打开插件 B 时,Manager 发起请求 https://your-server/api/plugin/B/latest,获取到最新版本号为 2,本地版本为 1,判定需要更新。
  • 覆盖安装:下载新的 plugin_b_2.0.zip
  • 数据库更新:在安装过程中,调用 InstalledDao#insert 或 update,将数据库中的插件 B 记录更新为新版本的信息(路径、版本号、hash 值等)。注意:这不会删除旧文件,但下次加载时会根据数据库的最新记录指向 2.0 版本的文件。
5.7.1.3 进程隔离与版本共存

Shadow 的一个强大特性是插件进程隔离。在版本切换时,需要注意处理进程中的残留实例:

  • 如果插件 B 正在运行

    • 简单方案:提示用户重启应用,或者自行杀死插件进程(调用 PPSController.exit()),这样下次进入时 Manager 会加载新的 2.0 版本。
    • 复杂方案(无感知) :用户关闭插件 B 界面后,后台立即清理旧进程,下次打开自动为新版本。
  • 如果插件 B 未运行:直接更新数据库中的记录,下次启动自然就是 2.0 版本。

方案总结与组件职责图

组件角色热修复中的职责关键操作
宿主 A不参与更新。提供运行环境,加载 Manager。无改动。仅负责启动 Manager。
Manager管家核心控制层。负责版本检测、下载、安装、数据库版本管理。1. 对比本地与远程版本(发现 2.0)。 2. 下载新包。 3. 解压并更新数据库(覆盖旧记录)。 4. 触发插件进程重启(可选)。
插件 B业务被更新的对象。1. 打包版本号必须递增(version=2)。 2. 上传至服务器。
插件 C业务保持不变。无改动。继续运行 1.0 版本。

总结:核心就是Manager,需要从服务器获取配置,动态下载插件,和插件管理的操作

5.7.2. 第二种场景:第二次发版:宿主A ,插件B,C都有更新,同时新增插件D ,如何管理插件和动态下方插件,方案是啥

核心挑战

挑战说明
多版本共存三个插件版本各不相同,Manager 需要能区分管理
依赖关系如果插件间有依赖(如 D 依赖 B 的某些能力),加载顺序需保证
增量下载避免用户下载完整包,尤其是只更新了小 Bug 的情况
Loader/Runtime 共用多个插件共用同一套 Loader 和 Runtime,避免重复下载和内存浪费

方案设计:基于 UUID 的分组管理与增量更新

5.7.2.1 插件分组设计(关键)

Shadow 的核心设计是 UUID 决定一组插件的 Loader 和 Runtime。在第二次发版时,你需要决定插件 B、C、D 是否共用同一套 Loader/Runtime。

┌─────────────────────────────────────────────────────────────┐
│                    UUID = "group-1"                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Loader 插件 (版本独立)                              │   │
│  │  Runtime 插件 (版本独立)                            │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  插件 B (partKey="pluginB", version=3.0)            │   │
│  │  插件 C (partKey="pluginC", version=2.0)            │   │
│  │  插件 D (partKey="pluginD", version=1.0)            │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

配置示例(插件 B 的 build.gradle):

shadow {
    packagePlugin {
        pluginTypes {
            release {
                // 关键:共用 Loader/Runtime,只配置业务插件
                // loaderApkConfig 和 runtimeApkConfig 留空
                pluginApks {
                    pluginB {
                        partKey = "pluginB"
                        businessName = "business_group"
                        version = 3  // 版本号递增
                        hostWhiteList = [...]  // 宿主白名单
                        dependsOn = []  // 如有依赖其他插件,填写 partKey
                    }
                }
            }
        }
        uuid = "group-1"  // 与插件 C、D 共用同一个 UUID
        version = 1  // 分组版本号(非插件版本)
    }
}
5.7.2.2 插件包结构

每个插件独立打包为 ZIP,包含:

  • pluginB.apk(业务代码)
  • config.json(元信息,含版本号)

config.json 示例

{
  "partKey": "pluginB",
  "version": 3,
  "uuid": "group-1",
  "dependencies": [],
  "minHostVersion": "1.0.0"
}
5.7.2.3 下发策略设计

在服务端维护插件版本配置表:

partKeylatestVersiondownloadUrlpatchUrldependsOnrequired
pluginB3/full/b3.zip/patch/b2-b3.hpz[]true
pluginC2/full/c2.zip/patch/c1-c2.hpz[]true
pluginD1/full/d1.zipnull[]false
5.7.2.4 Manager 升级检测逻辑

Manager 需要实现批量版本检测,而不是逐个检测:

┌─────────────────────────────────────────────────────────────────┐
│                    Manager 版本检测流程                         │
├─────────────────────────────────────────────────────────────────┤
│  1. 请求服务端接口 /api/plugins/latest                          │
│     传入当前已安装插件列表:[{partKey:"B",version:2},           ││                            {partKey:"C",version:1}]              │
│                                                                 │
│  2. 服务端返回需要更新的插件:                                   │
│     - pluginB: 23 (patch 可用)                                 │
│     - pluginC: 12 (full 下载)                                  │
│     - pluginD: 新增 (full 下载)                                 │
│                                                                 │
│  3. Manager 批量下载:优先下载 patch,并行下载 full             │
│                                                                 │
│  4. 按依赖顺序安装:如 D 依赖 B,则先安装 B 再安装 D            │
└─────────────────────────────────────────────────────────────────┘
5.7.2.5 数据库版本管理

Shadow 的插件安装信息通过 InstalledDao 持久化到数据库。每个插件按 partKey 唯一标识,版本信息独立存储。

字段说明
partKey插件唯一标识(如 pluginB)
version插件版本号
uuid所属分组
apkPath插件文件绝对路径
hash文件完整性校验
dependencies依赖的其他 partKey 列表
5.7.2.6 依赖加载顺序

Shadow 支持插件间的依赖关系配置,通过 dependsOn 指定:

// 插件 D 依赖插件 B
pluginD {
    partKey = "pluginD"
    dependsOn = ["pluginB"]  // 确保加载插件 D 前先加载插件 B
}

加载时处理:Shadow 会将依赖插件的 ClassLoader 设置为当前插件的 parent,实现类共享。

完整时序图

用户启动 App
    │
    ▼
宿主 A ──────────────────────────────────────────┐
    │ 加载 Manager (本地已有)                      │
    ▼                                             │
Manager ─────────────────────────────────────────┐│
    │ 1. 请求服务端 /api/plugins/latest          ││
    │    (携带本地插件列表: B2, C1)               ││
    │                                             ││
    │ 2. 服务端返回:                              ││
    │    - B: 23 (patch可用)                    ││
    │    - C: 12 (full)                         ││
    │    - D: 新增 (full)                        ││
    │                                             ││
    │ 3. 并行下载                                 ││
    │    ├── 下载 Bpatch.hpz                 ││
    │    ├── 下载 Cfull zip                  ││
    │    └── 下载 Dfull zip                  ││
    │                                             ││
    │ 4. 本地合并 (B)                            ││
    │    old.apk + patch.hpznew.apk           ││
    │    MD5 校验                                 ││
    │                                             ││
    │ 5. 按依赖顺序安装                           ││
    │    ├── 安装插件 B (version 3)              ││
    │    ├── 安装插件 C (version 2)              ││
    │    └── 安装插件 D (version 1)              ││
    │                                             ││
    │ 6. 更新数据库                               ││
    │    InstalledDao.update/insert              ││
    └────────────────────────────────────────────┘│
    │                                              │
    ▼                                              │
启动插件 B/C/D(按需)◄─────────────────────────────┘

实施清单

步骤负责模块具体内容
1构建脚本统一三个插件的 UUID,分别设置 version
2服务端新增批量版本检测接口,返回增量/全量下载地址
3Manager实现批量检测、并行下载、依赖排序安装
4Manager集成 ApkDiffPatch 实现增量合并
5Manager数据库支持多版本共存,按 partKey 区分

版本演进时间线

时间线
│
├── 第一次发版
│   ├── 宿主 A 1.0
│   ├── 插件 B 1.0
│   └── 插件 C 1.0
│
├── 热修复阶段(第一次发版后第7天)
│   └── 插件 B 2.0(热修复 Bug)
│
└── 第二次发版(当前)
    ├── 宿主 A 2.0(更新)
    ├── 插件 B 3.0(更新)
    ├── 插件 C 2.0(更新)
    └── 插件 D 1.0(新增)

组件关系图

deepseek_mermaid_20260325_577ce4.png

deepseek_mermaid_20260325_fe1118.png

核心设计原则

原则实现方式
全动态化宿主只负责加载 Manager,Manager 负责所有插件管理
版本隔离每个插件独立版本,通过 partKey 唯一标识
按需下载Manager 根据本地版本与服务端对比,只下载需要更新的插件
依赖感知插件支持 dependsOn 配置,Manager 按依赖顺序安装加载

服务端插件版本表

partKeyversiondownloadUrlpatchUrlsizemd5dependsOnminHostVersionstatus
plugin_b3/full/b3.zip/patch/b2-b3.hpz5MBabc123[]1.0active
plugin_b2/full/b2.zip-5MBdef456[]1.0archived
plugin_c2/full/c2.zip/patch/c1-c2.hpz3MBghi789[]1.0active
plugin_c1/full/c1.zip-3MBjkl012[]1.0archived
plugin_d1/full/d1.zip-2MBmno345["plugin_b"]1.0active

用户状态矩阵

用户类型宿主版本Manager 版本初始插件操作最终状态
活跃老用户1.02.0 (已更新)B 2.0, C 1.0检测更新 → 下载 B 3.0, C 2.0, D 1.0B 3.0, C 2.0, D 1.0
不活跃老用户1.01.0 (未更新)更新 Manager → 下载所有插件B 3.0, C 2.0, D 1.0
新用户2.02.0 (内置)下载所有插件B 3.0, C 2.0, D 1.0

实施步骤清单

阶段任务负责人
准备阶段1. 统一三个插件的 UUID,配置各自的 version 2. 配置插件依赖关系(如 D 依赖 B) 3. 生成增量补丁包(可选)开发
服务端1. 搭建版本管理 API 2. 上传插件包到 CDN 3. 配置版本数据库后端
Manager1. 实现批量版本检测 2. 实现增量更新合并 3. 实现依赖排序安装 4. 实现数据库版本管理客户端

整体的流程图.png

总结: 核心方案全动态化。宿主只负责加载 ManagerManager 负责所有插件(B、C、D)的版本检测、下载、安装和依赖管理。

关键策略

  1. 分组管理:通过 UUID 将一组业务插件(B、C、D)绑定在一起,共用一套Loader和Runtime。
  2. 版本控制:服务端维护插件版本表,Manager批量检测。
  3. 依赖排序:插件通过 dependsOn 声明依赖,Manager按顺序安装(如D依赖B,则先安装B)

5.8. 热修复一个类:优惠券叠加计算逻辑紧急修复,同时有一个奔溃问问题

业务场景:双11期间发现优惠券叠加计算Bug:满300减50的店铺券与满200减20的平台券同时使用时,计算金额错误,导致用户多付或少付款。需要:

  • 快速定位问题所在类 CouponCalculator.java
  • 开发修复版本,生成修复Dex文件
  • 通过后台配置系统,向所有用户推送热修复包
  • 用户下次打开购物车时自动应用修复,无需重启App

热修复是全量更新,还是增量更新? 结论:Shadow 的“热修复”本质上是「插件全量更新」,不是增量热修复(如修复单个方法)

基于Shadow的类级别热修复

优惠券计算器修复 CouponHotFixManager.java

热修复配置文件

// hotfix_config.json
{
  "hotfixes": [
    {
      "id": "coupon_calc_fix_20231101",
      "className": "com.taobao.app.coupon.CouponCalculator",
      "version": "1.2.0",
      "description": "修复双11优惠券叠加计算逻辑错误",
      "downloadUrl": "https://cdn.taobao.com/hotfix/double11/coupon_fix_v1.2.dex",
      "md5": "a1b2c3d4e5f678901234567890123456",
      "minAppVersion": "9.5.0",
      "maxAppVersion": "9.8.0",
      "applyStrategy": {
        "type": "immediate",
        "conditions": ["wifi_only", "battery_high"],
        "rollbackOnFailure": true
      },
      "affectedUsers": "all",
      "releaseTime": "2023-11-01T10:00:00Z",
      "expireTime": "2023-11-15T23:59:59Z"
    }
  ]
}

核心原理

Shadow的热修复本质是插件全量更新,通过Manager下载新版本插件包替换旧版本。

用户启动App → Manager检测版本 → 发现新版本 → 下载新插件包 → 更新数据库 → 下次加载生效

核心代码

5.8.1 优惠券计算器(待修复)
// 插件工程:CouponCalculator.java(Bug版本)
public class CouponCalculator {
    
    // Bug: 店铺券和平台券叠加计算错误
    public long calculateTotalDiscount(List<Coupon> coupons, long totalPrice) {
        long discount = 0;
        for (Coupon coupon : coupons) {
            if (coupon.getType() == CouponType.SHOP) {
                discount += calculateShopDiscount(coupon, totalPrice);
            } else if (coupon.getType() == CouponType.PLATFORM) {
                // Bug: 应该用优惠后金额,但用了原始金额
                discount += calculatePlatformDiscount(coupon, totalPrice); 
            }
        }
        return discount;
    }
}
5.8.2 修复后的版本
// 插件工程:CouponCalculator.java(修复版本 version=2.0)
public class CouponCalculator {
    
    // 修复: 店铺券和平台券正确叠加
    public long calculateTotalDiscount(List<Coupon> coupons, long totalPrice) {
        long currentPrice = totalPrice;
        long totalDiscount = 0;
        
        // 先计算店铺券(通常门槛低)
        for (Coupon coupon : coupons) {
            if (coupon.getType() == CouponType.SHOP) {
                long discount = calculateShopDiscount(coupon, currentPrice);
                totalDiscount += discount;
                currentPrice -= discount; // 关键修复: 使用优惠后金额
            }
        }
        
        // 再计算平台券(基于优惠后金额)
        for (Coupon coupon : coupons) {
            if (coupon.getType() == CouponType.PLATFORM) {
                totalDiscount += calculatePlatformDiscount(coupon, currentPrice);
            }
        }
        return totalDiscount;
    }
}
5.8.3 Manager热修复管理器
// Manager工程:CouponHotFixManager.java
public class CouponHotFixManager {
    
    private static final String HOTFIX_CONFIG_URL = "https://api.taobao.com/hotfix/config";
    
    /**
     * 检查热修复更新
     */
    public void checkHotFix() {
        // 1. 拉取配置
        fetchHotFixConfig(new ConfigCallback() {
            @Override
            public void onSuccess(HotFixConfig config) {
                for (HotFixItem item : config.hotfixes) {
                    // 2. 版本对比
                    if (needUpdate(item)) {
                        // 3. 下载修复包
                        downloadAndApply(item);
                    }
                }
            }
        });
    }
    
    /**
     * 判断是否需要更新
     */
    private boolean needUpdate(HotFixItem item) {
        // 获取当前插件版本
        int currentVersion = getPluginVersion(item.partKey);
        
        // 版本比较
        if (item.version <= currentVersion) return false;
        
        // 用户范围判断
        if (!isInUserScope(item.affectedUsers)) return false;
        
        // App版本范围判断
        String appVersion = getAppVersion();
        if (appVersion.compareTo(item.minAppVersion) < 0) return false;
        if (appVersion.compareTo(item.maxAppVersion) > 0) return false;
        
        return true;
    }
    
    /**
     * 下载并应用热修复
     */
    private void downloadAndApply(HotFixItem item) {
        // 检查网络条件
        if (!checkNetworkCondition(item.applyStrategy.conditions)) {
            // 延迟下载
            scheduleDownload(item);
            return;
        }
        
        // 下载新插件包
        DownloadManager.download(item.downloadUrl, new DownloadCallback() {
            @Override
            public void onSuccess(File pluginFile) {
                // MD5校验
                if (!verifyMd5(pluginFile, item.md5)) {
                    onError("MD5校验失败");
                    return;
                }
                
                // 安装新版本(更新数据库记录)
                installNewVersion(item.partKey, item.version, pluginFile);
                
                // 通知用户
                notifyUser("优惠券计算已优化,重启后生效");
            }
            
            @Override
            public void onError(String error) {
                // 降级处理
                if (item.applyStrategy.rollbackOnFailure) {
                    rollbackToOldVersion(item.partKey);
                }
            }
        });
    }
    
    /**
     * 安装新版本插件
     */
    private void installNewVersion(String partKey, int version, File pluginFile) {
        // 1. 复制到插件目录
        File destFile = new File(getPluginDir(), partKey + "_v" + version + ".apk");
        FileUtils.copy(pluginFile, destFile);
        
        // 2. 更新数据库
        InstalledDao dao = Shadow.getInstalledDao();
        InstalledPlugin plugin = dao.getByPartKey(partKey);
        if (plugin == null) {
            plugin = new InstalledPlugin();
            plugin.partKey = partKey;
        }
        plugin.version = version;
        plugin.apkPath = destFile.getAbsolutePath();
        plugin.updateTime = System.currentTimeMillis();
        dao.insertOrUpdate(plugin);
        
        // 3. 清理旧版本(可选,延迟清理)
        cleanOldVersion(partKey, version);
    }
}
5.8.4 热修复配置文件解析
// HotFixConfig.java
public class HotFixConfig {
    public List<HotFixItem> hotfixes;
    
    public static HotFixConfig parse(String json) {
        Gson gson = new Gson();
        return gson.fromJson(json, HotFixConfig.class);
    }
}

// HotFixItem.java
public class HotFixItem {
    public String id;
    public String partKey;           // 插件标识
    public String className;          // 修复的类名(仅记录)
    public int version;               // 新版本号
    public String downloadUrl;        // 下载地址
    public String md5;                // 文件校验
    public String minAppVersion;      // 最小宿主版本
    public String maxAppVersion;      // 最大宿主版本
    public ApplyStrategy applyStrategy;
    public String affectedUsers;      // 影响用户范围
    public long releaseTime;          // 发布时间
    public long expireTime;           // 过期时间
}

// ApplyStrategy.java
public class ApplyStrategy {
    public String type;               // immediate / delay
    public List<String> conditions;   // wifi_only, battery_high
    public boolean rollbackOnFailure;
}
5.8.5 在宿主中触发检查
// 宿主工程:MainActivity.java
public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // App启动时检查热修复
        CouponHotFixManager hotFixManager = new CouponHotFixManager();
        hotFixManager.checkHotFix();
    }
}

// 或在购物车页面打开时检查
public class CartActivity extends AppCompatActivity {
    
    @Override
    protected void onResume() {
        super.onResume();
        
        // 每次打开购物车检查优惠券逻辑是否有更新
        CouponHotFixManager hotFixManager = new CouponHotFixManager();
        hotFixManager.checkHotFix();
    }
}

热修复流程时序图

┌──────┐     ┌─────────┐     ┌─────────┐     ┌──────────┐
│ 用户  │     │ 宿主App │     │ Manager │     │  服务器   │
└──┬───┘     └────┬────┘     └────┬────┘     └────┬─────┘
   │              │               │               │
   │  打开购物车   │               │               │
   │─────────────>│               │               │
   │              │  检查热修复    │               │
   │              │──────────────>│               │
   │              │               │  请求配置      │
   │              │               │──────────────>│
   │              │               │               │
   │              │               │  返回配置      │
   │              │               │<──────────────│
   │              │               │               │
   │              │               │  版本对比      │
   │              │               │  (发现新版本)  │
   │              │               │               │
   │              │               │  下载新插件包   │
   │              │               │──────────────>│
   │              │               │               │
   │              │               │  返回插件包    │
   │              │               │<──────────────│
   │              │               │               │
   │              │               │  校验MD5      │
   │              │               │  更新数据库    │
   │              │               │               │
   │  下次启动生效 │               │               │
   │<─────────────│               │               │
   │              │               │               │

5.9. 热修复替换一个资源文件: 发现活动主图侵犯版权,需立即更换

业务场景:发现活动主图侵犯版权,需立即更换, 活动文案涉及敏感词,需立即修改 ResourceHotFixManager.java

核心原理

资源热修复同样是插件全量更新,通过Manager下载包含新资源的新版本插件包。

核心代码

5.9.1 资源热修复管理器
// Manager工程:ResourceHotFixManager.java
public class ResourceHotFixManager {
    
    private static final String RESOURCE_CONFIG_URL = "https://api.taobao.com/resource/hotfix";
    
    /**
     * 检查资源热修复
     */
    public void checkResourceHotFix() {
        fetchResourceConfig(new ConfigCallback() {
            @Override
            public void onSuccess(ResourceConfig config) {
                for (ResourceItem item : config.resources) {
                    if (needUpdate(item)) {
                        downloadAndApply(item);
                    }
                }
            }
        });
    }
    
    /**
     * 判断是否需要更新
     */
    private boolean needUpdate(ResourceItem item) {
        // 获取当前插件版本
        int currentVersion = getPluginVersion(item.partKey);
        
        // 版本比较
        if (item.version <= currentVersion) return false;
        
        // 检查是否在生效时间范围内
        long now = System.currentTimeMillis();
        if (now < item.startTime || now > item.endTime) return false;
        
        return true;
    }
    
    /**
     * 下载并应用资源修复包
     */
    private void downloadAndApply(ResourceItem item) {
        DownloadManager.download(item.downloadUrl, new DownloadCallback() {
            @Override
            public void onSuccess(File pluginFile) {
                // MD5校验
                if (!verifyMd5(pluginFile, item.md5)) {
                    onError("MD5校验失败");
                    return;
                }
                
                // 安装新版本插件
                installNewVersion(item.partKey, item.version, pluginFile);
                
                // 记录修复日志
                logHotFix(item);
            }
            
            @Override
            public void onError(String error) {
                // 降级:继续使用旧版本
                Log.e("ResourceHotFix", "下载失败: " + error);
            }
        });
    }
}
5.9.2 资源修复配置
// ResourceConfig.java
public class ResourceConfig {
    public List<ResourceItem> resources;
}

// ResourceItem.java
public class ResourceItem {
    public String id;                    // 修复ID
    public String partKey;               // 插件标识
    public int version;                  // 新版本号
    public String description;           // 修复描述
    public String downloadUrl;           // 下载地址
    public String md5;                   // 文件校验
    public long startTime;               // 生效开始时间
    public long endTime;                 // 生效结束时间
    public List<ResourceChange> changes; // 变更的资源列表
}

// ResourceChange.java
public class ResourceChange {
    public String type;                  // image / string / layout
    public String name;                  // 资源名称
    public String oldValue;              // 旧值(仅记录)
    public String newValue;              // 新值(图片为URL)
}
5.9.3 热修复配置文件示例
{
  "resources": [
    {
      "id": "double11_banner_fix_20231101",
      "partKey": "double11_plugin",
      "version": 2,
      "description": "替换侵权主图",
      "downloadUrl": "https://cdn.taobao.com/plugin/double11_plugin_v2.zip",
      "md5": "a1b2c3d4e5f678901234567890123456",
      "startTime": 1698768000000,
      "endTime": 1701446400000,
      "changes": [
        {
          "type": "image",
          "name": "banner_main",
          "oldValue": "侵权图片.png",
          "newValue": "合法图片.png"
        },
        {
          "type": "string",
          "name": "activity_title",
          "oldValue": "限时秒杀",
          "newValue": "狂欢秒杀"
        },
        {
          "type": "string",
          "name": "activity_slogan",
          "oldValue": "全年最低价",
          "newValue": "超值优惠"
        }
      ]
    }
  ]
}
5.9.4 插件内资源使用(需兼容新旧版本)
// 插件工程:Double11Activity.java
public class Double11Activity extends PluginContainerActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_double11);
        
        // 加载主图(自动使用新版本资源)
        ImageView banner = findViewById(R.id.iv_banner);
        int bannerResId = getResources().getIdentifier(
            "banner_main", "drawable", getPackageName()
        );
        banner.setImageResource(bannerResId);
        
        // 加载文案(自动使用新版本字符串)
        TextView title = findViewById(R.id.tv_title);
        title.setText(R.string.activity_title);
        
        TextView slogan = findViewById(R.id.tv_slogan);
        slogan.setText(R.string.activity_slogan);
    }
}

5.10. 热修复一个so文件:图片加载so库,会有奔溃

业务场景:双11期间图片加载量剧增,发现:需要紧急替换为优化后的Native库 解决方案:在不发版的情况下,动态替换 libimage_decoder.so 库文件

SoHotFixManager.java

核心原理

SO库热修复同样基于插件全量更新,通过Manager下载包含新SO库的新版本插件包,替换旧版本。

核心代码

5.10.1 SO热修复管理器
// Manager工程:SoHotFixManager.java
public class SoHotFixManager {
    
    private static final String SO_CONFIG_URL = "https://api.taobao.com/so/hotfix";
    
    /**
     * 检查SO热修复
     */
    public void checkSoHotFix() {
        fetchSoConfig(new ConfigCallback() {
            @Override
            public void onSuccess(SoConfig config) {
                for (SoItem item : config.soFiles) {
                    if (needUpdate(item)) {
                        downloadAndApply(item);
                    }
                }
            }
        });
    }
    
    /**
     * 判断是否需要更新
     */
    private boolean needUpdate(SoItem item) {
        // 获取当前插件版本
        int currentVersion = getPluginVersion(item.partKey);
        
        // 版本比较
        if (item.version <= currentVersion) return false;
        
        // 检查SO是否在运行中
        if (isSoLoaded(item.soName)) {
            // 需要重启插件才能生效
            markNeedRestart(item.partKey);
        }
        
        return true;
    }
    
    /**
     * 下载并应用SO修复包
     */
    private void downloadAndApply(SoItem item) {
        DownloadManager.download(item.downloadUrl, new DownloadCallback() {
            @Override
            public void onSuccess(File pluginFile) {
                // MD5校验
                if (!verifyMd5(pluginFile, item.md5)) {
                    onError("MD5校验失败");
                    return;
                }
                
                // 解压并替换SO库
                installNewVersion(item.partKey, item.version, pluginFile);
                
                // 提示用户重启插件
                if (needRestart(item.partKey)) {
                    showRestartDialog(item.partKey);
                }
            }
            
            @Override
            public void onError(String error) {
                // 降级:继续使用旧版本
                Log.e("SoHotFix", "下载失败: " + error);
            }
        });
    }
    
    /**
     * 安装新版本插件
     */
    private void installNewVersion(String partKey, int version, File pluginFile) {
        // 1. 解压插件包
        File pluginDir = new File(getPluginDir(), partKey + "_v" + version);
        unzip(pluginFile, pluginDir);
        
        // 2. 更新数据库
        InstalledDao dao = Shadow.getInstalledDao();
        InstalledPlugin plugin = dao.getByPartKey(partKey);
        if (plugin == null) {
            plugin = new InstalledPlugin();
            plugin.partKey = partKey;
        }
        plugin.version = version;
        plugin.apkPath = pluginDir.getAbsolutePath();
        plugin.updateTime = System.currentTimeMillis();
        dao.insertOrUpdate(plugin);
        
        // 3. 预加载SO到缓存(可选)
        preloadSo(pluginDir, "libimage_decoder.so");
    }
    
    /**
     * 预加载SO库
     */
    private void preloadSo(File pluginDir, String soName) {
        try {
            File soFile = new File(pluginDir, "lib/armeabi-v7a/" + soName);
            if (soFile.exists()) {
                System.load(soFile.getAbsolutePath());
                Log.d("SoHotFix", "SO预加载成功: " + soName);
            }
        } catch (Throwable e) {
            Log.e("SoHotFix", "SO预加载失败: " + e.getMessage());
        }
    }
    
    /**
     * 检查SO是否已加载
     */
    private boolean isSoLoaded(String soName) {
        // 通过反射检查已加载的SO列表
        try {
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            Field field = loader.getClass().getDeclaredField("nativeLibraries");
            field.setAccessible(true);
            Vector<?> libraries = (Vector<?>) field.get(loader);
            for (Object lib : libraries) {
                if (lib.toString().contains(soName)) {
                    return true;
                }
            }
        } catch (Exception e) {
            // 忽略反射异常
        }
        return false;
    }
}
5.10.2 SO热修复配置
// SoConfig.java
public class SoConfig {
    public List<SoItem> soFiles;
}

// SoItem.java
public class SoItem {
    public String id;                    // 修复ID
    public String partKey;               // 插件标识
    public String soName;                // SO文件名
    public int version;                  // 新版本号
    public String description;           // 修复描述
    public String downloadUrl;           // 下载地址
    public String md5;                   // 文件校验
    public String abi;                   // armeabi-v7a / arm64-v8a
    public boolean needRestart;          // 是否需要重启
    public long releaseTime;             // 发布时间
}

// 配置文件示例
{
  "soFiles": [
    {
      "id": "image_decoder_fix_20231101",
      "partKey": "double11_plugin",
      "soName": "libimage_decoder.so",
      "version": 2,
      "description": "修复图片解码崩溃问题",
      "downloadUrl": "https://cdn.taobao.com/plugin/double11_plugin_v2.zip",
      "md5": "a1b2c3d4e5f678901234567890123456",
      "abi": "arm64-v8a",
      "needRestart": true,
      "releaseTime": 1698768000000
    }
  ]
}
5.10.3 插件内加载SO库(支持动态替换)
// 插件工程:ImageLoader.java
public class ImageLoader {
    
    private static boolean sSoLoaded = false;
    
    /**
     * 加载SO库(优先使用插件内的SO)
     */
    public static void loadSoLibrary(String soName) {
        if (sSoLoaded) return;
        
        try {
            // 获取插件自身的ClassLoader
            ClassLoader loader = ImageLoader.class.getClassLoader();
            
            // 尝试从插件路径加载SO
            String pluginSoPath = getPluginSoPath(soName);
            if (pluginSoPath != null) {
                System.load(pluginSoPath);
                Log.d("ImageLoader", "从插件路径加载SO成功: " + pluginSoPath);
            } else {
                // 降级:从系统路径加载
                System.loadLibrary(soName);
                Log.d("ImageLoader", "从系统路径加载SO成功: " + soName);
            }
            
            sSoLoaded = true;
        } catch (Throwable e) {
            Log.e("ImageLoader", "SO加载失败: " + e.getMessage());
            // 降级:使用原生解码
            useFallbackDecoder();
        }
    }
    
    /**
     * 获取插件内的SO路径
     */
    private static String getPluginSoPath(String soName) {
        try {
            // 获取插件安装目录
            Context context = MyApplication.getContext();
            File pluginDir = new File(context.getFilesDir(), "ShadowPlugin/double11_plugin");
            
            // 遍历查找SO文件
            String[] abis = {"arm64-v8a", "armeabi-v7a"};
            for (String abi : abis) {
                File soFile = new File(pluginDir, "lib/" + abi + "/" + soName);
                if (soFile.exists()) {
                    return soFile.getAbsolutePath();
                }
            }
        } catch (Exception e) {
            Log.e("ImageLoader", "获取SO路径失败: " + e.getMessage());
        }
        return null;
    }
    
    /**
     * 降级方案
     */
    private static void useFallbackDecoder() {
        // 使用Java层解码库
        Log.w("ImageLoader", "使用降级解码器");
    }
}
5.10.4 在插件中使用
// 插件工程:Double11Activity.java
public class Double11Activity extends PluginContainerActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 加载修复后的SO库
        ImageLoader.loadSoLibrary("libimage_decoder.so");
        
        // 使用图片加载功能
        loadImages();
    }
    
    private void loadImages() {
        ImageView imageView = findViewById(R.id.iv_product);
        
        // 使用SO库解码图片
        Bitmap bitmap = NativeImageDecoder.decode(imagePath);
        imageView.setImageBitmap(bitmap);
    }
}

SO热修复流程时序图

┌──────┐     ┌─────────┐     ┌─────────┐     ┌──────────┐
│ 用户  │     │ 宿主App │     │ Manager │     │  服务器   │
└──┬───┘     └────┬────┘     └────┬────┘     └────┬─────┘
   │              │               │               │
   │  打开双11页面 │               │               │
   │─────────────>│               │               │
   │              │               │               │
   │              │  检查SO修复    │               │
   │              │──────────────>│               │
   │              │               │  请求配置      │
   │              │               │──────────────>│
   │              │               │               │
   │              │               │  返回新版本    │
   │              │               │<──────────────│
   │              │               │               │
   │              │               │  下载新插件包   │
   │              │               │──────────────>│
   │              │               │               │
   │              │               │  解压SO库      │
   │              │               │  更新数据库    │
   │              │               │               │
   │              │               │  提示重启      │
   │              │<──────────────│               │
   │              │               │               │
   │  重启插件页面 │               │               │
   │─────────────>│               │               │
   │              │               │               │
   │              │  加载新SO库    │               │
   │              │──────────────>│               │
   │              │               │               │
   │  崩溃问题修复 │               │               │
   │<─────────────│               │               │
   │              │               │               │

热修复的总结:

核心结论:Shadow的“热修复”本质上是插件全量更新。Manager通过版本对比,下载包含修复内容的新版本插件包,替换旧版本,下次加载时生效。


6. 业务场景总结表

需求业务场景触发时机技术实现
1加载双11插件点击Tab、App启动Shadow动态加载
2插件崩溃处理运行中崩溃异常捕获+自动恢复
3卸载插件活动结束、存储不足数据迁移+文件删除
4加载Activity进入活动页面Shadow代理Activity
5加载Fragment首页嵌入活动类加载+注入
6宿主插件通信数据同步、用户操作接口通信+事件总线
7热更新策略版本更新、bug修复灰度发布+版本管理
8类热修复逻辑错误、崩溃Dex替换+类加载
9资源热修复图片侵权、文案错误资源包替换
10SO库热修复Native崩溃、性能问题SO文件替换

7. 上线后实际效果对比

7.1. 崩溃率统计

数据对比(双11活动期间)

指标使用插件化前使用插件化后改善率
整体应用崩溃率0.32%0.28%↓ 12.5%
双11活动模块崩溃率N/A (发版固定)0.15%-
插件崩溃影响宿主率N/A0%完全隔离
崩溃自动恢复成功率N/A78.3%-

关键发现

  • 插件化并未显著增加整体崩溃率,反而通过进程隔离机制,有效防止了单个模块崩溃影响整个应用。
  • 通过内置的崩溃恢复机制,近八成插件崩溃可在用户无感知的情况下自动恢复。
  • 双11活动高峰期(0:00-0:30)插件加载请求激增,但崩溃率稳定在0.2%以下,证明Shadow的稳定性可满足大促场景需求。
7.2. 插件性能监控

核心性能指标

指标平均值P95P99说明
首次插件加载耗时1.2s2.8s4.5s包含下载、解压、安装、启动全流程
二次启动耗时0.3s0.6s0.9s插件已安装,仅需加载启动
插件Activity启动耗时0.15s0.3s0.5s与原生Activity启动耗时相当
插件与宿主通信耗时8ms25ms50msBinder跨进程调用
插件内存占用35MB68MB95MB独立进程,不影响宿主

优化成果

  • 通过预加载策略,将首次启动的感知时间从平均2.8秒优化至1.2秒,降幅57%。
  • 采用异步加载+骨架屏方案,用户感知加载时长缩短至0.8秒内。
  • 插件独立进程的内存占用得到有效控制,未出现OOM问题。
7.3. 性能监控详细数据

加载时间分解

首次加载双11插件 (总耗时 1200ms)
├── 插件包下载 (网络)     500ms ████████████████░░░░ 41.7%
├── MD5校验 + 解压        200ms ████████░░░░░░░░░░░░ 16.7%
├── 插件安装 (Dex优化)    300ms ████████████░░░░░░░░ 25.0%
├── 进程启动             150ms ██████░░░░░░░░░░░░░░ 12.5%
└── Activity初始化        50ms ██░░░░░░░░░░░░░░░░░░ 4.1%

内存占用趋势

时间节点宿主内存 (MB)双11插件进程 (MB)说明
插件未加载180-基线
插件预加载后1850仅下载,未启动进程
首次打开插件页19042插件进程启动
浏览商品详情19568图片加载,内存峰值
退出插件页后18835部分缓存保留
卸载插件后181-完全释放

性能优化建议

  1. 预加载策略:在用户可能打开插件前(如启动App后3秒),提前下载插件包,但不启动进程。
  2. 资源按需加载:插件内的图片、布局等资源采用懒加载策略,避免一次性加载过多。
  3. 进程生命周期管理:退出插件页面后,延迟30秒再销毁进程,避免频繁创建销毁的开销。
7.4. 四大组件使用案例

Activity 使用示例

// 插件内启动新的Activity
public class FlashSaleActivity extends PluginContainerActivity {
    
    private void openProductDetail(String productId) {
        // 标准Intent跳转
        Intent intent = new Intent(this, ProductDetailActivity.class);
        intent.putExtra("product_id", productId);
        startActivity(intent);
        
        // 也支持startActivityForResult
        // startActivityForResult(intent, REQUEST_CODE);
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
            // 处理返回结果
            refreshCart();
        }
    }
}

Service 使用示例

// 1. 在插件中定义Service
public class DownloadService extends PluginService {
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String taskId = intent.getStringExtra("task_id");
        // 执行下载任务
        startDownload(taskId);
        return START_NOT_STICKY;
    }
    
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

// 2. 在宿主中启动插件Service
public void startPluginDownload(String taskId) {
    Intent intent = new Intent();
    intent.setClassName(getPackageName(), 
        "com.double11.plugin.DownloadService");
    intent.putExtra("task_id", taskId);
    
    try {
        mPluginLoader.startPluginService(intent);
    } catch (RemoteException e) {
        // 降级:使用宿主自身的下载服务
        startHostDownload(taskId);
    }
}

// 3. 绑定插件Service(用于通信)
public void bindPluginService() {
    Intent intent = new Intent();
    intent.setClassName(getPackageName(), 
        "com.double11.plugin.DownloadService");
    
    try {
        mPluginLoader.bindPluginService(intent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                // 获取Binder,可以调用Service的方法
                IDownloadService downloadService = 
                    IDownloadService.Stub.asInterface(service);
                // 进行通信...
            }
            
            @Override
            public void onServiceDisconnected(ComponentName name) {
                // 处理断开连接
            }
        }, Context.BIND_AUTO_CREATE);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

BroadcastReceiver 使用示例 (替代方案)

由于Shadow对BroadcastReceiver支持有限,推荐使用LocalBroadcastManagerEventBus作为替代:

// 插件内发送事件
public class CartManager {
    
    public void addToCart(String productId) {
        // 插件内部处理...
        
        // 使用EventBus发送事件(同进程)
        EventBus.getDefault().post(new CartUpdatedEvent(productId));
        
        // 或使用LocalBroadcastManager
        Intent intent = new Intent("ACTION_CART_UPDATED");
        intent.putExtra("product_id", productId);
        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
    }
}

// 宿主接收事件
public class CartActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // EventBus方式
        EventBus.getDefault().register(this);
        
        // LocalBroadcastManager方式
        LocalBroadcastManager.getInstance(this).registerReceiver(
            mCartReceiver, 
            new IntentFilter("ACTION_CART_UPDATED")
        );
    }
    
    @Subscribe
    public void onCartUpdated(CartUpdatedEvent event) {
        // 更新购物车UI
        refreshCartUI();
    }
    
    private BroadcastReceiver mCartReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String productId = intent.getStringExtra("product_id");
            refreshCartUI(productId);
        }
    };
}

ContentProvider 使用示例 (替代方案)

对于数据共享需求,推荐使用宿主提供的ContentProvider公共数据库

// 宿主提供ContentProvider
public class AppDataProvider extends ContentProvider {
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // 返回宿主的数据,如用户信息、购物车数据等
        return getCartDataCursor();
    }
    
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // 插件调用此方法插入数据
        String partKey = uri.getQueryParameter("part_key");
        // 根据partKey区分数据来源
        return insertCartData(values);
    }
}

// 插件中访问
public class Double11Plugin {
    
    private void syncCartToHost(String productId, int count) {
        ContentValues values = new ContentValues();
        values.put("product_id", productId);
        values.put("count", count);
        values.put("source", "double11_plugin");
        
        Uri uri = Uri.parse("content://com.double11.app/cart");
        getContentResolver().insert(uri, values);
    }
}

8. Shadow插件化与ARouter组件化如何共存

8.1. 核心挑战

在同时使用Shadow插件化和ARouter组件化的项目中,面临的核心问题是:

挑战说明
路由表独立插件和宿主各自维护自己的ARouter路由表,无法互相感知
ClassLoader隔离ARouter在宿主ClassLoader中无法直接加载插件内的类
注解处理器冲突两者都依赖编译时注解处理,可能产生冲突
依赖管理复杂需要同时管理ARouter和Shadow的依赖,版本兼容性风险
8.2. 混合架构方案

方案一:宿主使用ARouter,插件内用原生跳转(推荐)

宿主工程                          插件工程
┌─────────────────┐              ┌─────────────────┐
│  使用ARouter    │              │  使用原生Intent │
│                 │              │                 │
│  @Route(path)   │              │  startActivity()│
│  navigation()   │◄─────────────│                 │
│                 │  通过Intent   │                 │
│                 │  传递路由信息  │                 │
└─────────────────┘              └─────────────────┘

实现代码

// 宿主中定义路由
@Route(path = "/host/cart/activity")
public class CartActivity extends AppCompatActivity {
    // 宿主内的页面
}

// 插件内跳转到宿主的页面
public class Double11PluginActivity extends PluginContainerActivity {
    
    private void openCartPage() {
        // 方式1:通过ARouter的Intent构建
        Intent intent = ARouter.getInstance()
            .build("/host/cart/activity")
            .withString("from", "double11")
            .getIntent();
        startActivity(intent);
        
        // 方式2:直接通过类名跳转(更简单)
        Intent intent = new Intent();
        intent.setClassName(getPackageName(), 
            "com.double11.app.CartActivity");
        startActivity(intent);
    }
}

方案二:路由服务化(更解耦)

将路由能力封装成服务,通过Shadow的通信机制暴露给插件:

// 1. 公共模块定义路由服务接口
public interface IRouterService extends IInterface {
    void navigate(String path, Bundle params);
    
    abstract class Stub extends Binder implements IRouterService {
        // AIDL风格实现
    }
}

// 2. 宿主实现路由服务
public class RouterServiceImpl extends IRouterService.Stub {
    
    @Override
    public void navigate(String path, Bundle params) {
        // 使用ARouter进行跳转
        Postcard postcard = ARouter.getInstance().build(path);
        if (params != null) {
            postcard.with(params);
        }
        postcard.navigation();
    }
}

// 3. 加载插件时注入路由服务
public class PluginManager {
    
    public void loadPlugin(String partKey) {
        Bundle bundle = new Bundle();
        bundle.putBinder("router_service", new RouterServiceImpl().asBinder());
        mPluginManager.enter(context, partKey, bundle, callback);
    }
}

// 4. 插件内调用
public class Double11PluginActivity extends PluginContainerActivity {
    
    private IRouterService mRouterService;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        IBinder binder = getIntent().getExtras().getBinder("router_service");
        mRouterService = IRouterService.Stub.asInterface(binder);
    }
    
    private void openCartPage() {
        Bundle params = new Bundle();
        params.putString("from", "double11");
        mRouterService.navigate("/host/cart/activity", params);
    }
}
8.3. 优缺点总结
方案优点缺点
方案一:混合使用实现简单,改造成本低插件内无法享受ARouter的便利性
方案二:路由服务化完全解耦,插件也能享受路由能力需要额外开发,有一定学习成本
8.4. 关于ComboLite 2.0等新框架

随着2025年ComboLite 2.0等新一代框架的出现,插件化技术也在演进:

  • 多端融合:ComboLite 2.0支持在Flutter页面中嵌入WebView或小程序容器,实现了跨技术栈的融合。
  • 调试能力:新框架普遍加强了调试支持,解决了传统插件化框架"插件内无法Debug"的痛点。
  • 轻量级:相比Shadow,新框架更加轻量,对工程侵入性更小。

启示:技术选型时需要前瞻性地评估框架的生命周期和社区活跃度,避免陷入技术债务。


9. 深度思考:能否用Shadow加载第三方APK?

9.1. 结论

不能。Shadow无法直接加载一个未经适配的第三方APK。

9.2. 根本原因
  1. 编译时Transform处理:Shadow要求在编译阶段对插件代码进行Transform,将原生的Activity替换为PluginContainerActivityService替换为PluginService等。未经处理的第三方APK无法在Shadow环境中运行。

  2. 资源ID冲突:Shadow通过修改R.java中的资源ID,为每个插件分配独立的资源段,避免与宿主或其他插件冲突。第三方APK的资源ID未经过此处理,必然导致资源访问错误。

  3. ClassLoader隔离:插件运行在独立的ClassLoader中,需要插件代码适配这种隔离机制。第三方APK的代码默认假设自己运行在主ClassLoader中,会因找不到类而崩溃。

  4. 生命周期管理:Shadow对插件内四大组件的生命周期进行了接管,需要插件主动配合。第三方APK没有这种配合,会导致生命周期回调异常。

9.3. 类比理解

就像微信给了一个入口跳转京东,京东的APK并不是作为一个插件运行在微信内部,而是:

  • 通过Scheme跳转拉起京东App(独立进程)
  • 或者通过小程序的方式运行(京东需要专门开发微信小程序版本)

Shadow的角色更像是"小程序容器",而不是"虚拟机"。它要求插件主动适配,而不是被动加载。

9.4. 如果要实现"加载任意APK"需要什么?

如果要实现类似功能,需要:

  1. Hook框架:使用Xposed、VirtualApp等框架,通过Hook系统API来模拟运行环境。
  2. 资源处理:运行时动态修改资源ID,解决冲突问题。
  3. Dex加载:使用DexClassLoader加载APK的Dex,但需要解决ClassLoader隔离问题。
  4. 四大组件模拟:通过占坑Activity的方式,模拟插件内Activity的启动。

这些方案技术难度极高,且存在兼容性和稳定性问题,不是Shadow的设计目标。

9.5. 正确使用姿势

Shadow的正确使用姿势是:插件开发者主动接入,遵循Shadow规范,享受插件化带来的动态能力

第三方SDK
    │
    ▼
是否需要动态加载?
    │
    ├── 是 ──▶ 使用Shadow规范开发插件
    │         (需要第三方配合)
    │
    └── 否 ──▶ 传统集成方式
              (直接依赖SDK)
9.6. 最佳实践建议
  1. 明确目标:Shadow适合"自己开发、自己分发"的插件,不适合"加载任意第三方APK"。
  2. 合作开发:如果需要集成第三方动态能力,应与第三方协商,共同按Shadow规范开发插件版本。
  3. 小程序替代:对于无法配合的第三方,可以考虑使用小程序容器(如微信小程序SDK)作为替代方案。

10:项目的地址:

GitHub: ATaoDuoduoShadow

Shadow作为一个强大的插件化框架,为电商大促等需要高度灵活性的业务场景提供了完美的解决方案。通过两年的实践,我们验证了其在稳定性、性能和动态化能力上的优势。

当然,道路并非一帆风顺,我们遇到了调试难、通信复杂、架构改造等挑战。但通过深入理解Shadow的设计思想(尤其是Loader和Runtime机制),并结合服务注册中心、Binder跨进程通信、全动态化下发等设计模式,这些问题都得到了妥善解决。