4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理

前言

在 Android 开发中,动态化一直是一个热门话题。腾讯开源的 Shadow 框架以其"无侵入"、"无需 Root"、"兼容性好"著称。很多人觉得 Shadow 源码复杂难懂,但其实其核心思想非常纯粹:把插件 Activity 伪装成一个普通类,通过宿主容器代理转发生命周期。

本文将剥离复杂的编译期字节码修改(AOP),手动实现一个简化版的 ShadowLite 。我们将通过三个模块(接口层、插件层、宿主层),亲手还原插件化的三大核心难题:类加载、资源加载、生命周期分发,并深度解析其中四个最关键的"魔法"方法。

核心结论提前看

Shadow 的核心 trick 在于:插件里的 Activity 其实不是真正的 Activity,它只是一个继承了普通类的 POJO。 真正注册在 Manifest 里的是宿主的"壳子 Activity"(Container),由壳子持有插件对象,手动调用其生命周期方法。


简单的版本shadow主要是解决:类加载,资源加载,Activity的生命周期问题!

一、核心设计思路

要实现一个简单的插件化框架,我们需要解决三个问题:

  1. 类加载 (ClassLoader) :如何加载未安装在系统中的 APK 中的类? -> DexClassLoader
  2. 资源加载 (Resources) :如何加载插件 APK 中的图片和布局? -> 反射构建 AssetManager
  3. 生命周期 (Lifecycle) :插件 Activity 未在 Manifest 注册,系统无法回调其生命周期怎么办? -> 代理模式 + 接口回调

ShadowLite 的核心思路就是将这三点分离并重新组合 。我们将插件 Activity 降级为一个实现了特定生命周期接口的普通对象。宿主的容器 Activity 负责提供真实的 ContextResourcesClassLoader,并将系统回调的事件(如 onCreate)转发给插件对象。

架构设计

我们将工程拆分为三个 Module:

  1. module_interface:公共接口库。定义生命周期回调接口,宿主和插件都依赖它。
  2. module_plugin :插件工程。包含具体的业务 Activity,但它们不继承 android.app.Activity,而是继承我们定义的普通基类。
  3. shadow_lite (宿主) :宿主工程。包含 PluginContainerActivity(壳子)和 #### PluginManagerImpl``(加载器)。

下图展示了 ShadowLite 的模块依赖与核心交互流程:

二、手机 ShadowLite 实战编码

都是根据这个架构图写的:

2.1 定义生命周期接口 (module_interface)

这是宿主与插件通信的桥梁。插件 Activity 实现此接口,宿主容器通过此接口调用插件的方法。

java 复制代码
public interface LifecyclerInterface {

    // 绑定宿主上下文(关键:让插件获取到宿主的 Context)
    void attach_Inner(Activity hostActivity);

    // 模拟生命周期回调
    void onCreate_Inner(Bundle savedInstanceState);
    void onStart_Inner();
    void onResume_Inner();
    void onPause_Inner();
    void onStop_Inner();
    void onDestroy_Inner();

    // 常用能力代理
    LayoutInflater getLayoutInflater_Inner();
}

2.2 编写插件基类与业务类 (module_plugin)

这是 Shadow 思想的精髓。插件中的 Activity 不需要在 Manifest 注册 ,因此它不能直接继承系统的 Activity(否则安装时会报错或无法启动)。我们让它继承一个普通类 ShadowActivity

2.2.1 插件基类 ShadowActivity

这个类实现了 LifecyclerInterface,内部持有一个真正的 Activity 对象(即宿主传进来的容器),将所有依赖 Context 的操作委托给它。

java 复制代码
public class ShadowActivity implements LifecyclerInterface {

    // 宿主容器 Activity,插件的所有 Context 相关操作都委托给它
    protected Activity hostActivity;

    @Override
    public void attach_Inner(Activity activity) {
        this.hostActivity = activity;
    }

