探究Android中的ClassLoader

1.什么是ClassLoader?

ClassLoader就是类加载器,作用是将编译后的class文件加载到虚拟机中,使之成为java类

2.Android中的ClassLoader

  1. BootClassLoader:主要加载Android Framework层的字节码文件
  2. PathClassLoader:主要加载已经安装到系统中的apk文件中的字节码文件
  3. DexClassLoader:主要加载没有安装到系统中的apk,jar文件中的字节码文件
  4. BaseDexClassLoader:PathClassLoader和DexClassLoader的父类,真正实现功能的代码都在这个ClassLoader中

3.ClassLoader的双亲委托

Android中的ClassLoader基本继承了Java中ClassLoader的特点,双亲委托的特点就是从java继承过来。何为双亲委托?就是同一个ClassLoader继承树,如果父ClassLoader已经加载了某个类,那么子Classloader就不会再去加载这个类。那么这种双亲委托的设计有什么好处吗?

  1. 共享性(同一个ClassLoader树对于一个类,只会加载一次,做到了一次加载,一起使用)
  2. 隔离性(一些敏感的类会被父ClassLoader先加载,子ClassLoader不会再去加载这些类,保证不会被串改)
    我们通过查看ClassLoader的源码就可以很清楚这个双亲委托的特点:
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;
    }

这个方法的逻辑是这样的:

  1. 先去判断当前的ClassLoader是否加载目标的类
  2. 再去判断父ClassLoader是否加载过目标类(如果有父ClassLoader的话)
  3. 如果都没有在去真正去加载这个类,调用的findClass这个方法(因为ClassLoader是个抽象类,所有findClass的实现在子ClassLoader中,例如PathClassLoader,DexClassLoader)

4.Android中的ClassLoader的工作流程

这节我们将通过具体ClassLoader源码的阅读去探究一下工作流程(注:这部分源码在AS中无法查看,需要到Android源码网站上去学习,网址如下:http://androidxref.com/)
DexClassLoader.java

public class DexClassLoader extends BaseDexClassLoader {

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

PathClassLoader.java

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中,那我们就来看看BaseDexClassLoader这个类。先看构造方法

 public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }

介绍上构造方法的参数含义:

  1. dexPath:需要加载含有dex文件的路径,一般是jar,apk
  2. optimizedDirectory:解压dex文件后临时存放内容的文件夹路径,一般放在内存储中,PathClassLoader不需要这个参数,所以传null
  3. librarySearchPath:包含native lib的目录路径,没有传null
  4. parent:父类加载器

构造方法中就干了一件事情,就是初始化了DexPathList对象,这个对象是用来存储一个或多个dex文件的信息,很重要,后面做详细了解。
还记得上面在将双亲委托时,ClassLoader最后去真正完成加载工作的是findClass这个方法,那我们就来看看BaseDexClassLoader的findClass方法

    @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;
    }

然后发现这个方法只是一个中转,他调用了pathList的findClass方法去完成加载任务,没想到这么快就要去看看DexPathList这个类了...

看DexPathList的源码不能直接看findClass这个方法了,要先看下他的成员变量,重点介绍下dexElements,他是一个Element数组,如下

private Element[] dexElements;

又出现一个新类,Element是什么鬼...他是DexPathList的内部类,下面是Element的成员变量

        private final File dir;
        private final boolean isDirectory;
        private final File zip;
        private final DexFile dexFile;

看到了一个dexFile,它代表的是一个dex文件,那么dexElements其实就是一个存储着多个dex文件信息的数组。在DexPathList的构造方法中有这么一行代码:

this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,                                           suppressedExceptions, definingContext);

从物理的dex文件到dexFile的转化就是通过这个方法,这个过程的代码在DexPathList的makeElements方法中,如下:

