Android插件化基础1-----加载SD上APK

作者: 隔壁老李头 | 来源:发表于2017-04-26 13:45 被阅读813次

    Android插件化基础的主要内容包括

    本文是第一篇文章,主要是讲解如何加载SD卡上的apk中的class

    本文涉及的内容如下:

    • 1.java的类加载与双亲委托
    • 2.android apk安装简述
    • 3.demo演示
    • 4.demo背后的故事----android的类加载流程 (重点)
    • 5.总结
    • 6.github地址

    一、Java类加载介绍

    先来复习下Java类加载的事情,对Java类加载很熟悉的朋友可以直接略过第一部分,直接从第二部分开始

    (一)什么是ClassLoader:

    我们写完一个java程序后,通过编译,形成若干个.class文件,而这些若干个.class文件组织成一个完成的java程序,当程序运行时,都会调用一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从一个class文件调用到另外一个class文件的某个方法,如果另外一个class不存在,则会引发系统异常。而在程序启动的时候,不会一次性加载程序所有的class文件,而是根据程序的需要,通过java类加载机制(ClassLoader)来动态加载某个class文件到内存中,从而只有class被记载到内存之后,才能被其他class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用到的

    (二)ClassLoader的作用:

    • 1 负责将Class加载到JVM中
    • 2 审查每个类由谁加载(父类优先的等级加载机制)
    • 3 将Class字节码重新解析成JVM统一要求的对象格式

    (三)ClassLoader类的结构分析

    为了更好的理解类加载机制,我们来深入研究下ClassLoader和他的方法

    ClassLoader 类是一个抽象类

    public abstract class ClassLoader
    
    /**  * A classloader isan object thatisresponsible forloading classes. The
    * classClassLoader isan abstract class.  Given thebinary nameofa 
    *class, a classloader should attempt to* locate orgenerate data that 
    *constitutes a definition fortheclass.  A 
    * typical strategy istotransform thenameintoa filenameandthenreada 
    * "class file"ofthatnamefroma filesystem. **/
    
    

    大致的意思是: ClassLoader 是一个负责加载classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,ClassLoader尝试定位或者产生一个class数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件
    以下是ClassLoader常用到的几个方法及其重载方法:

    • 1 ClassLoader
    • 2 defineClass(byte[] ,int ,int )把字节数组b中的内容转换成Java类,返回* 的结果是java.lang.Class类的实例,这个方法被声明为final的
    • 3 findClass(String name)查找名称为name类,返回结果java.lang.Class类的实例
    • 4 loadClass(String name) 加载名称为name的类,返回的结果是java.lang.Class类的实例
    • 5 resolveClass(Class<?>) 链接指定Java类

    其中defineClass方法用来将byte字节流解析成JVM能够识别的Class对象,有了这个方法意味着我们不仅仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,如果我们通过网络接受到一个类的字节码,拿到这个字节码流直接创建类的Class对象形式实例化对象。如果直接调用这个方法生成类的Class对象,这个对象Class对象还没有resolve,这个resolve将会在这个对象真正实例化时才进行

    (三)Java默认提供的三个ClassLoader

    • 1 BootStrap ClassLoader:称为启动类加载器,是java类加载层次中最高层次的类加载器,负责加载JDK中的核心类库,如:rt.jar,resource.jar,charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关jar或clas
    • 2 Extension ClassLoader:称为扩展类加载器,负责加载java的扩展类苦苦,java虚拟机的实现会提供一个扩展目录,该类加载器在此目录里面查找并加载java类。默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar
    • 3 App ClassLoader:称为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件,一般来说,Java应用的类都是由它们来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
      除了系统提供的类加载器以外,开发人员也可以通过集成java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

    (四)ClassLoader加载类的原理----双亲委托模型:

    1原理介绍:

    ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但是可以作用于其他ClassLoader实例的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上到下依次检查的,首先由最顶层的类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没有加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给app ClassLoader进行加载。如果他没有家在得到的话,则返回给委托的发起者,由它到制定的文件系统或者网络等URL加载该类,如果他们都没有加载这个类,则票抛出ClassNotFoundException异常,否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。如下图

    class_image.png

    2为什么要使用双亲委托这种模型?:

    因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要ClassLoader再加载一次了,考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大安全隐患,而双亲委托可以避免这种情况,因为String已经在启动的时候就被引导类加载器(Bootstrap ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中的ClassLoader的搜索类默认算法。

    二.android apk安装简述

    (一)android 打包简述

    Android应用打包成apk时,class文件会被打包成一个或者多个dex文件,将一个apk文件后缀改成.zip格式后解压;里面有class.dex文件,由于android64K方法数的问题,使用MultiDex就会生成多个dex文件。如下图


    image.png

    当Android 系统安装包安装一个应用的时候,会针对不同的平台对Dex进行优化,这个过程由一个专门的工具来处理叫DexOpt。DexOpt是第一次加载Dex文件的时候执行,该过程会生成一个ODEX文件,即Optimised Dex,执行ODEX的效率会比直接执行Dex文件的效率要高很多,加快App的启动和响应。

    PS:

    • 1 odex优化有什么用:
      ODEX的用途是分离程序资源和可执行文件,达到快速软件加载速度和开机速度的目的。
    • 2 棒棒糖与ART带来的问题
      很多人会有疑问,Android5.0开始,默认已经使用ART,弃用Dalvik了,应用程序会在安装时被编译成OAT文件,(ART上运行的格式)ODEX还有什么用那?
      这里我们引用google的权威回答:
    Dex file compilation uses a tool called dex2oat and takes more time than 
    dexopt. The increase in time varies, but 2-3x increases in compile time 
    are not unusual. For example, apps that typically take a second to install 
    using dexopt might take 2-3 seconds.
    

    这里解释下:DEX转换成OAT的这个过程是在用户安装程序或者刷入ROM,OTA更新后首次启动时执行的,按照google的说法,相比做过ODEX优化,未做过优化的DEX转成成OAT要花费更长的时间,比如2-3倍。比如安装一个odex优化过的程序假设需要1秒钟,未做过优化的程序就需要2-3秒。由此次可见,虽然dalvik被弃用了,但是ODEX优化在Android棒棒糖上依旧拥有显著的优化效果。首先ODEX优化不仅仅只是针对应用程序,还会对内核镜像,jar库文件等进行优化。其次,资源和可执行文件分离带来的性能提升无论是运行在ART还是Dalvik,都有效。

    (二)android 安装

    下载好的Android apk, 在安装过程中,其中文件内容是这样处理的:

    • 1 先把apk拷贝到/data/app下, 没错,就是完整的apk, 例如
      com.test.demo-2.apk
    • 2 解压apk,把其中的classes.dex 拷贝到/data/dalvik-cache, 其命名规则是 apk路径+classes.dex, 如: data/app/com.test.demo2.apk/classes.dex
    • 3 在/data/data下创建对应的目录,用于存储程序的数据,例如cache, database等, 目录名称与包名相同, 如com.test.demo.

    要注意的是, 安装过程并没有把资源文件, assets目录下文件拷贝出来,他们还在apk包里面呆着,所以,当应用要访问资源的时候,其实是从apk包里读取出来的。其过程是,首先加载apk里的resources(这个文件是存储资源Id与值的映射文件),根据资源id读取加载相应的资源。

    由于本文主要是讲解android类加载,android apk安装过程就不详细描述了

    三 Demo演示 :

    (一)先看下demo目录

    项目.jpeg
    • 1 其中 dexclassloaderapp 是用来演示的运行的app
    • 2 AppTest 是用来打包成apk的项目

    看下 AppTest 里面的目录结构

    显示.jpeg

    分别看下 IDexTest,IDexTestImpl和string.xml

    public interface IDexTest {
    
        String  getText();
    }
    
    public class IDexTestImpl implements IDexTest {
        @Override
        public String getText() {
            return "我是SD卡上的APK";
        }
    }
    
    <resources>
        <string name="app_name">AppTest</string>
        <string name="showtext">我是SD上的字符串</string>
    </resources>
    

    AppTest 被打包成apk的时候,我们要在 dexclassloaderapp 里面获取这些数据 。现在开始打包apk,名字为"AppTest-release.apk",然后把这个apk放到sd上。
    现在run dexclassloaderapp 项目会出现下面的显示

    device-1.png
    点击 测试加载类 上面的textview会有原来的"类信息!"转变"我是SD卡上的APK",证明已经成功加载到SD卡上的apk
    (ps:6.0手机注意权限,有的手机没有开通权限会报找不到类) 类加载.gif

    四demo背后的故事----android的类加载流程:

    先看下 dexclassLoaderapp 是如何实现在家外部APK class的
    具体实现是在load方法里面

        private void load() {
            // 获取到包含 class.dex 的 jar 包文件
            final File apkFile =
                    new File(Environment.getExternalStorageDirectory().getPath() + File.separator + "apptest-release.apk");
    
            if (!apkFile.exists()) {
    
                Log.e("LGC", "文件不存在");
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        pd.dismiss();
                        Toast.makeText(MainActivity.this, "文件不存在", Toast.LENGTH_LONG);
    
                    }
                });
                return;
            }
    
            if (!apkFile.canRead()) {
                // 如果没有读权限,确定你在 AndroidManifest 中是否声明了读写权限
                // 如果是6.0以上手机要查看手机的权限管理,你的这个app是否具有读写权限
                Log.d("LGC", "apkFile.canRead()= " + apkFile.canRead());
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        pd.dismiss();
                        Toast.makeText(MainActivity.this, "没有读写权限", Toast.LENGTH_LONG);
    
                    }
                });
                return;
            }
    
    
            // getCodeCacheDir() 方法在 API 21 才能使用,实际测试替换成 getExternalCacheDir() 等也是可以的
            // 只要有读写权限的路径均可
            Log.i("LGC", "getExternalCacheDir().getAbsolutePath()=" + getExternalCacheDir().getAbsolutePath());
            Log.i("LGC", "apkFile.getAbsolutePath()=" + apkFile.getAbsolutePath());
    
            try {
                DexFile dx = DexFile.loadDex(apkFile.getAbsolutePath(), File.createTempFile("opt", "dex", getApplicationContext().getCacheDir()).getPath(), 0);
    
                // Print all classes in the DexFile
                for (Enumeration<String> classNames = dx.entries(); classNames.hasMoreElements(); ) {
                    String className = classNames.nextElement();
                    if (className.equals("com.yibao.test.IDexTestImpl")) {
                        Log.d("LGC", "#########################################################" + className);
                        Log.d("LGC", className);
                        Log.d("LGC", "#########################################################" + className);
    
    
                    }
                    Log.d("LGC", "Analyzing dex content, fonud class: " + className);
                }
            } catch (IOException e) {
                Log.d("LGC", "Error opening " + apkFile.getAbsolutePath(), e);
            }
            DexClassLoader dexClassLoader =
                    new DexClassLoader(apkFile.getAbsolutePath(), getExternalCacheDir().getAbsolutePath(), null, getClassLoader());
            try {
                // 加载 com.test.IDexTestImpl 类
                Class clazz = dexClassLoader.loadClass("com.test.IDexTestImpl");
    
                Object dexTest = clazz.newInstance();
    
                Method getText = clazz.getMethod("getText");
    
                final String result = getText.invoke(dexTest).toString();
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        pd.dismiss();
                        if (!TextUtils.isEmpty(result)) {
                            tv.setText(result);
                        }
    
                    }
                });
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
    
        }
    

    大体的流程是先new 一个DexFile,然后loadClass就获取了这个SD卡上的apk资源了,为什么可以这样那?

    因为Android Framework提供了DexClassLoader这个类,简化了『通过一个类的全限定名获取描述次类的二进制字节流』这个过程;我们只需要告诉DexClassLoader一个dex文件或者apk文件的路径就能完成类的加载。
    详细叙述请看下面

    Android的Dalvik/ART虚拟机如果标准Java的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此我们可以利用这一点,在程序运行时手动加载Class,从而达到代码动态加载可执行文建的目的。Android的Dalvik/ART虚拟机虽然与标准java的JVM不一样,所以ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似动态加载插件的功能,只是在Android应用中动态加载一个插件的工作要比java复杂的多。

    (一)android 类加载设计图

    结构图.jpg

    (二)android类加载 类图

    image1.png

    SecureClassLoader的子类是URLClassLoader,其只能用来加载jar文件,在android的Dal/ART上是没法使用的,这里就不过多的介绍了!

    classloader.jpg

    这里面有两个重要的类
    PathClassLoader和DexClassLoader他们分别继承BaseDexClassLoader,那他们的区别是什么?那么看下他们的构造函数

    PathDexClassLoader

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

    DexClassLoader

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

    他们都是调用父类BaseDexClassLoader的构造函数
    BaseDexClassLoader

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

    这四个参数分别的含义:

    • 1 String dexPath
      包含class.dex的apk、jar文件路径,多个用文件分隔符(默认是:)分隔
    • 2 String optimizedDirectory
      用来缓存优化的dex文件的路径,即从apk或者jar文件中提取出来的dex文件。该路径不可以为空,且应该是应用私有,有读写权限(实际上也可以使用外部存储空间,但是这样的话,有代码注入风险),可以通过方式来穿件一个这样的路径.
    • 3 String libraryPath
      存储C/C++库文件的路径集
    • 4 ClassLoader parent
      父类加载器,遵从双亲委托模型
    很明显,对比PathClassLoader只能加载已经安装应用的dex或者Apk文件,DexClassLoader则没有这个限制,可以从SD卡上加载包含class.dex的jar和.apk文件,也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的dex的加载。DexClassLoader 的源码里面只有一个构造函数,也是遵从双亲委托模型

    简单介绍了PathClassLoader和DexClassLoader,但这两者都是对BaseDexClassLoader的一层简单的封装,真正的实现都在BaseClassLoader内,那么咱们看下BaseClassLoader内的具体实现

    (三)android 类加载类从类的角度来先看流程

    通过上面的BaseDexClassLoader的构造函数,咱们知道了BaseDexClassLoader构造的时候创建了一个DexPathList类的对象
    那咱们就继续跟踪看下DexPathList这个的类的构造函数

    DexPathList

    
        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;
            this.dexElements =
                    makeDexElements(splitDexPath(dexPath), optimizedDirectory);
            this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
        }
    

    代码里面调用了makeDexElements()这个方法,其中一个参数是调用splitLibraryPath()方法的返回值。所以先看下splitLibraryPath()方法

        private static ArrayList<File> splitDexPath(String path) {
            return splitPaths(path, null, false);
        }
    
        private static ArrayList<File> splitPaths(String path1, String path2, boolean wantDirectories) {
            ArrayList<File> result = new ArrayList<File>();
            splitAndAdd(path1, wantDirectories, result);
            splitAndAdd(path2, wantDirectories, result);
            return result;
        }
    
        private static void splitAndAdd(String path, boolean wantDirectories,
                                        ArrayList<File> resultList) {
            if (path == null) {
                return;
            }
    
            String[] strings = path.split(Pattern.quote(File.pathSeparator));
    
            for (String s : strings) {
                File file = new File(s);
    
                if (!(file.exists() && file.canRead())) {
                    continue;
                }
                
                           /*
                 * Note: There are other entities in filesystems than
                 * regular files and directories.
                 */
                if (wantDirectories) {
                    if (!file.isDirectory()) {
                        continue;
                    }
                } else {
                    if (!file.isFile()) {
                        continue;
                    }
                }
    
                resultList.add(file);
            }
        }
    

    splitDexPath这个方法里面调用splitPaths()方法,而splitPaths方法调用了splitAndAdd()方法,通过代码查看,大概能明白,这个一系列的方法主要作用是过滤,过滤掉不可读的file和不存在的file,即剩下的都是canRead且是exists的,然后吧这些files add进一个ArrayList<File>,然把这个这个ArrayList<File>作为参数,调用makeDexElements这个方法,那么咱么一起看下这个方法

        private static final String DEX_SUFFIX = ".dex";
        private static final String JAR_SUFFIX = ".jar";
        private static final String ZIP_SUFFIX = ".zip";
        private static final String APK_SUFFIX = ".apk";
        //上面是支持的后缀,由于在下面这个方法用到了,我就放到到这里
    
     private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) {
            ArrayList<Element> elements = new ArrayList<Element>();
    
            for (File file : files) {
                ZipFile 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)) {
                    try {
                        zip = new ZipFile(file);
                    } catch (IOException ex) {
                        System.logE("Unable to open zip file: " + file, ex);
                    }
                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException ignored) {
    
                    }
                } else {
                    System.logW("Unknown file type for: " + file);
                }
    
                if ((zip != null) || (dex != null)) {
                    elements.add(new Element(file, zip, dex));
                }
            }
    
            return elements.toArray(new Element[elements.size()]);
        }
    

    上面的方法大概的意思是,遍历刚上传入的ArrayListM<File>,如果是.dex结尾的直接调用loadDexFile方法,如果是.apk或者.zip或者.jar结尾的用这个File去构造一个ZipFile对象,然后还是把这个ZipFile作为参数调用loadDexFile这个方法,那么咱们就去这个方法里面去看看

      private static DexFile loadDexFile(File file, File optimizedDirectory)
                throws IOException {
            if (optimizedDirectory == null) {
                return new DexFile(file);
            } else {
                String optimizedPath = optimizedPathFor(file, optimizedDirectory);
                return DexFile.loadDex(file.getPath(), optimizedPath, 0);
            }
        }
    
    
        private static String optimizedPathFor(File path,
                                               File optimizedDirectory) {
    
            String fileName = path.getName();
            if (!fileName.endsWith(DEX_SUFFIX)) {
                int lastDot = fileName.lastIndexOf(".");
                if (lastDot < 0) {
                    fileName += DEX_SUFFIX;
                } else {
                    StringBuilder sb = new StringBuilder(lastDot + 4);
                    sb.append(fileName, 0, lastDot);
                    sb.append(DEX_SUFFIX);
                    fileName = sb.toString();
                }
            }
    
            File result = new File(optimizedDirectory, fileName);
            return result.getPath();
        }
    

    上面代码有个设置如果optimizedDirectory==null(PS:PathClassLoader其中的optimizedDirectory就是null)则直接new一个DexFile,如果不为空则调用optimizedPathFor方法,optimizedPathFor就是把复制一份file放到
    optimizedDirectory目录下,最后把这个文件返回回去。
    得到这个DexFile以后,用这个DexFile构造一个Element对象
    在makeDexElements的for循环里面依照上面的方法获取一组DexFile,然后用这一组DexFile去组成Element数组对象放到内存中。

    上述仅仅是构造DexClassLoader流程,下面咱们看下具体导入类的流程

    loadClass()方法,由于DexClassLoader类本身就一个构造函数,所以知道这个方法是父类的方法,那么找下DexClassLoader的父类BaseDexClassLoader.java,结果发现BaseDexClassLoader.java也没有这个方法,所以应该在BaseDexClassLoader.java的父类里面,那么继续寻找BaseDexClassLoader.java的父类ClassLoader里面有这个方法

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
         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;
          }
    
           protected Class<?> findClass(String name) throws ClassNotFoundException {
                 throw new ClassNotFoundException(name);
           }
    

    看上面的源代码发现是先调用findLoadedClass(name) 看注释应该可以理解为检查下是否已经加载了,如果已经加载了,则直接返回Class,如果没有加载,则看看有没有父类加载器,如果有父类加载器,则调用父类加载器的loadClass()方法,如果没有父类加载器即根类加载器,通过根加载器加载(想下是不是上面说的双亲委托模型)。如果都没有,则通过findClass()方法查找,那么进入findClass()进去看看,通过上面源代码发现ClassLoader是个空方法,而DexClassLoader大家也知道,就一个构造函数,所以可以确定这个方法的具体实现在BaseClassLoader里面,那么咱们现在进去BaseClassLoader里面看看

       @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是通过pathList对象的findClass()方法来获取类的,那么咱们继续进去DexPathList.java的findClass方法里面去看看

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

    最后调用两个DexFile的loadClassBinaryName来导入类的,现在进入DexFIle.java中的loadClassBinaryName()方法中去看下

        public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
            return defineClass(name, loader, mCookie, suppressed);
        }
    
        private static Class defineClass(String name, ClassLoader loader, int cookie,
                                         List<Throwable> suppressed) {
            Class result = null;
            try {
                result = defineClassNative(name, loader, cookie);
            } catch (NoClassDefFoundError e) {
                if (suppressed != null) {
                    suppressed.add(e);
                }
            } catch (ClassNotFoundException e) {
                if (suppressed != null) {
                    suppressed.add(e);
                }
            }
            return result;
        }
    
        private static native Class defineClassNative(String name, ClassLoader loader, int cookie)
                throws ClassNotFoundException, NoClassDefFoundError;
    

    通过上面可以看到DexFile的loadClassBinaryName()方法里面调用了defineClass()方法,而defineClass里面又调用natvie方法defineClassNative()在C层去加载类,由于涉及到底层的业务,由于涉及到比较大的内容,这里就不过多的叙述了,后续有时间单独再出一个C层的分析文章。defineClassNative大家可以先理解为通过底层去加载类,如果有这个类,就加载出来,至此整个流程已经完全跑完。不知道大家理解没有。

    由上述可以归结出android类 加载的时序图,如下图:
    (第一次画时序图,画的不好,大家将就的看下,(__) 嘻嘻……)

    (四)android 类加载时序图

    时序图.png loadClass.png

    可以看出BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile数组,由上面分析知道,dexPath传入的原始(.apk,.zip,jar等)文件在optimizedDirectory文件夹生成相应的优化后的odex文件,dexElements数组就是这些odex文件的集合,如果不分包,一般这个数组只有一个Element元素,也就只有一个DexFile文件,而对于这个类加载,就是遍历这个集合,通过DexFile去寻找。最终调用native方法的defineClass

    五总结

    DexClassLoader和PathClassLoader都属于双亲委托模型的类加载器。也就是说,它们在加载一个类之前,会去检查自己及自己以上的类加载器是否已经加载过这个类,如果加载过,就会直接将之返回,而不会重复加载
    PathClassLoader是通过构造函数new DexFile(path)来产生DexFile对象的;而DexClassLoader则是通过静态方法loadDex(path,outpath,0)得到DexFile对象。这两者的区别在于DexClassLoader需要提供一个可写的outputpath路径,用来释放apk包或者jar包中的dex文件。换个说法来说,就是PathClassLoader不能从zip包中释放dex,因此只支持直接操作Dex格式的文件,或者已经安装apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持apk.jar,dex文件,并且会制定的outpath路径释放dex文件

    六github地址

    项目地址

    感谢
    http://androidxref.com/6.0.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
    http://gityuan.com/2017/03/19/android-classloader/
    http://www.jianshu.com/p/2c23a9e88e3d
    http://www.blogjava.net/zh-weir/archive/2011/10/29/362294.html
    https://segmentfault.com/a/1190000004062880
    http://www.voidcn.com/blog/Mr_LiaBill/article/p-4979756.html
    https://yq.aliyun.com/ziliao/160711?spm=5176.8246799.blogcont.19.IRwTYy
    http://www.cnblogs.com/coding-way/p/5212208.html

    相关文章

      网友评论

      • 零度的风:写的很不错。版主看到的一些资料我也看过。不过有个问题一直萦绕着我,没弄明白,使用DexFile.loadDex()将dex优化到一个指定的路径,那么这里的优化与在安装apk时的dexopt或者dex2oat优化的方式一样吗?不知道版主可否讨论下,谢谢
        隔壁老李头: @零度的风 谢谢有机会一起交流
        零度的风:@隔壁老李头 谢谢你的回复。我想这个问题要彻底弄明白只有去跟代码。从dexopt与dex2oat的c代码入手。还有就是现在Android N的混合编译对loadDex方法加载后的dex文件处理逻辑又不太一样,值得深入研究下。期待版主对这一块的分析与讨论,我会一直关注你的,文章写的不错,点个赞。用64k,而不用65k就专业很多,哈哈。
        隔壁老李头:问的好啊,为你点赞,这个我很久以前研究过,我记得是一样的。都是在C层做的,当时我是跟踪到DexFile的构造函数,一直跟踪到底层。后面你再根据APK安装的流程,你会发现到后面有一部分代码是一样的!代码具体的位置我不记得了,不过是否和dex2oat一样,这块没研究过,不过如果和dexFile一样,那么应该也是一样的。 后面有时间搞篇文章单独讨论下

      本文标题:Android插件化基础1-----加载SD上APK

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