深入理解Android插件化

285 阅读16分钟

插件化的APP可以根据上线后用户所需,下载所需要的组件。组件化是全部打包。需要理解:组件化是一个整体,无论哪个模块有问题,或者需要更新,都必须全部升级,插件化则不同,只需重新下载对应的组件就行了。

插件化与组件化的本质区别

  • 插件化:APP可以根据用户上线后的实际需求,动态下载并加载所需功能组件,做到按需扩展、更新,灵活性高。
  • 组件化:所有功能模块(组件)在打包时整体编译进APK,无法按需拆卸或独立升级。任何模块有更新或Bug,都需要整体打包和发布,代价高、灵活性差。

插件化的核心——类的加载流程

首先要理解Java中的类加载流程

image.png

插件化本质上就是处理插件包里的类文件如何动态加载进宿主APP。这就涉及到反射、ClassLoader的相关机制。


反射的基本知识

插件化中,类加载后往往通过反射调用方法,下图梳理了反射的基础流程:

image.png

反射的性能问题

反射虽然灵活,但性能开销明显,主要包括:

  1. 产生大量临时对象,频繁GC
  2. 访问权限检查(如private、public);
  3. 生成未优化字节码,执行效率低;
  4. 自动装箱/拆箱频繁
  5. 小量场景下影响不大(通常反射调用次数未超千级不会成为瓶颈)。

安卓中的ClassLoader体系

类的加载主要依赖ClassLoader,Android常见的有三种:

  • BootClassLoader:加载Android Framework层的类(系统API)。
  • PathClassLoader:加载APP和Google官方库的类(如AppCompatActivity)。
  • DexClassLoader:用于加载外部存储/自定义路径下的dex文件或apk、jar。

注意:Android 8.0(Oreo)之后DexClassLoader和PathClassLoader已经合并实现,实际用的是PathClassLoader,但API和用法都保留了DexClassLoader,主要是为了兼容性和扩展。

它们的继承关系如下:

image.png

  • BootClassLoader是ClassLoader的一个内部类。

如何动态加载插件类

步骤一:生成插件dex文件

使用Android SDK的dx工具将编译好的class文件转换为dex文件:

dx --dex --output=output.dex input.class
# output.dex为输出dex路径,input.class为输入class文件的路径

步骤二:用DexClassLoader加载

// optimizedDirectory通常指定为APP的缓存目录,存放优化后的odex文件
DexClassLoader dexClassLoader = new DexClassLoader(
    "/sdcard/text.dex", // 插件dex路径
    MainActivity.this.getCacheDir().getAbsolutePath(), // 优化输出目录
    null, // native库路径,通常为null
    MainActivity.this.getClassLoader() // 父ClassLoader
);

try {
    Class<?> clazz = dexClassLoader.loadClass("com.enjoy.plugin.Test");
    // 后续可反射调用类的方法
} catch (ClassNotFoundException e) {
    throw new RuntimeException(e);
}

说明:
这里的DexClassLoader仅能加载指定dex文件中的类(PathClassLoader也可用于此,但主要用法是加载APP自己的dex)。

步骤三:反射调用插件类的方法

try {
    Class<?> clazz = Class.forName("com.enjoy.plugin.Test", true, dexClassLoader);
    Method print = clazz.getMethod("print");
    print.invoke(null); // 调用静态方法
} catch (Exception e) {
    e.printStackTrace();
}

Java反射加载类的常用方式主要有三种,每种方式有本质区别和适用场景。

方式是否初始化类是否需要类已存在是否支持动态名称典型用途
Class.forName()JDBC驱动、插件加载
ClassLoader.loadClass()插件框架、延迟加载
Xxx.class泛型、注解、编译期已知类型
  • 注意Class.forName要指定自定义的ClassLoader,否则会找不到插件类。
  • 反射调用时需确保方法签名匹配。

小结

  1. 插件化本质上是动态加载和调用插件包里的类,依赖ClassLoader和反射。
  2. 反射灵活但有性能代价,少量场景可接受,大量慎用
  3. Android主流用DexClassLoader加载外部dex,但实际APP通常需要处理classloader隔离、资源访问等复杂问题,实际落地比理论复杂。

