美文网首页
探究Android中的ClassLoader

探究Android中的ClassLoader

作者: 懒癌患者2018 | 来源:发表于2017-07-15 19:17 被阅读0次

    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

    相关文章

      网友评论

          本文标题:探究Android中的ClassLoader

          本文链接:https://www.haomeiwen.com/subject/eydzhxtx.html