我们在上一篇文章《Android插件化原理和实践 (二) 之 加载插件中的类代码》中埋下了一个悬念,那就是通过构造一个DexClassLoader对象后使用反射只能反射出普通的类,而不能正常使用四大组件,因为会报出异常。今天我们就来解开这个悬念和提出解决方法。
1 揭开悬念
还记得《Android应用程序启动详解(二)之Application和Activity的启动过程》中有介绍了Activity的启动过程吗?在ActivityThread.java中有下面的代码:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent){
……
Activity activity = null;
try {
// 关键代码
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
……
}
……
}
在上述关键代码地方,通过获取一个ClassLoader来作为参数,然后创建出一个Activity实例,而这个ClassLoader对象实质上是一个PathClassLoader,因为通过跟踪源码可以发现此对象的创建地方在ClassLoader.java中,如代码:
/**
* Encapsulates the set of parallel capable loader types.
*/
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
// String[] paths = classPath.split(":");
// URL[] urls = new URL[paths.length];
// for (int i = 0; i < paths.length; i++) {
// try {
// urls[i] = new URL("file://" + paths[i]);
// }
// catch (Exception ex) {
// ex.printStackTrace();
// }
// }
//
// return new java.net.URLClassLoader(urls, null);
// TODO Make this a java.net.URLClassLoader once we have those?
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
这里的ClassLoader对象就是PathClassLoader对象,所以我们在App的Activity中,通过getClassLoader获取到的是PathClassLoader。就是意味着,Activity的创建只能在PathClassLoader中存在的类,其实四大组件的创建都是一样。也就是说组件类必须是要定义在宿主中才可以正常创建出来。我们在上一篇文章的最后提出,在通过DexClassLoader来加载起插件后,再使用startService来启动插件的一个服务,那么当然就会报出异常。
2 ClassLoader相关类源码分析
其实解决方法说起来是很简单的,就是要把插件的ClassLoader对应的dex文件塞入到宿主的ClassLoader中去就可以了。至少怎样塞法?那就要先来看看PathClassLoader和DexClassLoader 它们的父类BaseDexClassLoader和BaseDexClassLoader的父类ClassLoader的源码了:
ClassLoader.java
public abstract class ClassLoader {
private final ClassLoader parent;
……
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
……
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
……
}
我们都知道Android中Class的加载是执行的ClassLoader的loadClass方法。loadClass方法中可以看到,在开始时,会先检查类是否被加载过,如果没有加载过,则会优先委派它的父类去加载类,如果最后没有哪个父类加载过,那就自己通过findClass方法来加载这个类。这个就是双亲委派机制。再继续来看BaseDexClassLoader的代码,因为findClass的实现在它这里。
BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
……
}
BaseDexClassLoader的构造函数中创建了一个DexPathList类型的对象pathList,然后在findClass的时候,实质上是调用了pathList的findClass方法,接下来看看DexPathList的源码:
DexPathList.java
/*package*/ final class DexPathList {
private final Element[] dexElements;
/**
* Constructs an instance.
*
* @param definingContext the context in which any as-yet unresolved
* classes should be defined
* @param dexPath list of dex/resource path elements, separated by
* {@code File.pathSeparator}
* @param libraryPath list of native library directory path elements,
* separated by {@code File.pathSeparator}
* @param optimizedDirectory directory where optimized {@code .dex} files
* should be found and written to, or {@code null} to use the default
* system directory for same
*/
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException("optimizedDirectory doesn't exist: " + optimizedDirectory);
}
if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) {
throw new IllegalArgumentException("optimizedDirectory not readable/writable: " + optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if the
* zip file turns out to be resource-only (that is, no classes.dex file in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
……
}
DexPathList这个类非常重要,其中关键就在于以上三个方法。首先看它的构造函数,构造函数的第4个参数就是前面所说的PathClassLoader和DexClassLoader的区别,我们来看一下它的注释翻译,意思大概是:接收dex文件路径,若为空,那么使用系统默认路径,所以说PathClassLoader传空就到默认目录/data/dalvik-cache下去加载dex,因为我们的应用已经安装并优化了,优化后的dex存在于/data/dalvik-cache目录下。接着来看看构造函数后面,那里通过makeDexElements方法获取一个Element[]的数组对象dexElements。
再继续来看下makeDexElements方法,该方法是加载了dex文件,并创建了一个Element[]的数组对象elements来保存dex文件的相关信息。
最后看看findClass方法,它就是BaseClassLoader的findClass方法调用了DexPathList的findClass方法,它逻辑很简单,就是遍历dexElements数组,然后从数组每个对象中去查找目标类,若找到就立即返回并停止遍历。
3 解决方案
看完相关关键源代码后,回归正传,我们其实要做的事情,就是要把插件的dex塞入到宿主的deElements数组中就可以了。所以这里我们使用了反射,其步骤如下:
- 根据宿主的ClassLoader,获取宿主的dexElements数组,就是要反射出BaseDexClassLoader的DexPathList对象pathList,然后再反射出pathList里头的dexElements数组
- 根据插件的apk文件,反射出一个Element类型对象,也就是插件的dex
- 把插件的dex和宿主的dexElements合并成一个新的dex数组,替换宿主原有的dexElements数组
上述步骤通过代码实现如下:
private void loadPlugin(Context context, String apkName)
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
ClassLoader pathClassLoaderClass = context.getClassLoader();
// 获取 PathClassLoader(BaseDexClassLoader) 的 DexPathList 对象变量 pathList
Class baseDexClassLoaderClass = BaseDexClassLoader.class;
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObj = pathListField.get(pathClassLoaderClass);
// 获取 DexPathList 的 Element[] 对象变量 dexElements
Class dexPathListClass = pathListObj.getClass();
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] dexElementListObj = (Object[])dexElementsField.get(pathListObj);
// 获得 Element 类型
Class<?> elementClass = dexElementListObj.getClass().getComponentType();
// 创建一个新的Element[], 将用于替换原始的数组
Object[] newElementListObj = (Object[]) Array.newInstance(elementClass, dexElementListObj.length + 1);
// 构造插件的Element,构造函数参数:(File file, boolean isDirectory, File zip, DexFile dexFile)
File apkFile = context.getFileStreamPath(apkName);
File optDexFile = context.getFileStreamPath(apkName.replace(".apk", ".dex"));
Class[] paramClass = {File.class, boolean.class, File.class, DexFile.class};
Object[] paramValue = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Constructor elementCtor = elementClass.getDeclaredConstructor(paramClass);
elementCtor.setAccessible(true);
Object pluginElementObj = elementCtor.newInstance(paramValue);
Object[] pluginElementListObj = new Object[] { pluginElementObj };
// 把原来 PathClassLoader 中的 elements 复制进去新的Element[]中
System.arraycopy(dexElementListObj, 0, newElementListObj, 0, dexElementListObj.length);
// 把插件的 element 复制进去新的 Element[] 中
System.arraycopy(pluginElementListObj, 0, newElementListObj, dexElementListObj.length, pluginElementListObj.length);
// 替换原来 PathClassLoader 中的 dexElements 值
Field field = pathListObj.getClass().getDeclaredField("dexElements");
field.setAccessible(true);
field.set(pathListObj, newElementListObj);
}
将上面方法替换到上一遍文章Demo的MainActivity.java中的同名方法和修改调用处不用接收返回值,接着把插件中的AndroidMainifest.xml关于要调用的Service的声明复制到宿主中的AndroidMainifest.xml中并补充完整包名,最后修改onDo方法中的调用代码就大功造成,MainActivity.java代码如下:
public class MainActivity extends Activity {
private final static String sApkName = "Plugin-debug.apk";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
simulationDownload(this, sApkName);
try {
loadPlugin(this, sApkName);
} catch (Exception e) {
e.printStackTrace();
}
onDo();
}
/**
* 加载插件
* @param context
* @param apkName
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IOException
* @throws InvocationTargetException
* @throws InstantiationException
* @throws NoSuchFieldException
*/
private void loadPlugin(Context context, String apkName)
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
……
}
/**
* 执行插件代码 和 启动插件中的服务
*/
private void onDo() {
try {
Class mLoadClassBean = Class.forName("com.zyx.plugin.TestBean");
Object testBeanObject = mLoadClassBean.newInstance();
Method getNameMethod = mLoadClassBean.getMethod("getName");
getNameMethod.setAccessible(true);
String name = (String) getNameMethod.invoke(testBeanObject);
Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
Intent intent = new Intent();
String serviceName = "com.zyx.plugin.TestService";
intent.setClassName(MainActivity.this, serviceName);
startService(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 模拟下载,实际上是将Assets中的插件apk文件复制到/data/data/files 目录下
* @param context
* @param sourceName
*/
private void simulationDownload(Context context, String sourceName) {
……
}
}
此时运行App后,依然会弹出一个Toast,内容就是TestBean中的mName值,而且还会正常启动Service。好了,到这里就介绍完了宿主是如何加到插件中的代码了,其实反过来,插件要使用宿主中的代码是一样的,只要在保证插件加载完成后,通过反射调用宿主的类的可以了,这里不作过多的演示了,读者可以自己去尝试。
至于Demo中为什么要用Service来验证,是因为Service不像Activity那样,Service在启动后不需要加载任何资源。上述Demo仅仅是解决了宿主加载插件的问题,而关于资源的加载,我们留到下一遍文章中来详细介绍。
顺便一提,其实这种合并dex方案也可应用于热修复。当补丁的dex和宿主dex合并后,它们存在了相同的类和方法,但位于Elements数组前面的dex中的类和方法在遍历过程中优先执行并跳出,那么后面原来旧的就不会生效。