    // --- 生命周期空实现,供子类重写 ---
    @Override
    public void onCreate_Inner(Bundle save) {}
    @Override
    public void onStart_Inner() {}
    @Override
    public void onResume_Inner() {}
    @Override
    public void onPause_Inner() {}
    @Override
    public void onStop_Inner() {}
    @Override
    public void onDestroy_Inner() {}

    // --- 上下文能力代理 ---
    @Override
    public LayoutInflater getLayoutInflater_Inner() {
        if (hostActivity != null) {
            return hostActivity.getLayoutInflater();
        }
        return null;
    }

    // 代理其他常用方法,确保插件代码以为自己在 Activity 环境中运行
    public ApplicationInfo getApplicationInfo() {
        return hostActivity.getApplicationInfo();
    }
    
    public Resources getResources() {
        // 注意:这里应该返回插件的 Resources,但在简单版中,
        // 我们通过重写宿主容器的 getResources 来间接实现,或者在此处做特殊处理
        return hostActivity.getResources(); 
    }
    
    public Window getWindow() {
        return hostActivity.getWindow();
    }
    
    public void setContentView(int layoutResID) {
        if (hostActivity != null) {
            hostActivity.setContentView(layoutResID);
        }
    }
}

2.2.2 插件业务类 PluginActivity

注意:它继承的是 ShadowActivity (普通类),而不是 android.app.Activity

java 复制代码
public class PluginActivity extends ShadowActivity {

    @Override
    public void onCreate_Inner(Bundle savedInstanceState) {
        super.onCreate_Inner(savedInstanceState);
        // 这里的 R.layout 是插件工程的 R,setContentView 委托给了宿主
        // 关键点:宿主容器的 getResources 必须被替换为插件的 Resources,否则找不到插件的 layout
        setContentView(R.layout.activity_main); 
    }

    @Override
    public void onStart_Inner() {
        // 业务逻辑
    }
}

2.3 宿主核心实现 (shadow_lite)

宿主负责"脏活累活":加载 APK、创建 ClassLoader、反射创建 AssetManager、以及充当容器。

2.3.1 插件管理器 PluginManagerImpl

负责加载插件 APK 并初始化 DexClassLoaderResources

java 复制代码
public class PluginManagerImpl {

    private Resources pluginResources;
    private DexClassLoader pluginClassLoader;
    private Context hostContext;

    private static class Holder {
        private static final PluginManagerImpl INSTANCE = new PluginManagerImpl();
    }

    public static PluginManagerImpl getInstance() {
        return Holder.INSTANCE;
    }

    public void setContext(Context context) {
        this.hostContext = context;
    }

    public Resources getPluginResources() {
        return pluginResources;
    }

    public DexClassLoader getPluginClassLoader() {
        return pluginClassLoader;
    }

    /**
     * 加载插件 APK
     * @param apkPath 插件 APK 的绝对路径
     */
    public void loadPlugin(String apkPath) {
        if (hostContext == null) return;

        // 1. 初始化 DexClassLoader
        File dexOutputDir = hostContext.getDir("dex", Context.MODE_PRIVATE);
        pluginClassLoader = new DexClassLoader(
                apkPath,
                dexOutputDir.getAbsolutePath(),
                null,
                hostContext.getClassLoader()
        );

        // 2. 初始化 Resources (核心难点:反射创建 AssetManager)
        try {
            AssetManager assetManager = createAssetManager(apkPath);
            pluginResources = new Resources(
                    assetManager,
                    hostContext.getResources().getDisplayMetrics(),
                    hostContext.getResources().getConfiguration()
            );
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Load plugin resources failed", e);
        }
    }

    /**
     * 【核心魔法 1】反射创建指向特定 APK 的 AssetManager
     */
    private AssetManager createAssetManager(String apkPath) throws Exception {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
        addAssetPathMethod.setAccessible(true);
        int cookie = (int) addAssetPathMethod.invoke(assetManager, apkPath);
        if (cookie == 0) {
            throw new RuntimeException("Failed to add asset path: " + apkPath);
        }
        return assetManager;
    }
}

