Android ClassLoader插件化

一、什么是插件化

在android开发中,所谓插件化,就是在android运行中,我们可以动态加载dex文件到app的进程中。这样的做的好处是app能在不升级的情况下更新有问题的dex模块。

二、如何加载一个dex插件

  1. android给我们提供了一个加载插件的工具ClassLoader。为了理解这个东西,我们先做个实验。你可以新建个android工程,并在onCreate()方法中加上如下代码:
ClassLoader classLoader = getClassLoader();
Log.d("MainActivity",classLoader.toString());

打印出的日志应该类似这样

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.debug.pluginhost-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]

这个日志告诉我们,android系统给每个app进程分配了一个PathClassLoader对象,这个对象包含了两部分信息,一部分dex文件的路径集合,也就是DexPathList(/data/app/com.debug.pluginhost-2/base.apk),一部分是nativeLibrary路径集合(/vender/lib,/system/lib)。
我们把上面的代码稍微改一下

ClassLoader classLoader = getClassLoader();
Log.d("MainActivity",classLoader.toString());
Log.d("MainActivity",classLoader.getClass().getSuperclass().toString());//BaseDexClassLoader
Log.d("MainActivity",classLoader.getClass().getSuperclass().getSuperclass().toString());//ClassLoadr

看下日志

D/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.debug.pluginhost-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
D/MainActivity: class dalvik.system.BaseDexClassLoader
D/MainActivity: class java.lang.ClassLoader

我们看下PathClassLoader的源码,非常简单

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

只能看BaseDexClassLoader的源码了,我们挑重要的看

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, 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 列表(结合开头的日志),并复写了父类的findClass方法。我们看下它的父类ClassLoader的代码,我们还是挑重要的看

public abstract class ClassLoader {
    ...
    private final ClassLoader 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) {
                long t0 = System.nanoTime();
                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.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }
   ...

}

上面的loadClass方法就是著名的双亲委派。先检查系统是否加载了这个类,如果没加载,看看自己parent是否加载了,如果parent也没加载,那么直接从自己的pathList里找。我们再把最初的代码改下,看看这个parent成员变量是个啥

ClassLoader classLoader = getClassLoader();
        Log.d("MainActivity",classLoader.toString());
        Log.d("MainActivity",classLoader.getClass().getSuperclass().toString());//BaseDexClassLoader
        Log.d("MainActivity",classLoader.getClass().getSuperclass().getSuperclass().toString());//ClassLoadr

        try {
            Field[] fields = classLoader.getClass().getSuperclass().getSuperclass().getDeclaredFields();
            for (int i=0;i < fields.length;i++){
                if(fields[i].getName().equals("parent")){
                    Field parent = fields[i];
                    parent.setAccessible(true);
                    Log.d("MainActivity",parent.get(classLoader).toString());
                    return;
                }
            }

            Log.d("MainActivity","没查到");
        }catch (Exception e){
            e.printStackTrace();
        }

看下日志

D/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.debug.pluginhost-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
D/MainActivity: class dalvik.system.BaseDexClassLoader
D/MainActivity: class java.lang.ClassLoader
D/MainActivity: java.lang.BootClassLoader@360f4a86

我们看到ClassLoader的parent成员变量是个BootClassLoader对象。顾名思义,应该是系统启动时加载类的工具,它是ClassLoader的非静态内部类,并继承自ClassLoader。它是所有ClassLoader的根部。我们结合一个例子来理解下加载dex插件的过程。

try {
    HttpDownloader downloader = new HttpDownloader();
    String url = "http://192.168.2.192/static/script/plugin.jar";
    String path = "dex";
    String name = "plugin.jar";
    int res = downloader.downloadFiles(url, path, name);
    Log.d("MainActivity","download result="+res+"");

    //上面的代码是把插件下载下来,我们主要看下面代码
    PathClassLoader pcl = (PathClassLoader)MainActivity.this.getClassLoader();
    DexClassLoader dexClassLoader = new DexClassLoader(MainActivity.this.getCacheDir()+"/dex/plugin.jar",MainActivity.this.getCacheDir()+"/dex/",null,pcl);
    Class<?>  clz = dexClassLoader.loadClass("com.debug.qclibrary.Plugin");
    Method debug = clz.getDeclaredMethod("debug",new Class[0]);
    debug.invoke(null);

}catch (Exception e){
    e.printStackTrace();
}
  • DexClassLoader就是简单的继承了BaseClassCloader
  • 初始化的时候,我们把当前app的ClassLoader对象(一个PathClassLoader对象)作为parent传递给了DexClassLoader
  • 我们调用dexClassLoader.loadClass("com.debug.qclibrary.Plugin")方法,由于BaseClassLoader没有复写这个方法,所以直接调用了ClassLoader的loadclass 方法,这个方法,我们上面贴过了。执行流程就是先检查系统是否加载了这个类,如果加载了,直接返回,如果没加载就调用parent的loadClass方法,这里的parent应该是pcl变量。
  • 调用pcl的loadClass方法时,依然是先检测系统是否加载,如果没加载就用parent的loadClass方法,注意,我们上面通过实验得知,pcl的parent是个BootClassLoader对象,我们看下 BootClassLoader的loadClass方法的实现
    @Override
    protected Class<?> loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            clazz = findClass(className);
        }

        return clazz;
    }

如果pcl的parent的loadClass方法也没返回非空,那么直接调用pcl的findClass方法,也就是去"/data/app/com.debug.pluginhost-2/base.apk"找目标class,如果还没找到,那么让dexClassload去自己的dexPathList里找,如果找到就返回,找不到就抛出异常。

三、总结双亲委派

  1. 如果我们通过DexClassLoader动态加载dex插件,那么这个任务都是优先让自己的parent去加载,只有parent没正确返回时才尝试自己从自己的dexPathList去加载。
  2. 为什么要用双亲委派?
  • 防止已经加载的类再次加载。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容