private static Element[] makeElements(List<File> files, File optimizedDirectory,
285                                          List<IOException> suppressedExceptions,
286                                          boolean ignoreDexFiles,
287                                          ClassLoader loader) {
288        Element[] elements = new Element[files.size()];
289        int elementsPos = 0;
290        /*
291         * Open all files and load the (direct or contained) dex files
292         * up front.
293         */
294        for (File file : files) {
295            File zip = null;
296            File dir = new File("");
297            DexFile dex = null;
298            String path = file.getPath();
299            String name = file.getName();
300
301            if (path.contains(zipSeparator)) {
302                String split[] = path.split(zipSeparator, 2);
303                zip = new File(split[0]);
304                dir = new File(split[1]);
305            } else if (file.isDirectory()) {
306                // We support directories for looking up resources and native libraries.
307                // Looking up resources in directories is useful for running libcore tests.
308                elements[elementsPos++] = new Element(file, true, null, null);
309            } else if (file.isFile()) {
310                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
311                    // Raw dex file (not inside a zip/jar).
312                    try {
313                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
314                    } catch (IOException suppressed) {
315                        System.logE("Unable to load dex file: " + file, suppressed);
316                        suppressedExceptions.add(suppressed);
317                    }
318                } else {
319                    zip = file;
320
321                    if (!ignoreDexFiles) {
322                        try {
323                            dex = loadDexFile(file, optimizedDirectory, loader, elements);
324                        } catch (IOException suppressed) {
325                            /*
326                             * IOException might get thrown "legitimately" by the DexFile constructor if
327                             * the zip file turns out to be resource-only (that is, no classes.dex file
328                             * in it).
329                             * Let dex == null and hang on to the exception to add to the tea-leaves for
330                             * when findClass returns null.
331                             */
332                            suppressedExceptions.add(suppressed);
333                        }
334                    }
335                }
336            } else {
337                System.logW("ClassLoader referenced unknown path: " + file);
338            }
339
340            if ((zip != null) || (dex != null)) {
341                elements[elementsPos++] = new Element(dir, false, zip, dex);
342            }
343        }
344        if (elementsPos != elements.length) {
345            elements = Arrays.copyOf(elements, elementsPos);
346        }
347        return elements;
348    }

这个方法的实现好长...简要的说,这个方法就是遍历目标文件夹中的所有文件,找出那些dex后缀的文件,转化成DexFile,存到Element数组中,并且找到那些压缩的文件,解压他们,找到他们内部的dex文件,也转化成DexFile文件,存到Element数组中。其实就一个作用,将目标文件中可以转化成DexFile文件的文件全部转成DexFile,存到Element数组中,供findClass用。

现在我们来看下DexPathList的findClass方法

  public Class findClass(String name, List<Throwable> suppressed) {
414        for (Element element : dexElements) {
415            DexFile dex = element.dexFile;
416
417            if (dex != null) {
418                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419                if (clazz != null) {
420                    return clazz;
421                }
422            }
423        }
424        if (dexElementsSuppressedExceptions != null) {
425            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426        }
427        return null;
428    }

遍历了整个Element数组,调用每个dexfile的loadClassBinaryName的方法去加载类。然后我们来看下DexFile的loadClassBinaryName,感觉越来越接近真相了...

288    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
289        return defineClass(name, loader, mCookie, this, suppressed);
290    }
291
292    private static Class defineClass(String name, ClassLoader loader, Object cookie,
293                                     DexFile dexFile, List<Throwable> suppressed) {
294        Class result = null;
295        try {
296            result = defineClassNative(name, loader, cookie, dexFile);
297        } catch (NoClassDefFoundError e) {
298            if (suppressed != null) {
299                suppressed.add(e);
300            }
301        } catch (ClassNotFoundException e) {
302            if (suppressed != null) {
303                suppressed.add(e);
304            }
305        }
306        return result;
307    }
387    private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
388                                                  DexFile dexFile)
389            throws ClassNotFoundException, NoClassDefFoundError;

层层调用,最后调用了defineClassNative整个native方法完成加载工作,流程结束...

总结下,流程其实不复杂,只是嵌套的比较多,真正复杂的逻辑在natvie层,DexFile很重要,接触过热更新,插件化框架的朋友,可以去看看框架源码,DexFile露脸的机会不少...

5.实验

做个关于ClassLoader的小实验,将一个未安装的apk文件通过ClassLoader加载到系统中,并调用其方法。let's go!!!

QQ截图20170715185055.jpg

上图是这个小demo的项目结构,app是我们将安装到系统中的module,bundle就是我们准备通过ClassLoader去加载到系统的module。
基本的实验流程如下:

  1. 在bundle这个module中写个普通的类和普通的方法
  2. 将bundle打包,debug,release都行
  3. 将bundle.apk push到系统的存储上
  4. 在app中写代码加载bundle.apk,并通过反射技术生成实例对象,再调用其方法

bundle中的实验代码如下:

public class Printer {

    public void print(){
        Log.i("info","i am printer from bundle");
    }

}

非常简单!

app中MainActivity代码:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String apkPath = getExternalCacheDir().getAbsolutePath() + "/bundle.apk";
        Log.i("info", "apkPath=" + apkPath);
        loadApk(apkPath);
    }

    private void loadApk(String apkPath) {
        File optFile = getDir("opt", MODE_PRIVATE);
        Log.i("info", "optFile=" + optFile);
        DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optFile.getAbsolutePath(), null, getClassLoader());
        try {
            Class clz = dexClassLoader.loadClass("com.loubinfeng.www.boundle.Printer");
            if (clz != null) {
                Object instance = clz.newInstance();
                Method method = clz.getMethod("print");
                method.invoke(instance);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行后,真的在Logcat中看到了打印信息

Paste_Image.png

实验demo地址:https://github.com/loubinfeng2013/ClassloaderDemo

推荐阅读更多精彩内容