2.3.2 宿主容器 PluginContainerActivity

这是真正注册在 AndroidManifest.xml 中的 Activity。它像一个"壳",内部实例化插件类,并转发生命周期。

java 复制代码
public class PluginContainerActivity extends Activity {

    private static final String TARGET_CLASS_NAME = "com.example.plugin.PluginActivity";
    private LifecyclerInterface pluginInstance;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        try {
            ClassLoader pluginLoader = PluginManagerImpl.getInstance().getPluginClassLoader();
            if (pluginLoader == null) {
                PluginManagerImpl.getInstance().loadPlugin("/sdcard/plugin.apk");
                pluginLoader = PluginManagerImpl.getInstance().getPluginClassLoader();
            }

            Class<?> clazz = pluginLoader.loadClass(TARGET_CLASS_NAME);
            Object obj = clazz.getDeclaredConstructor().newInstance();
            
            if (obj instanceof LifecyclerInterface) {
                pluginInstance = (LifecyclerInterface) obj;
                pluginInstance.attach_Inner(this);
                pluginInstance.onCreate_Inner(savedInstanceState);
            }
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, "Load Plugin Failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (pluginInstance != null) pluginInstance.onStart_Inner();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (pluginInstance != null) pluginInstance.onResume_Inner();
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        if (pluginInstance != null) pluginInstance.onPause_Inner();
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (pluginInstance != null) pluginInstance.onStop_Inner();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (pluginInstance != null) pluginInstance.onDestroy_Inner();
    }

    // ================= 【核心魔法 2 & 3】关键重写 =================
    
    @Override
    public ClassLoader getClassLoader() {
        ClassLoader loader = PluginManagerImpl.getInstance().getPluginClassLoader();
        return loader != null ? loader : super.getClassLoader();
    }

    @Override
    public Resources getResources() {
        Resources res = PluginManagerImpl.getInstance().getPluginResources();
        return res != null ? res : super.getResources();
    }
    
    @Override
    public LayoutInflater getLayoutInflater() {
        Resources res = getResources(); 
        LayoutInflater inflater = super.getLayoutInflater();
        return inflater.cloneInContext(new ContextThemeWrapper(this, 0) {
            @Override
            public Resources getResources() {
                return res;
            }
        });
    }
}

为了更直观地理解整个调用流程,下图展示了从加载插件到界面显示的关键时序:

sequenceDiagram participant 宿主App participant PluginManager participant PluginContainerActivity participant PluginActivity participant 系统 宿主App->>PluginManager: loadPlugin("/sdcard/plugin.apk") PluginManager-->>PluginManager: 创建 DexClassLoader PluginManager-->>PluginManager: 反射创建 AssetManager
并添加插件路径 PluginManager-->>PluginManager: 构建插件 Resources PluginManager-->>宿主App: 加载完成 宿主App->>系统: startActivity(Intent 指向
PluginContainerActivity) 系统->>PluginContainerActivity: onCreate() PluginContainerActivity->>PluginManager: getPluginClassLoader() PluginManager-->>PluginContainerActivity: 返回 DexClassLoader PluginContainerActivity->>PluginManager: getPluginResources() PluginManager-->>PluginContainerActivity: 返回插件 Resources PluginContainerActivity->>PluginContainerActivity: 重写 getResources() 返回插件 Resources PluginContainerActivity->>PluginContainerActivity: 重写 getClassLoader() 返回插件 ClassLoader PluginContainerActivity->>PluginContainerActivity: 加载 PluginActivity 类并实例化 PluginContainerActivity->>PluginActivity: attach_Inner(this) PluginActivity-->>PluginActivity: 保存 hostActivity 引用 PluginContainerActivity->>PluginActivity: onCreate_Inner(savedInstanceState) PluginActivity->>PluginActivity: setContentView(R.layout.activity_main) PluginActivity->>PluginActivity: 内部调用 hostActivity.setContentView() PluginActivity->>PluginContainerActivity: 实际执行 setContentView() PluginContainerActivity->>PluginContainerActivity: getLayoutInflater() PluginContainerActivity->>PluginContainerActivity: 返回的 LayoutInflater 内部使用插件 Resources PluginContainerActivity->>PluginContainerActivity: 加载插件布局文件 PluginContainerActivity-->>PluginActivity: 布局设置完成 PluginActivity-->>PluginContainerActivity: onCreate_Inner 返回 Note over PluginContainerActivity,系统: 后续系统生命周期回调
继续转发给 PluginActivity 系统->>PluginContainerActivity: onStart() PluginContainerActivity->>PluginActivity: onStart_Inner() 系统->>PluginContainerActivity: onResume() PluginContainerActivity->>PluginActivity: onResume_Inner()