安卓中的类加载机制

image.png

需要注意,这里的DexClassLoader因为是我们自己使用,所以可以传入PathClassloader为父类,如果没有传入,就是空,所以这个图的使用需要注意这点。

Class加载流程时序图

image.png

对应的源码流程

1. dalvik.system.BaseDexClassLoader#findClass

负责从当前ClassLoader(包括其sharedLibraryLoaders)和对应的DexPathList中查找目标类:

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 1. 先查找sharedLibraryLoaders
    if (sharedLibraryLoaders != null) {
        for (ClassLoader loader : sharedLibraryLoaders) {
            try {
                return loader.loadClass(name);
            } catch (ClassNotFoundException ignored) {}
        }
    }
    // 2. 查找自身的dexPath
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c != null) {
        return c;
    }
    // 3. 查找"after" sharedLibraryLoaders
    if (sharedLibraryLoadersAfter != null) {
        for (ClassLoader loader : sharedLibraryLoadersAfter) {
            try {
                return loader.loadClass(name);
            } catch (ClassNotFoundException ignored) {}
        }
    }
    // 4. 全部未找到,抛出异常
    ClassNotFoundException cnfe = new ClassNotFoundException(
            "Didn't find class "" + name + "" on path: " + pathList);
    for (Throwable t : suppressedExceptions) {
        cnfe.addSuppressed(t);
    }
    throw cnfe;
}

2. java.lang.ClassLoader#loadClass(String, boolean)

这是标准的Java类加载链,涉及双亲委托机制:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 1. 是否已加载
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            // 2. 优先交给父加载器
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 忽略,继续往下走
        }
        if (c == null) {
            // 3. 本地查找
            c = findClass(name);
        }
    }
    return c;
}

3. java.lang.ClassLoader#findLoadedClass

实际调用到JVM层检查该类是否已加载:

protected final Class<?> findLoadedClass(String name) {
    ClassLoader loader = (this == BootClassLoader.getInstance()) ? null : this;
    return VMClassLoader.findLoadedClass(loader, name);
}

最终所有的类查找,都落到底层的java.lang.VMClassLoader#findLoadedClass实现,由JVM返回是否已加载该类。


插件化的本质——合并dexElements数组

一个APP所有可用的Class都在dexElements数组里。
插件化的核心就是:将宿主和插件的dexElements合并,替换到宿主ClassLoader里
这样查找Class时,插件代码就“融入”了宿主的查找范围,APP无需重启即可调用插件功能。

插件化的核心步骤

  1. 获取宿主的dexElements;
  2. 获取插件dexElements;
  3. 合并两个dexElements(注意类型是Element,不是Object);
  4. 将新数组赋值给宿主的dexElements字段。

代码实现示例

public class LoadUtil {

    private static final String apkPath = "/sdcard/plugin-debug.apk";

