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

写在前面

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. 项目效果图
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架构(动态插入)

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

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

scss 复制代码
电商主APP (宿主)
├── 插件管理层 (插件Manager)
├── Shadow引擎层(宿主)
└── 核心业务插件 (插件)
    ├── 首页
    ├── 分类  
    ├── 购物车 
    ├── 我的
    └── 双11插件(shadow)
3.2. 整体的架构是怎么样的?分层 (需要修改插件)
bash 复制代码
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. 整体业务流程图

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


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

考虑下面2个问题

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

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

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

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

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

用的1.8的jdk进行编译的!

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

arduino 复制代码
classpath 'com.android.tools.build:gradle:7.0.2' 

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

在host工程中添加依赖:

java 复制代码
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

php 复制代码
include ':sample-runtime'
include ':sample-loader'

用到的是application,而不是libray

arduino 复制代码
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

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


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. 反射调用
ini 复制代码
Class<?> factoryClass = classLoader.loadClass("com.tencent.shadow.dynamic.impl.ManagerFactoryImpl");
ManagerFactory factory = (ManagerFactory) factoryClass.newInstance();
PluginManagerImpl manager = factory.buildManager(context);

DynamicPluginManager,里面调用了实现

scss 复制代码
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

打包插件

ruby 复制代码
# 编译并打包插件
./gradlew :plugin-double11:packageDebugPlugin

# 输出文件位置
# plugin-double11/build/outputs/plugin/debug/plugin-double11-debug.zip
5.1.3 插件配置
css 复制代码
// 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 宿主中的加载插件的核心逻辑
typescript 复制代码
// 宿主: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. 异常自动上报,方便定位问题

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

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

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

5.3. 如何卸载插件?

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

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

插件卸载 - 核心实现

流程图

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

核心代码

scss 复制代码
// 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 加载 - 核心代码

流程图

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

核心代码

5.4.1 插件中定义 Activity
scala 复制代码
// 插件工程: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
scala 复制代码
// 插件工程: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
scala 复制代码
// 宿主工程: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)
xml 复制代码
<!-- 插件工程:AndroidManifest.xml -->
<manifest package="com.double11.plugin">
    
    <application>
        <!-- Shadow 会自动处理 Activity,无需手动注册 -->
        <!-- ProductDetailActivity 不需要在这里声明 -->
    </application>
    
</manifest>
5.4.5 宿主 Manifest(注册插件容器)
xml 复制代码
<!-- 宿主工程: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 定义 继承 PluginContainerActivity extends PluginContainerActivity
无需注册 插件 Manifest 不需要注册 Activity 自动处理
跳转方式 标准 Intent new Intent(context, TargetActivity.class)
宿主配置 注册容器 Activity PluginContainerActivity

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

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

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

页面就是: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

ini 复制代码
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
ini 复制代码
// 获取插件的 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. 配置就会导致能看到它上面的一层结构!
php 复制代码
include ':common'
project(':common').projectDir = new File('../common')

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


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

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

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

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

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

java 复制代码
// 宿主工程: 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)
arduino 复制代码
// 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 宿主实现接口
ini 复制代码
// 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 加载插件时传递接口实例
java 复制代码
// 宿主: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 插件接收并使用
scss 复制代码
// 插件: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)
java 复制代码
// 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
scala 复制代码
// 插件工程: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
typescript 复制代码
// 宿主工程: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 宿主接收购物车更新
typescript 复制代码
// 宿主工程: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);
    }
}

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

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

scala 复制代码
// 插件添加商品后发送事件
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. 插件 AServiceRegistry 查找所需服务。

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

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

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

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

核心原理图解

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        宿主进程                              │
│                                                              │
│  ┌──────────────┐          ┌──────────────┐                │
│  │   插件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)
java 复制代码
// 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 购物车插件注册服务
scala 复制代码
// 购物车插件: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插件调用购物车服务
scala 复制代码
// 双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增强(支持服务注册)
typescript 复制代码
// 宿主工程: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);
    }
}

流程图解

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

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

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

typescript 复制代码
// 公共模块定义接口
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的加载和管理是通过自己检测自己的更新

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

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

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

css 复制代码
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  宿主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)拉起 LoaderRuntime 来运行具体的业务插件(如 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 字段

ini 复制代码
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#insertupdate,将数据库中的插件 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。

ini 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    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):

ini 复制代码
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 示例

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

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

partKey latestVersion downloadUrl patchUrl dependsOn required
pluginB 3 /full/b3.zip /patch/b2-b3.hpz [] true
pluginC 2 /full/c2.zip /patch/c1-c2.hpz [] true
pluginD 1 /full/d1.zip null [] false
5.7.2.4 Manager 升级检测逻辑

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

