Android 插件化分析(5)- 加载外部dex

Android 插件化能从外部下载apk并加载主要依赖于ClassLoader。

ClassLoder是一个抽象类,其中最重要的是BaseDexClassLoader及其子类PathClassLoader和DexClassLoader.

    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }

构造函数一共涉及到四个parent参数

  • dexPath :需要加载的dex的路径
  • optimizedDirectory:缓存加载的dex文件
  • libraryPath: 加载的.so文件路径
  • parent:父类加载器. ClassLoader会优先去从父类加载器去查找类,如果已加载,则直接使用,不过没加载 ,再自己加载,提高了性能。

PathClassLoader和DexClassLoader很相似,区别在于构造函数DexClassLoader 多了一个optimizedDirectory参数,而PathClassLoader会直接传递null给父类。

optimizedDirectory是用来缓存加载的dex文件的,如果为null会直接使用dex原有的路径作为缓存目录。所以DexClassLoader可以缓存指定的路径的dex,而PathClassLoader通常是用来加载Android系统类和应用的类。

大部分逻辑都在他们的父类BaseDexClassLoader里,BaseDexClassLoader里维护了一个DexPathList,顾名思义是Dex的一个集合类,里面维护了一个Element的数组,Element是表示dex或者包含dex的目录。

public class BaseDexClassLoader extends ClassLoader {
    // 需要加载的dex列表
    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 suppressedExceptions = new ArrayList();
        // 使用pathList对象查找name类
        Class c = pathList.findClass(name, suppressedExceptions);
        return c;
    }
}

对于DexPathList,通过其内部的静态方法makeDexElements创建了Element数组以及findClass去查询某个类

private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,
                                         ArrayList suppressedExceptions) {
    ArrayList elements = new ArrayList();
    // 所有从dexPath找到的文件
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();
        // 如果是文件夹,就直接将路径添加到Element中
        if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()){
            // 如果是文件且文件名以.dex结束
            if (name.endsWith(DEX_SUFFIX)) {
                try {
                    // 直接从.dex文件生成DexFile对象
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                zip = file;

                try {
                    // 从APK/JAR文件中读取dex文件
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + 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 suppressed) {
    // 遍历从dexPath查询到的dex和资源Element
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        // 如果当前的Element是dex文件元素
        if (dex != null) {
            // 使用DexFile.loadClassBinaryName加载类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

如何使用DexClassLoader加载外部dex

首先我们要创建自己的DexClassLoader ,根据其构造函数,我们需要传入四个参数

    public static ClassLoader extractPlugin(String plugin) {
        Context context = PluginApplication.sContext;
        File extractFile = null;
        try {
		    // 讲assets下的apk复制到file目录
            extractFile = context.getFileStreamPath(plugin);
            try (InputStream is = context.getAssets().open(plugin);
                 FileOutputStream fos = new FileOutputStream(extractFile)) {
                byte[] buffer = new byte[1024];
                int count;
                while ((count = is.read(buffer)) > 0) {
                    fos.write(buffer, 0, count);
                }
                fos.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        String dexPath = extractFile.getPath();
		// 将apk解压,把.so放在extractFile.getParent()目录,如果没有.so文件,直接传入null
        String libPath = unzipLibraryFile(dexPath, extractFile.getParent());
		// dex目录作为缓存dex目录
        File fileRelease = context.getDir("dex", Context.MODE_PRIVATE);
		// 创建ClassLoader
        return new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), libPath, context.getClassLoader());
    }

注意在解压缩库文件的时候,只解压缩对应平台的库文件即可,否则报错!

获取到ClassLoader只要通过反射就能调用插件里的类了

    @Test
    public void testPlugin() {
        try {
            ClassLoader classLoader = PluginLoader.extractPlugin("plugin-debug.apk");
            Class clz = classLoader.loadClass("justwen.plugin.PluginDemo");
            assertEquals("Hello World", clz.newInstance().toString());
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }


public class PluginDemo {

    @Override
    public String toString() {
        return "Hello World";
    }
}

实际上我们也可以将我们的dex 通过ClassLoader的静态方法makeDexElements去生成一个Element数组,然后通过反射,将Element数组插入默认ClassLoader的DexPathList。

如果DexPathList里包含的多个相同的类,ClassLoader会优先查找排在前面的类。所以如果 我们将修改后的类插入到原来有问题的类的前面,那么原来的类将会被新类所覆盖,这就是热更新的基础。

你可能感兴趣的:(android,插件化)