插件化的APP可以根据上线后用户所需,下载所需要的组件。组件化是全部打包。需要理解:组件化是一个整体,无论哪个模块有问题,或者需要更新,都必须全部升级,插件化则不同,只需重新下载对应的组件就行了。
插件化与组件化的本质区别
- 插件化:APP可以根据用户上线后的实际需求,动态下载并加载所需功能组件,做到按需扩展、更新,灵活性高。
- 组件化:所有功能模块(组件)在打包时整体编译进APK,无法按需拆卸或独立升级。任何模块有更新或Bug,都需要整体打包和发布,代价高、灵活性差。
插件化的核心——类的加载流程
首先要理解Java中的类加载流程:
插件化本质上就是处理插件包里的类文件如何动态加载进宿主APP。这就涉及到反射、ClassLoader的相关机制。
反射的基本知识
插件化中,类加载后往往通过反射调用方法,下图梳理了反射的基础流程:
反射的性能问题
反射虽然灵活,但性能开销明显,主要包括:
- 产生大量临时对象,频繁GC;
- 访问权限检查(如private、public);
- 生成未优化字节码,执行效率低;
- 自动装箱/拆箱频繁;
- 小量场景下影响不大(通常反射调用次数未超千级不会成为瓶颈)。
安卓中的ClassLoader体系
类的加载主要依赖ClassLoader,Android常见的有三种:
- BootClassLoader:加载Android Framework层的类(系统API)。
- PathClassLoader:加载APP和Google官方库的类(如AppCompatActivity)。
- DexClassLoader:用于加载外部存储/自定义路径下的dex文件或apk、jar。
注意:Android 8.0(Oreo)之后DexClassLoader和PathClassLoader已经合并实现,实际用的是PathClassLoader,但API和用法都保留了DexClassLoader,主要是为了兼容性和扩展。
它们的继承关系如下:
- 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,否则会找不到插件类。
- 反射调用时需确保方法签名匹配。
小结
- 插件化本质上是动态加载和调用插件包里的类,依赖ClassLoader和反射。
- 反射灵活但有性能代价,少量场景可接受,大量慎用。
- Android主流用DexClassLoader加载外部dex,但实际APP通常需要处理classloader隔离、资源访问等复杂问题,实际落地比理论复杂。
安卓中的类加载机制
需要注意,这里的DexClassLoader因为是我们自己使用,所以可以传入PathClassloader为父类,如果没有传入,就是空,所以这个图的使用需要注意这点。
Class加载流程时序图
对应的源码流程
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无需重启即可调用插件功能。
插件化的核心步骤
- 获取宿主的dexElements;
- 获取插件dexElements;
- 合并两个dexElements(注意类型是Element,不是Object);
- 将新数组赋值给宿主的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启动流程
如上图所示,我们需要找到Hook点来做Intent替换:
- 进入AMS之前,把插件Activity替换成宿主Manifest中注册过的ProxyActivity(占位Activity);
- AMS校验通过、从AMS出来后,再把ProxyActivity换回插件Activity,达到真正启动插件Activity的目的。
Hook的原理与流程
要解决的问题: 插件Activity没有在Manifest注册,AMS校验时无法通过。
核心思路:
- 先用ProxyActivity(Manifest中注册的占位Activity)骗过AMS校验;
- 启动后,在应用进程把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。
- ActivityThread.mH Handler分发消息时,有两个hook点:
Handler.Callback和handleMessage(); - 利用Callback,在分发消息前,修改msg.obj中的intent,将ProxyIntent替换成目标Intent。
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();
}
}
总结 & 前瞻性风险
-
本质原理:通过双重Intent替换,绕过Manifest校验,实现Activity插件化加载。
-
技术手段:动态代理+反射+Handler.Callback,修改系统内部变量,有一定兼容和安全风险。
-
兼容问题:
- Android版本变更频繁,API私有化、反射受限,Android 9+严格限制部分反射能力,可能需要配合隐藏API解锁/自定义ROM/厂商机型适配。
- Android 10+的分区存储、安全机制进一步收紧,未来官方对插件化、热修复等方案并不鼓励,有下架或审核风险。
-
建议:如无刚需,尽量用官方支持的动态Feature模块(Dynamic Feature Module, App Bundle)等新方案,插件化只是权宜之计,长期看前景并不乐观。
插件化——如何加载插件资源
资源系统比类系统更复杂。普通类可以靠ClassLoader hack进来,但资源(layout、drawable、string等)绑定的是apk与asset,而且资源id(如0x7f07004d)是各自独立的,容易冲突。
资源加载时序
核心源码流程
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不会冲突。
现实中,大部分插件化框架都难以100%避免资源冲突,aapt方案最彻底,但成本极高。
插件资源隔离的实用建议
- 插件BaseActivity统一切换Resource和Context,避免全局污染。
- 尽量避免和宿主/其他插件资源重名、同id。
- 复杂场景下,直接用独立进程/动态feature模块等官方支持的方式更稳妥。
- 如果要搞“热修复”或大规模插件化,务必验证资源动态加载方案的兼容性,不能只靠demo级实现。
前瞻性风险与趋势
- Android对反射和非官方API限制越来越严格,插件资源hack未来兼容风险只增不减。
- Play/App Bundle分包/动态feature已经是Google主推的热更新/扩展方式。
- 插件化资源id冲突、主题丢失、混淆、性能等问题始终很难彻底消除。
- 建议:新项目优先选择官方的Dynamic Feature/AAB,不建议再自造插件化资源系统。
学后检测
一、单选题(每题4分)
-
关于插件化和组件化,下列哪项描述最准确?
A. 组件化和插件化都支持APP运行时动态加载功能模块
B. 插件化可以实现APP无需整体升级,按需动态加载功能
C. 组件化只支持热更新,不能分模块开发
D. 插件化和组件化对包体积的影响相同答案:B
解析:
插件化的最大特点是运行时动态下载/加载模块并独立升级,组件化则是整体打包,不能动态下发模块。
-
下列哪一个ClassLoader专用于加载外部Dex、APK或Jar包?
A. BootClassLoader
B. PathClassLoader
C. DexClassLoader
D. SystemClassLoader答案:C
解析:
DexClassLoader可以加载来自存储路径的dex、apk、jar,适合插件化场景。
-
Android 8.0(Oreo)以后,DexClassLoader和PathClassLoader的区别是?
A. DexClassLoader被废弃
B. 实现合并,底层实际只用PathClassLoader
C. 只能用DexClassLoader加载插件
D. 二者无任何关联答案:B
解析:
8.0以后实现合并,DexClassLoader只是对PathClassLoader的兼容壳。
-
Android资源id如0x7f07004d,0x7f表示什么?
A. 资源类型id
B. 包id(package id)
C. 资源名
D. 资源在类型中的索引答案:B
解析:
0x7f是包id,不同apk理论上应分配不同包id,防止冲突。
二、多选题(每题5分)
-
关于Android插件化类加载,以下哪些说法正确?
A. 需要操作ClassLoader和反射
B. 插件类注入宿主后,APP无需重启即可用新功能
C. 合并dexElements可让宿主和插件共用代码
D. 反射操作没有任何兼容性问题答案:A、B、C
解析:
D错误,反射操作受Android版本、厂商ROM影响,有兼容性和安全风险。
-
以下哪些措施有助于缓解资源id冲突?
A. 插件和宿主的资源命名统一前缀
B. 修改aapt为每个插件分配不同包id
C. 插件和宿主共用一个Resources对象
D. 插件Activity用独立的Context和Resources答案:A、B、D
解析:
C做法反而加大冲突风险,插件/宿主应资源隔离。
三、判断题(每题2分)
-
插件化方案在所有Android版本都100%兼容、可上线无风险。( )
答案:错
解析:
反射、私有API越来越受限,插件化方案有较多兼容与审核风险。
-
Resource id冲突只要资源命名不同就能完全避免。( )
答案:错
解析:
id是编译分配,同名资源会冲突,不同名但包id、类型id分配不当也会冲突。
-
插件化场景下,资源最好通过独立的AssetManager和Context隔离加载。( )
答案:对
解析:
这样能大大减少宿主、插件资源混用带来的问题。
四、简答题(每题10分)
-
简述插件化与组件化的主要区别,以及各自的典型应用场景。
答案要点:
- 插件化:支持APP运行时动态加载/卸载/升级功能模块,按需扩展;如应用市场、浏览器扩展、热更新等。
- 组件化:主要关注开发阶段的模块拆分,编译期整体打包,运行时不可单独卸载、升级,适合大型团队协作。
解析:
插件化面向灵活性和动态性,组件化面向工程化和团队协作。
-
请简述插件和宿主资源id冲突的成因,并列举两种缓解方法。
答案要点:
-
成因:资源id由aapt编译自动分配,不同apk同名资源、同包id导致id冲突;AppCompat等全局静态id类无法区分。
-
缓解方法:
- 插件资源命名强制前缀。
- 修改aapt,插件与宿主分配不同包id。
- 插件单独使用自己的Resources与Context,避免全局污染。
-
五、编程题(每题15分)
-
实现一个简单的插件类动态加载与调用。假设插件编译成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负责动态加载,反射调用静态方法,无需实例化插件对象。