less 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    Manager 版本检测流程                         │
├─────────────────────────────────────────────────────────────────┤
│  1. 请求服务端接口 /api/plugins/latest                          │
│     传入当前已安装插件列表:[{partKey:"B",version:2},           ││                            {partKey:"C",version:1}]              │
│                                                                 │
│  2. 服务端返回需要更新的插件:                                   │
│     - pluginB: 2→3 (patch 可用)                                 │
│     - pluginC: 1→2 (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 指定:

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

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

完整时序图

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

实施清单

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

版本演进时间线

css 复制代码
时间线
│
├── 第一次发版
│   ├── 宿主 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(新增)

组件关系图

核心设计原则

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

服务端插件版本表

partKey version downloadUrl patchUrl size md5 dependsOn minHostVersion status
plugin_b 3 /full/b3.zip /patch/b2-b3.hpz 5MB abc123 [] 1.0 active
plugin_b 2 /full/b2.zip - 5MB def456 [] 1.0 archived
plugin_c 2 /full/c2.zip /patch/c1-c2.hpz 3MB ghi789 [] 1.0 active
plugin_c 1 /full/c1.zip - 3MB jkl012 [] 1.0 archived
plugin_d 1 /full/d1.zip - 2MB mno345 ["plugin_b"] 1.0 active

用户状态矩阵

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

实施步骤清单

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

总结: 核心方案全动态化 。宿主只负责加载 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

热修复配置文件

json 复制代码
// 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 优惠券计算器(待修复)
arduino 复制代码
// 插件工程: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 修复后的版本
ini 复制代码
// 插件工程: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热修复管理器
typescript 复制代码
// 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 热修复配置文件解析
arduino 复制代码
// 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 在宿主中触发检查
scala 复制代码
// 宿主工程: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();
    }
}

热修复流程时序图

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

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

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

核心原理

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

核心代码

5.9.1 资源热修复管理器
typescript 复制代码
// 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 资源修复配置
arduino 复制代码
// 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 热修复配置文件示例
json 复制代码
{
  "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 插件内资源使用(需兼容新旧版本)
scss 复制代码
// 插件工程: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热修复管理器
typescript 复制代码
// 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热修复配置
arduino 复制代码
// 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库(支持动态替换)
typescript 复制代码
// 插件工程: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 在插件中使用
scala 复制代码
// 插件工程: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 资源热修复 图片侵权、文案错误 资源包替换
10 SO库热修复 Native崩溃、性能问题 SO文件替换

7. 上线后实际效果对比

7.1. 崩溃率统计

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

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

关键发现

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

核心性能指标

指标 平均值 P95 P99 说明
首次插件加载耗时 1.2s 2.8s 4.5s 包含下载、解压、安装、启动全流程
二次启动耗时 0.3s 0.6s 0.9s 插件已安装,仅需加载启动
插件Activity启动耗时 0.15s 0.3s 0.5s 与原生Activity启动耗时相当
插件与宿主通信耗时 8ms 25ms 50ms Binder跨进程调用
插件内存占用 35MB 68MB 95MB 独立进程,不影响宿主

优化成果

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

加载时间分解

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

内存占用趋势

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

性能优化建议

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

Activity 使用示例

java 复制代码
// 插件内启动新的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 使用示例

java 复制代码
// 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作为替代:

java 复制代码
// 插件内发送事件
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公共数据库

java 复制代码
// 宿主提供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,插件内用原生跳转(推荐)

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

实现代码

java 复制代码
// 宿主中定义路由
@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的通信机制暴露给插件:

java 复制代码
// 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规范,享受插件化带来的动态能力

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

10:项目的地址:

GitHub: ATaoDuoduoShadow

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

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

相关推荐
西电研梦1 小时前
西电26考研初/复试分数占比、笔试、面试斩杀线
考研·面试·职场和发展·研究生·西安电子科技大学
wl85111 小时前
SAP HCM 公积金超过上限后的计税方案
前端·html
二月夜1 小时前
Vue项目打包为WAR文件部署Tomcat完整指南
前端·vue.js·tomcat
终端鹿1 小时前
Vue3 核心 API 完结篇:toRaw / markRaw / shallowReactive / shallowRef 等进阶响应式 API 详解
前端·javascript·vue.js
bigcarp1 小时前
edge浏览器IE模式(Internet Explorer 兼容)-tplink摄像头需要
前端·edge
27669582921 小时前
悟空租车帮app最新登录算法
开发语言·前端·python·悟空app·租车帮·租车帮app·租车帮登录逆向
摇滚侠1 小时前
微信小程序是前端,也需要 Java 开发的后端服务
java·前端·微信小程序
lxf_gis1 小时前
【JavaEE】Spring Web MVC
前端·spring·java-ee
sunxunyong2 小时前
集群增加用户&权限
前端·javascript·vue.js