    public static void loadClass(Context context) {
        try {
            // 反射获取BaseDexClassLoader的pathList
            Class<?> baseDexClassLoaderClazz = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoaderClazz.getDeclaredField("pathList");
            pathListField.setAccessible(true);

            // 反射获取DexPathList的dexElements
            Class<?> dexPathListClazz = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClazz.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            // 1. 获取宿主ClassLoader及其dexElements
            ClassLoader hostClassLoader = context.getClassLoader();
            Object hostPathList = pathListField.get(hostClassLoader);
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

            // 2. 获取插件ClassLoader及其dexElements
            ClassLoader pluginClassLoader = new DexClassLoader(
                    apkPath,
                    context.getCacheDir().getAbsolutePath(),
                    null,
                    hostClassLoader
            );
            Object pluginPathList = pathListField.get(pluginClassLoader);
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

            // 3. 合并两个dexElements
            Object[] newDexElements = (Object[]) Array.newInstance(
                    hostDexElements.getClass().getComponentType(),
                    hostDexElements.length + pluginDexElements.length
            );
            System.arraycopy(hostDexElements, 0, newDexElements, 0, hostDexElements.length);
            System.arraycopy(pluginDexElements, 0, newDexElements, hostDexElements.length, pluginDexElements.length);

            // 4. 替换宿主ClassLoader的dexElements字段
            dexElementsField.set(hostPathList, newDexElements);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意点:

  • 合并的数组类型必须是Element[],不是Object[]。
  • 反射访问私有字段有兼容性和安全风险,Android 9+对私有API反射有更严格限制,生产环境要考虑兼容和安全。
  • 插件ClassLoader可以用PathClassLoader或DexClassLoader,Android 8以后两者实现基本无异。

总结

  • 插件化的核心是ClassLoader+反射+合并dexElements数组,让插件的Class“注入”到APP的类加载体系。
  • 只要宿主的ClassLoader能查到插件的Class,任何地方都可以直接Class.forName()或反射调用插件功能。
  • 这种做法带来极大的灵活性,但也有兼容性、安全性和热插拔风险(如多版本冲突、API变化等),不是所有项目都适用

如何启动插件的四大组件——以Activity为例

上一节讲的是普通类的加载,但像Activity这样的四大组件,必须在宿主的Manifest中注册,否则启动时会被AMS(ActivityManagerService)拦截、校验,直接报错。
所以要实现插件Activity的动态启动,必须绕过Manifest校验机制,这就涉及到Hook和动态代理。


Activity启动流程

image.png
image.png

如上图所示,我们需要找到Hook点来做Intent替换

  • 进入AMS之前,把插件Activity替换成宿主Manifest中注册过的ProxyActivity(占位Activity);
  • AMS校验通过、从AMS出来后,再把ProxyActivity换回插件Activity,达到真正启动插件Activity的目的。

Hook的原理与流程

image.png

要解决的问题: 插件Activity没有在Manifest注册,AMS校验时无法通过。
核心思路:

  1. 先用ProxyActivity(Manifest中注册的占位Activity)骗过AMS校验;
  2. 启动后,在应用进程把ProxyActivity替换成真实的插件Activity。

技术手段:

  • 动态代理(Java Proxy)、反射
  • Hook IActivityManager/ActivityManagerService
  • Hook Handler.Callback(ActivityThread主线程消息)

Hook建议:

  • 优先Hook静态变量或单例对象,减少多实例问题;
  • 优先Hook public方法,private方法兼容性较差。

具体Hook流程

步骤一:启动插件Activity(替换Intent,骗过AMS)

通常启动插件Activity会这样写(包名不同于宿主):

Intent intent = new Intent();
intent.setComponent(new ComponentName("com.enjoy.plugin", "com.enjoy.plugin.MainActivity"));
startActivity(intent);

最终会走到Activity.startActivityForResult和Instrumentation的execStartActivity

public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
    // ...省略...
    mInstrumentation.execStartActivity(
        this, mMainThread.getApplicationThread(), mToken, this,
        intent, requestCode, options);
    // ...省略...
}

execStartActivity会通过**ActivityTaskManager.getService().startActivity(...)**真正发起启动请求,这里就是第一个Hook点。

动态代理Hook IActivityManager/IActivityTaskManager:

public static void hookAMS() {
    try {
        // 1. 获取系统的IActivityManager/IActivityTaskManager单例
        Field singletonField;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            Class<?> clazz = Class.forName("android.app.ActivityManagerNative");
            singletonField = clazz.getDeclaredField("gDefault");
        } else {
            Class<?> clazz = Class.forName("android.app.ActivityManager");
            singletonField = clazz.getDeclaredField("IActivityManagerSingleton");
        }
        singletonField.setAccessible(true);
        Object singleton = singletonField.get(null);

        // 2. 拿到mInstance
        Class<?> singletonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);
        final Object mInstance = mInstanceField.get(singleton);

        // 3. 创建动态代理,拦截startActivity
        Class<?> iActivityManager = Class.forName("android.app.IActivityManager");
        Object proxyInstance = Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{iActivityManager},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 拦截startActivity
                        if ("startActivity".equals(method.getName())) {
                            int index = -1;
                            for (int i = 0; i < args.length; i++) {
                                if (args[i] instanceof Intent) {
                                    index = i;
                                    break;
                                }
                            }
                            Intent intent = (Intent) args[index];
                            Intent proxyIntent = new Intent();
                            // 替换成宿主的ProxyActivity
                            proxyIntent.setClassName("com.enjoy.leo_plugin", "com.enjoy.leo_plugin.ProxyActivity");
                            proxyIntent.putExtra(TARGET_INTENT, intent);
                            args[index] = proxyIntent;
                        }
                        // 保持原有逻辑
                        return method.invoke(mInstance, args);
                    }
                });

        // 4. 用代理对象替换系统IActivityManager
        mInstanceField.set(singleton, proxyInstance);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