2.4 配置文件 AndroidManifest.xml

重点 :只需要注册宿主的 PluginContainerActivity。插件中的 PluginActivity 绝对不要在这里注册。

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.taoduoduo.shadow">

    <application ... >
        <!-- 宿主主入口 -->
        <activity android:name=".TaoduoduoMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- 唯一的容器 Activity,用于承载所有插件页面 -->
        <activity android:name=".PluginContainerActivity" />
        
    </application>
</manifest>

三、四大核心方法深度解析

这四个方法是 ShadowLite 的"心脏"和"血管"。如果不理解它们的内部实现细节,插件化就只是一堆黑盒代码。

1. createAssetManager(String apkPath):资源的"任意门"

  • 核心作用 :Android 系统默认只加载宿主 APK 的资源。这个方法通过反射 ,强行创建一个新的 AssetManager 实例,并将插件 APK 的路径"注入"进去,使其能够读取插件内部的图片、布局和字符串。

  • 为什么需要反射AssetManager 的构造函数是隐藏的,且 addAssetPath 方法被标记为 @hide(非公开 API)。系统希望应用只管理自己的资源,但插件化必须打破这个限制。

  • 内存模型

    • 宿主 AssetManager : [ /data/app/host.apk ] -> 只能读宿主的 R.layout.main
    • 插件 AssetManager (新建) : [ /sdcard/plugin.apk ] -> 能读插件的 R.layout.main
    • 关键点 : 这两个 Manager 是独立的。如果我们用宿主的 ContextsetContentView,它会用宿主的 Manager 去找布局,当然找不到插件的布局。所以必须替换 Context 中的 Resources。

2. loadPlugin(String apkPath):代码的"搬运工"

  • 核心作用 :初始化双核引擎:DexClassLoader (负责加载类)和 Resources(负责加载资源)。它是插件初始化的入口。

  • 双亲委派模型的"破坏"

    • 正常情况:BootClassLoader -> SystemClassLoader -> PathClassLoader (宿主)。
    • 插件化情况:当插件代码请求一个类时,DexClassLoader 先问父加载器(宿主)。宿主说"没有",DexClassLoader 转身去 /sdcard/plugin.apk 里找。
    • 反之 :如果插件代码引用了宿主的类(如 Activity 或接口),父加载器(宿主)会说"有!"直接返回。这保证了接口的一致性 ,避免 ClassCastException
  • 资源串联 :在 loadPlugin 中,我们用新建的 AssetManager 构建全新的 Resources 对象,并复用宿主的屏幕密度配置(Metrics/Configuration),让插件 UI 表现与宿主一致。

