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 里 ClassLoaderdefineClass() 方法直接抛出 UnsupportedOperationException 异常,必须借助 DexClassLoaderPathClassLoader

  • DexClassLoaderPathClassLoader 都遵循双亲委托机制,因为只重写了 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 查询到插件的 packageNameApplicationInfo:

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 包名的情形,可以直接通过 PackageManagergetResourcesForApplication() 方法通过包名得到 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);
}