核心要点:

  • 通过动态代理,拦截startActivity,替换Intent为ProxyActivity,并把目标Intent作为extra携带。

步骤二:启动流程出来后再换回插件Activity

当APP主线程收到启动消息后,要把ProxyActivity“换回”真正的插件Activity。

  1. ActivityThread.mH Handler分发消息时,有两个hook点Handler.CallbackhandleMessage()
  2. 利用Callback,在分发消息前,修改msg.obj中的intent,将ProxyIntent替换成目标Intent。

image.png

public static void hookHandler() {
    try {
        // 1. 获取ActivityThread实例
        Class<?> clazz = Class.forName("android.app.ActivityThread");
        Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
        activityThreadField.setAccessible(true);
        Object activityThread = activityThreadField.get(null);

        // 2. 获取mH Handler
        Field mHField = clazz.getDeclaredField("mH");
        mHField.setAccessible(true);
        final Handler mH = (Handler) mHField.get(activityThread);

        // 3. Hook Callback
        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);
        Handler.Callback callback = new Handler.Callback() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                switch (msg.what) {
                    case 100: // 旧版本LAUNCH_ACTIVITY
                        try {
                            Field intentField = msg.obj.getClass().getDeclaredField("intent");
                            intentField.setAccessible(true);
                            Intent proxyIntent = (Intent) intentField.get(msg.obj);
                            Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                            if (intent != null) {
                                intentField.set(msg.obj, intent);
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        break;
                    case 159: // 新版本EXECUTE_TRANSACTION
                        try {
                            Field mActivityCallbacksField = msg.obj.getClass().getDeclaredField("mActivityCallbacks");
                            mActivityCallbacksField.setAccessible(true);
                            List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);

                            for (int i = 0; i < mActivityCallbacks.size(); i++) {
                                if (mActivityCallbacks.get(i).getClass().getName()
                                        .equals("android.app.servertransaction.LaunchActivityItem")) {
                                    Object launchActivityItem = mActivityCallbacks.get(i);
                                    Field mIntentField = launchActivityItem.getClass().getDeclaredField("mIntent");
                                    mIntentField.setAccessible(true);
                                    Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);
                                    Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                                    if (intent != null) {
                                        mIntentField.set(launchActivityItem, intent);
                                    }
                                }
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        break;
                }
                // 必须return false,交回系统继续处理
                return false;
            }
        };
        mCallbackField.set(mH, callback);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

总结 & 前瞻性风险

  1. 本质原理:通过双重Intent替换,绕过Manifest校验,实现Activity插件化加载。

  2. 技术手段:动态代理+反射+Handler.Callback,修改系统内部变量,有一定兼容和安全风险。

  3. 兼容问题

    • Android版本变更频繁,API私有化、反射受限,Android 9+严格限制部分反射能力,可能需要配合隐藏API解锁/自定义ROM/厂商机型适配。
    • Android 10+的分区存储、安全机制进一步收紧,未来官方对插件化、热修复等方案并不鼓励,有下架或审核风险。
  4. 建议:如无刚需,尽量用官方支持的动态Feature模块(Dynamic Feature Module, App Bundle)等新方案,插件化只是权宜之计,长期看前景并不乐观。

插件化——如何加载插件资源

资源系统比类系统更复杂。普通类可以靠ClassLoader hack进来,但资源(layout、drawable、string等)绑定的是apk与asset,而且资源id(如0x7f07004d)是各自独立的,容易冲突。


资源加载时序

image.png

核心源码流程

Activity启动时,底层通过一系列调用链最终为每个Activity创建了Resources对象

  • ActivityThread#performLaunchActivity

    • 创建ContextImpl

    • 进入ResourcesManager#createBaseTokenResources

      • 走到createResourcesForActivity
      • 最终调用createResourcesImpl
      • 这里通过createAssetManager构造AssetManager(关键点
      • AssetManager保存了所有可用的apk资源路径

源码流程关键段略(详见原文)。


插件化加载资源的原理

原理总结

Android的Resources本质靠AssetManager维护apk资源的路径。要让宿主能访问插件资源,只需让AssetManager加载插件apk的asset path

常用实现方式

方式一:合并资源路径

直接用反射给宿主的AssetManager添加插件apk路径:

public static Resources loadResource(Context context) {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
        addAssetPathMethod.invoke(assetManager, apkPath); // 插件apk路径
        Resources hostRes = context.getResources();
        return new Resources(assetManager, hostRes.getDisplayMetrics(), hostRes.getConfiguration());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
宿主如何切换Resource?

覆写BaseActivity中的getResources():

public class BaseActivity extends AppCompatActivity {
    @Override
    public Resources getResources() {
        Resources resources = LoadUtil.getResources(getApplication());
        return resources == null ? super.getResources() : resources;
    }
}

注意:传Activity自身context容易死循环或栈溢出,必须用Application级context。


方式二:独立Context和Resources

为插件Activity构造独立的Context和Resource对象,解决资源id冲突:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Resources resources = LoadUtil.getResources(getApplication());
    // 构造新的ContextThemeWrapper
    mContext = new ContextThemeWrapper(getBaseContext(), 0);
    try {
        Field mResourcesField = mContext.getClass().getDeclaredField("mResources");
        mResourcesField.setAccessible(true);
        mResourcesField.set(mContext, resources);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

布局加载时用这个Context:

View view = LayoutInflater.from(mContext).inflate(R.layout.activity_main, null);
setContentView(view);

资源id冲突与彻底解决方案

为什么会冲突?

  • 每个apk的资源id编译时唯一,如0x7f07004d,但不同apk同名资源id实际值不一样。
  • 运行时如AppCompat等第三方库只认当前ClassLoader和id,插件和宿主的同名资源id不一致会直接崩溃或找不到资源。

冲突根源

  • 资源id三段式:0x7f0a000a

    • 7f: 包id
    • 0a: 类型id
    • 000a: 资源在类型内的序号
  • 插件和宿主各自独立分配,id一定会撞(即使同名同内容)。

解决办法

  • 修改aapt源码,让不同apk分配不同的包id(如7e~ff)。
  • 通过定制aapt/aapt2,或打包工具支持,为插件分配全局唯一资源id。
  • 这样即便资源名一致,id不会冲突。

image.png
image.png

现实中,大部分插件化框架都难以100%避免资源冲突,aapt方案最彻底,但成本极高


插件资源隔离的实用建议

  1. 插件BaseActivity统一切换Resource和Context,避免全局污染。
  2. 尽量避免和宿主/其他插件资源重名、同id。
  3. 复杂场景下,直接用独立进程/动态feature模块等官方支持的方式更稳妥。
  4. 如果要搞“热修复”或大规模插件化,务必验证资源动态加载方案的兼容性,不能只靠demo级实现。

前瞻性风险与趋势

  • Android对反射和非官方API限制越来越严格,插件资源hack未来兼容风险只增不减。
  • Play/App Bundle分包/动态feature已经是Google主推的热更新/扩展方式。
  • 插件化资源id冲突、主题丢失、混淆、性能等问题始终很难彻底消除。
  • 建议:新项目优先选择官方的Dynamic Feature/AAB,不建议再自造插件化资源系统。

学后检测

一、单选题(每题4分)

  1. 关于插件化和组件化,下列哪项描述最准确?

    A. 组件化和插件化都支持APP运行时动态加载功能模块
    B. 插件化可以实现APP无需整体升级,按需动态加载功能
    C. 组件化只支持热更新,不能分模块开发
    D. 插件化和组件化对包体积的影响相同

    答案:B

    解析:
    插件化的最大特点是运行时动态下载/加载模块并独立升级,组件化则是整体打包,不能动态下发模块。


  1. 下列哪一个ClassLoader专用于加载外部Dex、APK或Jar包?

    A. BootClassLoader
    B. PathClassLoader
    C. DexClassLoader
    D. SystemClassLoader

    答案:C

    解析:
    DexClassLoader可以加载来自存储路径的dex、apk、jar,适合插件化场景。


  1. Android 8.0(Oreo)以后,DexClassLoader和PathClassLoader的区别是?

    A. DexClassLoader被废弃
    B. 实现合并,底层实际只用PathClassLoader
    C. 只能用DexClassLoader加载插件
    D. 二者无任何关联

    答案:B

    解析:
    8.0以后实现合并,DexClassLoader只是对PathClassLoader的兼容壳。


  1. Android资源id如0x7f07004d,0x7f表示什么?

    A. 资源类型id
    B. 包id(package id)
    C. 资源名
    D. 资源在类型中的索引

    答案:B

    解析:
    0x7f是包id,不同apk理论上应分配不同包id,防止冲突。


二、多选题(每题5分)

  1. 关于Android插件化类加载,以下哪些说法正确?

    A. 需要操作ClassLoader和反射
    B. 插件类注入宿主后,APP无需重启即可用新功能
    C. 合并dexElements可让宿主和插件共用代码
    D. 反射操作没有任何兼容性问题

    答案:A、B、C

    解析:
    D错误,反射操作受Android版本、厂商ROM影响,有兼容性和安全风险。


  1. 以下哪些措施有助于缓解资源id冲突?

    A. 插件和宿主的资源命名统一前缀
    B. 修改aapt为每个插件分配不同包id
    C. 插件和宿主共用一个Resources对象
    D. 插件Activity用独立的Context和Resources

    答案:A、B、D

    解析:
    C做法反而加大冲突风险,插件/宿主应资源隔离。


三、判断题(每题2分)

  1. 插件化方案在所有Android版本都100%兼容、可上线无风险。( )

    答案:错

    解析:
    反射、私有API越来越受限,插件化方案有较多兼容与审核风险。


  1. Resource id冲突只要资源命名不同就能完全避免。( )

    答案:错

    解析:
    id是编译分配,同名资源会冲突,不同名但包id、类型id分配不当也会冲突。


  1. 插件化场景下,资源最好通过独立的AssetManager和Context隔离加载。( )

    答案:对

    解析:
    这样能大大减少宿主、插件资源混用带来的问题。


四、简答题(每题10分)

  1. 简述插件化与组件化的主要区别,以及各自的典型应用场景。

    答案要点:

    • 插件化:支持APP运行时动态加载/卸载/升级功能模块,按需扩展;如应用市场、浏览器扩展、热更新等。
    • 组件化:主要关注开发阶段的模块拆分,编译期整体打包,运行时不可单独卸载、升级,适合大型团队协作。

    解析:
    插件化面向灵活性和动态性,组件化面向工程化和团队协作。


  1. 请简述插件和宿主资源id冲突的成因,并列举两种缓解方法。

    答案要点:

    • 成因:资源id由aapt编译自动分配,不同apk同名资源、同包id导致id冲突;AppCompat等全局静态id类无法区分。

    • 缓解方法:

      1. 插件资源命名强制前缀。
      2. 修改aapt,插件与宿主分配不同包id。
      3. 插件单独使用自己的Resources与Context,避免全局污染。

五、编程题(每题15分)

  1. 实现一个简单的插件类动态加载与调用。假设插件编译成dex,路径为/sdcard/plugin.dex,插件中有com.demo.plugin.Hello类和public static void sayHi()方法。请补全如下Java代码:

    // 补全loadAndInvoke()方法,实现插件类的动态加载和调用
    public class PluginLoader {
        public static void loadAndInvoke(Context context) {
            // TODO
        }
    }
    

    参考答案:

    public class PluginLoader {
        public static void loadAndInvoke(Context context) {
            try {
                String dexPath = "/sdcard/plugin.dex";
                String optDir = context.getCacheDir().getAbsolutePath();
                DexClassLoader dexClassLoader = new DexClassLoader(
                        dexPath, optDir, null, context.getClassLoader());
                // 加载类
                Class<?> clazz = dexClassLoader.loadClass("com.demo.plugin.Hello");
                // 调用静态方法
                Method method = clazz.getMethod("sayHi");
                method.invoke(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    解析:
    DexClassLoader负责动态加载,反射调用静态方法,无需实例化插件对象。