3. getResources():狸猫换太子

  • 核心作用 :这是最关键 的一步重写。当插件代码调用 setContentView(R.layout.xxx) 时,底层最终会调用 Context.getResources() 来获取资源。如果返回的是宿主的 Resources,插件布局必挂。

  • 执行流程推演

    1. 插件代码执行:setContentView(R.layout.activity_plugin)
    2. 调用链最终到达 LayoutInflater.inflate(...)
    3. LayoutInflatercontext 中获取 Resources
    4. 拦截 :因为 PluginContainerActivity 重写了 getResources(),此时返回的是 pluginResources
    5. inflate 方法拿着 pluginResources (内部指向插件 APK) 去解析 XML,成功找到布局和圖片。
  • 如果没有这一步inflate 会拿着宿主的 AssetManager 去宿主 APK 里找 activity_plugin.xml,结果当然是 Resources$NotFoundException

4. getClassLoader():类的"导航仪"

  • 核心作用:确保在容器 Activity 内部(以及其衍生的子组件中)加载类时,优先去插件 APK 中寻找。

  • 应用场景

    • Fragment 的状态恢复。
    • 序列化机制 (Serializable / Parcelable) 反序列化时。
    • 依赖注入框架 (如 ARouter, Dagger) 查找实现类时。
  • 潜在坑点 :必须保证父子加载器顺序正确。插件的 DexClassLoader 父加载器必须是宿主的 ClassLoader,这样才能共享接口类定义,避免类型转换异常。


四、实战操作步骤

  1. 构建工程

    • 创建 module_interface (Java Library)。
    • 创建 module_plugin (Android Library),依赖 interface,编写 PluginActivity
    • 创建 shadow_lite (Android Application),依赖 interface,编写 PluginManagerImplPluginContainerActivity
  2. 打包插件

    • 单独编译 module_plugin 所在的工程(或者将其作为独立 App 编译),生成 plugin.apk
    • 注意 :为了简化,插件工程也需要有自己的 ApplicationManifest(虽然里面的 Activity 不注册,但需要声明包名和资源)。
  3. 部署测试

    • 将生成的 plugin.apk 推送到手机 /sdcard/plugin.apk
    • 安装并运行宿主 shadow_lite
    • 点击按钮启动 PluginContainerActivity
    • 现象 :界面显示了插件 PluginActivity 中定义的布局和内容。

五、总结与扩展

通过这个 ShadowLite,我们理解了插件化的骨架:

  1. ClassLoader 隔离:实现代码的热更新。
  2. Resources 隔离:实现资源的独立加载。
  3. 生命周期代理:绕过系统限制,实现未注册组件的运行。

真实 Shadow 框架做了什么扩展?

  • AOP 字节码修改 :手动写 ShadowActivity 太麻烦且容易出错。真实的 Shadow 在编译期通过 Gradle 插件,自动将插件中所有的 extends Activity 修改为 extends ShadowActivity,并将所有 super.onCreate() 调用修改为调用代理逻辑。这对开发者完全透明。
  • 多进程支持:处理不同进程间的 ClassLoader 共享和资源同步。
  • Intent 匹配 :模拟系统的 Intent 解析机制,支持 startActivity(new Intent(this, PluginActivity.class)) 这种标准写法,而不是手动 loadClass

源码的地址:

github.com/pengcaihua1...


参考博客: mp.weixin.qq.com/s?__biz=MzA...

blog.51cto.com/u_16163452/...

相关推荐
亿元程序员2 小时前
这款值68亿的游戏,你不实战一下吗?安排!
前端
Kapaseker2 小时前
一杯美式搞定 Kotlin 空安全
android·kotlin
摸鱼的春哥2 小时前
Agent教程15:认识LangChain(中),状态机思维
前端·javascript·后端
三少爷的鞋2 小时前
Android 协程时代,Handler 应该退休了吗?
android
明月_清风2 小时前
告别遮挡:用 scroll-padding 实现优雅的锚点跳转
前端·javascript
明月_清风2 小时前
原生 JS 侧边栏缩放:从 DOM 监听到底层优化
前端·javascript
万少11 小时前
HarmonyOS 开发必会 5 种 Builder 详解
前端·harmonyos
哈里谢顿12 小时前
1000台裸金属并发创建中的重难点问题分析
面试
哈里谢顿12 小时前
20260303面试总结(全栈)
面试