Android应用程序插件化研究之DexClassLoader
来自: http://www.liuguangli.win/archives/366
最近在研究Android应用的插件化开发,看了好几个相关的开源项目。插件化都是在解决以下几个问题:
- 如何把插件apk中的代码和资源加载到当前虚拟机。
- 如何把插件apk中的四大组件注册到进程中。
- 如何防止插件apk中的资源和宿主apk中的资源引用冲突。
就这几个问题,我开始研究插件化开发实现的相关技术,本篇文章主要讲第一点:如何加载另一个apk中的class。我们要把一个包含class文件的jar加载到java虚拟机中,需要使用ClassLoader这个类。Android的编译系统中对class文件进行了进一步的处理:最后变成 .dex文件,.dex文件包含在apk中,google提供了一个类来加载.dex文件,这个类就是DexClassLoader,它继承自ClassLoader。本篇的重点是写一个例子来说明DexClassLoader的用法。先来熟悉下DexClassLoader。
DexClassLoader介绍
DexClassLoader是一个类加载器,可以用来从.jar和.apk文件中加载class。可以用来加载执行没用和应用程序一起安装的那部分代码。
构造函数:DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
dexPath:被解压的apk路径,不能为空。
optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。
libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
parent:父亲加载器,一般为ClassLoader.getSystemClassLoader()。
创建一个插件apk工程module:apkbeloaded
我把插件的包名命名为:com.dexclassdemo.liuguangli.apkbeloaded,
我们创建两个类:Registry和ClassToBeImported
Registry:
package com.dexclassdemo.liuguangli.apkbeloaded;  import java.util.ArrayList;  /**   * Created by liuguangli on 16/2/13.   */  public class Registry {      public static ArrayList<Class<?>> _classes = new ArrayList<Class<?>>();      static{          _classes.add(ClassToBeImported.class);          //more classes here      }  }    这个类中个只有一个成员变量_classes,集合引用ClassToBeImported.class。
ClassToBeImported:
package com.dexclassdemo.liuguangli.apkbeloaded;  import android.util.Log;  /**   * Created by liuguangli on 16/2/13.   */  public class ClassToBeImported {      public static ClassLoader method(){          Log.v("ClassToBeImported", "called method of class " + ClassToBeImported.class.getName());          return ClassToBeImported.class.getClassLoader();      }  }    我们会演示这个方法如何在宿主中被调用的,并且我们可以跟踪这个类的类加载器。我们编译这个工程得到的ask文件为:apkbeloaded-debug.apk。
创建一个宿主apk工程
我把宿主包名命名为:dexclassloaderdemo。我们在assets目录下创建一个目录plugins,然后把apkbeloaded-debug.apk拷贝到该目录下。在MainActivity中创建一个方法为:loadDexClasses
public  void loadDexClassses() {          if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {              Log.v("loadDexClassses", "LoadDexClasses is only available for ICS or up");          }          String paths[] = null;          try {              paths = getAssets().list("plugins");          } catch (IOException e) {              e.printStackTrace();          }          if (paths == null) {              Log.v("loadlDexClasses", "There was no " + paths);              return;          }          Log.v("loadDexClasses", "Dex Preparing to loadDexClasses!");          for (String file : paths) {            //接下来完善          }      }    我们从getAssets的plugins中读取文件列表,然后打算逐个加载(当然本例子中只有一个)。注意,我们不能直接从asserts目录中加载apk(asserts不是一个文件系统目录,只是apk的一个资源目录),先来写一个从assert目录中拷贝apk到一个文件目录下:copyAssetsApkToFile(本例中拷贝到sdcard,实际开发中最好拷贝到私有目录,防止被注入)。
public void copyAssetsApkToFile(Context context, String src, String des) {          try {              InputStream is = context.getAssets().open(src);              FileOutputStream fos = new FileOutputStream(new File(des));              byte[] buffer = new byte[1024];              while (true) {                  int len = is.read(buffer);                  if (len == -1) {                      break;                  }                  fos.write(buffer, 0, len);              }              is.close();              fos.close();          } catch (Exception e) {              e.printStackTrace();          }      }    下面我们来完善核心代码:
for (String file : paths) {              File pluginDir  = Environment.getExternalStorageDirectory();              pluginDir.mkdirs();              String desDir = pluginDir.getAbsolutePath();              String des = desDir + "/" + "apkbeloaded-debug.apk";              File desFile = new File(des);              File optimizedDirectory = this.getDir(OPT_DIR, Context.MODE_PRIVATE);              if (!desFile.exists()){                  copyAssetsApkToFile(this, "plugins/"+file, des);              }              final DexClassLoader classloader = new DexClassLoader(                      des, optimizedDirectory.getAbsolutePath(),                      "data/local/tmp/natives/",                      ClassLoader.getSystemClassLoader());                Log.v("loadDexClasses", "Searching for class : "                      + "com.registry.Registry");              try {                  Class<?> classToLoad = (Class<?>)   classloader.loadClass("com.dexclassdemo.liuguangli.apkbeloaded.Registry");                    Field classesField = classToLoad.getDeclaredField("_classes");                    ArrayList<Class<?>> classes = null;                    classes = (ArrayList<Class<?>>) classesField.get(null);                  for (Class<?> cls : classes) {                      Log.v("loadDexClasses", "Class loaded " + cls.getName());                      if (cls.getName().contains("ClassToBeImported")) {                          Method m = cls.getMethod("method");                          ClassLoader xb = (ClassLoader) m.invoke(null);                          if (xb.equals(ClassLoader.getSystemClassLoader()))                              Log.v("loadDexClasses", "Same ClassLoader");                          else                              Log.v("loadDexClasses", "Different ClassLoader");                          Log.v("loadDexClasses", xb.toString());                      }                    }              } catch (IllegalAccessException e) {                  e.printStackTrace();              } catch (ClassNotFoundException e) {                  e.printStackTrace();              } catch (NoSuchFieldException e) {                  e.printStackTrace();              } catch (NoSuchMethodException e) {                  e.printStackTrace();              } catch (InvocationTargetException e) {                  e.printStackTrace();              }              }    Demo源码
</div>