Android插件化原理和实践 (三) 之 加载插件中的组件代码

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lyz_zyx/article/details/84777806

我们在上一篇文章《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对象,所以我们AppActivity中,通过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);
    }
    ……
}

我们都知道AndroidClass的加载是执行的ClassLoaderloadClass方法。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数组中就可以了。所以这里我们使用了反射,其步骤如下:

  1. 根据宿主的ClassLoader,获取宿主的dexElements数组,就是要反射出BaseDexClassLoader的DexPathList对象pathList,然后再反射出pathList里头的dexElements数组
  2. 根据插件的apk文件,反射出一个Element类型对象,也就是插件的dex
  3. 把插件的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中的类和方法在遍历过程中优先执行并跳出,那么后面原来旧的就会生效。

 

点击下载Demo完整代码

 

 

 

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/84777806
今日推荐