DexClassLoader 实现 Android 插件加载
Java 中的 ClassLoader:
Java 中 ClassLoader
用于动态加载 Class
到 JVM,包含 BootstrapClassLoader
(C++ 编写,用于加载系统核心类)、ExtClassLoader
(用于加载 lib/ext/ 目录的扩展 API)、AppClassLoader
(加载 CLASSPATH
目录下的类)。
双亲委托机制:
任何自定义 ClassLoader 都必须继承
ClassLoader
抽象类,并指定其 parent 加载器,默认为BootstrapClassLoader
;任何自定义 ClassLoader 在加载一个类之前都会先委托其 parent 去加载,只有 parent 加载失败才会自己加载;
这样既可以防止重复加载,又可以排除安全隐患(防止用户替换系统核心类);
所以一般只需要重写
findClass()
方法即可(在 parent 加载失败时调用);
双亲委托机制是在
loadClass()
方法实现的,要想避开(自己验证安全性,比如 Tomcat 的WebAppClassLoader
),必须重写loadClass()
方法;
自定义 ClassLoader 用途:
在执行非置信代码前先做签名认证等;
从网络、数据库等动态加载类;
类的卸载:
只有当类的实例被回收,才会被 unload,但被
BootstrapClassLoader
加载的系统类除外;重复加载类会报异常,只能重新定义新的 ClassLoader 再次加载;
Dalvik 的 ClassLoader:
Android 里
ClassLoader
的defineClass()
方法直接抛出UnsupportedOperationException
异常,必须借助DexClassLoader
和PathClassLoader
;DexClassLoader
和PathClassLoader
都遵循双亲委托机制,因为只重写了findClass()
方法,没有重写loadClass()
方法;Dalvik 虚拟机识别的是
DexFile
而不是JarFile
;且DexFile.loadClass()
方法必须通过类加载器调用,否则无效;
利用 DexClassLoader 实现 Android 插件加载:
比如我们在主应用 HostApp 中需要调用 视频插件 VideoPlayerPlugin 中的 playVideo() 方法。
给插件加入 Intent 标识:
HostApp 要查询插件信息,只能通过 PackageManager
。这里我首先想到的是直接通过其 getPackageInfo()
方法。但是试想,可能插件有很多个,而且包名不同。所以最好还是通过在插件中定义空的 Activity
并加入 Intent
标识,然后调用 queryIntentActivities()
方法去查询插件信息:
<activity android:name=".plugin">
<intent-filter>
<action android:name="com.rincliu.videoplayerplugin"/>
</intent-filter>
</activity>
查询插件信息:
首先使用 PackageMananer
查询到插件的 packageName
和 ApplicationInfo
:
Intent intent = new Intent("com.rincliu.videoplayerplugin");
List<ResolveInfo> plugins = getPackageManager().queryIntentActivities(intent, 0);
ActivityInfo act = plugins.get(0).activityInfo;
String packageName = act.packageName;
ApplicationInfo app = act.applicationInfo;
上面是直接读取的第一条信息(plugins
要先判空),如果有很多种插件,或者有好几个版本,这样就需要继续读取插件的版本号等配置信息作进一步区分:
Resources res = pm.getResourcesForApplication(packageName);
int resId = res.getIdentifier("version", "string", packageName);
String version = res.getString(resId);
使用 DexClassLoader 调用插件:
创建 DexClassLoader
对象:
String dexSourceDir = app.sourceDir;
String dexOutputDir = getApplicationInfo().dataDir;
String dexLibDir = app.nativeLibraryDir;
ClassLoader parentLoader = this.getClass().getClassLoader();
DexClassLoader loader = new DexClassLoader(dexSourceDir, dexOutputDir, dexLibDir, parentLoader);
使用反射调用插件中的方法:
try {
Class<?> clazz = loader.loadClass(packageName + ".VideoPlayerPlugin");
//Object obj = clazz.newInstance();
Constructor<?> localConstructor = clazz.getConstructor();
Object obj = localConstructor.newInstance();
//Method method = ((Class<?>) obj).getMethod("play", String.class);
Method method = clazz.getDeclaredMethod("play", String.class);
method.invoke(obj, "/sdcard/demo.mp4");
} catch (Exception e) {}
补充:
上面是已知插件 APK 包名的情形,可以直接通过 PackageManager
的 getResourcesForApplication()
方法通过包名得到 Resources
;
如果包名未知,可通过反射先构造 AssetManager
,然后调用 Resources
构造方法:
private AssetManager createAssetManager(String apkPath) {
AssetManager am = null;
try {
am = AssetManager.class.newInstance();
if (am != null) {
Method method = am.getClass().getDeclaredMethod("addAssetPath", String.class);
method.setAccessible(true);
int result = (Integer) method.invoke(am, apkPath);
if (res == 0) {
am = null;
}
} catch (Exception e) {
am = null;
}
return am;
}
private Resources createResources(Context context, AssetManager assetmanager) {
DisplayMetrics displaymetrics = context.getResources().getDisplayMetrics();
Configuration configuration = context.getResources().getConfiguration();
return new Resources(assetmanager, displaymetrics, configuration);
}