Android类加载机制源码探究

1. JVM类加载之双亲委派模式

(此小节只做简单介绍,详细分析请阅读笔者的另一篇文章:JVM高级特性与实践(九):类加载器 与 双亲委派模式(自定义类加载器源码探究ClassLoader)

(1)介绍

Java开发者对于“双亲委派模式”必然不陌生,这是JVM中的一个重要知识点,它是类加载器的重要特征,类加载器分类如下:

    启动类加载器:负责将指定类库加载到虚拟机内存中。无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。
    拓展类加载器:负责将指定类库加载到内存中。开发者可以直接使用标准扩展类加载器
    自定义类加载器:负责用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

上图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

(2)工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。

(3)模式优点

使用双亲委派模型来组织类加载器之间的关系,好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。

相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但是永远无法被加载运行。

2. Android类加载介绍

Android中的ClassLoader类加载机制主要用来加载dex文件,系统提供了PathClassLoader、DexClassLoader两个API可供选择。ClassLoader种类如下:

    BootClassLoader
    BaseDexClassLoader:父类
    PathClassLoader:只能加载已安装到Android系统的APK文件;
    DexClassLoader:支持加载外部的APK、Jar或dex文件;(所有的插件化方案都是使用它来加载插件APK中的.class文件,也是动态加载的核心依据!)

如上,在简单理解之后发现Android的ClassLoade和Java的大体上是一一对应的,只不过内部实现有些变化。思考一个问题,一个App正常运行最少需要哪些ClassLoade?

答案揭晓:最少需要BootClassLoader和PathClassLoader。首先BootClassLoader是无可或缺的,因为它需要加载framework层的一些class文件,而PathClassLoader用来加载已安装到系统上的文件。

因此一个应用运行至少需要以上两个ClassLoade,下面通过一个简单demo来证实以上猜想。
 

public class MainActivity extends AppCompatActivity { 
    @Override 
    protected void onCreate(Bundle savedInstanceState)
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
        ClassLoader classLoader = getClassLoader(); 
        if(classLoader != null){ 
            Log.e("lemonnnnnn", "classLoader: " + classLoader.toString()); 
            while (classLoader.getParent() != null){ 
                classLoader = classLoader.getParent(); 
                Log.e("lemonnnnnn", "classLoader: " + classLoader.toString()); 
            } 
        } 
    } 
}

 

 

上述测试代码逻辑也很简单,获取并输出加载当前应用的类加载器,然后再判断其父加载器并输出(双亲委派模式)。查看控制台显示可知输出了PathClassLoader、BootClassLoader,因此证实了以上猜想。

双亲委派模式 特点及作用

    类加载的共享功能,一些framework层的类被顶层classLoader加载过后会缓存在内存中,避免重复加载。
    类加载的隔离功能,不同继承实现的classLoader加载的类肯定不会是同一个类,一些系统层级类java.lang.String 会在初始化时被加载,可避免用户写代码访问核心类库可见的成员变量。 例如java.lang.String就是在系统启动之前就已经加载好,用户可自定义一个String类提前加载与之替换,这会带来严重的安全问题。

上述就引发出一个问题:如何的两个类才算是相同的类呢?两个类的包名、类名相同即可?并非如此!还需加上一个条件:同一个ClassLoader加载,以上三个条件成立,这两个类才能被称为相同类。
3. Android类加载源码过程解析

此处的ClassLoader是java.lang包下的,因此与那篇讲解Java类加载机制中讲解的逻辑大同小异,最多只是版本上的差别,无须赘述,最大的区别则在于继承此类并实现的一些类,也就是Android的dalvik.system包下的BaseDexClassLoader、PathClassLoader、DexClassLoader,见下图:

 

 

如上图,在AS编辑器中点进详情无法阅读dalvik.system包下类源码,接下来在网页中提供源码作以分析。

(1)DexClassLoader源码分析

package dalvik.system; 
import java.io.File; 
public class DexClassLoader extends BaseDexClassLoader { 
    public DexClassLoader(String dexPath, String optimizedDirectory, String
                librarySearchPath, ClassLoader parent) { 
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); 
    } 
}

以上源码可以看到DexClassLoader类中只有一个构造方法,4个参数含义分别是:

    dexPath:指定要加载dex文件的路径;
    optimizedDirectory:指定dex文件需要被写入的目录,一般是应用程序内部路径(不可以为null);
    librarySearchPath:包含native库的目录列表(可能为null);
    parent:父类加载器;

DexClassLoader类注释:用来加载包含dex的jar包或apk中的类,也可以执行于尚未安装到应用中的代码,因此它才是动态加载的核心!
(2)PathClassLoader源码分析

package dalvik.system; 
public class PathClassLoader extends BaseDexClassLoader { 
    public PathClassLoader(String dexPath, ClassLoader parent) { 
        super(dexPath, null, null, parent); 
    } 
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoade
         parent){ 
        super(dexPath, null, librarySearchPath, parent); 
    } 
}

有别于DexClassLoader,PathClassLoader只是一个简单的类加载器实现,运行于本地文件系统中的文件和目录,但不尝试从网络加载类。Android用此类加载器PathClassLoader来加载一些系统级别类和已存在于应用中的类。

查看源码可知PathClassLoader有两个构造方法,其参数相较于DexClassLoader,少了一个指定dex文件需要被写入的内部目录optimizedDirectory,因此PathClassLoader只能加载已安装到应用的dex文件。

(3)BaseDexClassLoader源码分析

以上DexClassLoaderPathClassLoader两个类源码没有具体实现,最大的区别在于后者只能加载已安装于应用的dex文件,而详情部分还是要参数它们的父类——BaseDexClassLoader

上图是BaseDexClassLoader类重点源码部分,类中只有一个成员变量DexPathList,暂不理会其含义,继续查看其构造方法,其中创建了DexPathList对象,传入了四个参数,分别是:

    DexClassLoader:父类加载器本身;
    dexPath:需要加载的dex文件路径;
    librarySearchPath:包含native库的目录列表(可能为null);
    optimizedDirectory: dex文件需要被写入的内部目录(可能为null);

BaseDexClassLoader 构造方法中的这些参数是其子类传过来的,之前介绍过,并不陌生,只是对于在其构造方法中只做了一件事——创建DexPathList对象,有些不解。继续查看重点方法findClass(String name),重点部分笔者用红框圈出来了,通过成员变量dexList的findClass 加载获取的类返回,若类为null则报错,此处意味着真正执行加载类的重点部分并非是BaseDexClassLoader,它也只是一个中介,真相在于DexPathList类,继续延伸查看此类。
(4)DexPathList源码分析——背后的Boss

首先查看它的一些重要成员变量:

    DEX_SUFFIX:字符串类型,值是”.dex”;
    definingContext: ClassLoader类型,加载器,也就是BaseDexClassLoader 构造方法中创建DexPathList时传入的加载器;
    dexElements: Element[]类型,Element是一个内部类。此类作用就是指定dex/resource/native 库路径,其内部重要成员DexFile的dexFile,这是dex文件在Dalvik安卓虚拟机中的具体实现,稍后讲解;
    后续成员变量类型类似,只是代表不同数据,不再赘述……

接下来查看其构造方法:

查看其构造方法,就是用来接收参数并对成员变量赋值。由此可知参数definingContext(即ClassLoader)、dexPath一定不可为null,否则直接报异常,optimizedDirectory被写入内部的目录可能为null(即使用默认系统目录),然而重点在于笔者圈起来的第二个红框,调用内部makeElements方法获取Element数组赋值给成员变量dexElements。深入查看,如何通过上述几个参数获得Element数组,此方法有几个重载,最终调用的方法如下:
 

private static Element[] makeElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions,boolean ignoreDexFiles,ClassLoader loader) { 
    Element[] elements = new Element[files.size()]; 
    int elementsPos = 0; 
    //循环遍历所有File并加载dex 
    for (File file : files) { 
        File zip = null; 
        File dir = new File(""); 
        DexFile dex = null; 
        String path = file.getPath(); 
        String name = file.getName(); 
        if (path.contains(zipSeparator)) { 
            String split[] = path.split(zipSeparator, 2); 
            zip = new File(split[0]); 
            dir = new File(split[1]); 
        } else if (file.isDirectory()) { 
            //若果该file是文件夹格式,则继续递归 
            elements[elementsPos++] = new Element(file, true, null, null); 
        } else if (file.isFile()) { 
            if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) { 
                // ⭐️⭐️⭐️⭐️⭐️若该file是文件且是以dex后缀结尾,说明正是需要加载的文件,调用loadDexFile去创建一个dex(DexFile类型) 
                try { 
                   dex = loadDexFile(file, optimizedDirectory, loader, elements); 
                } catch (IOException suppressed) { 
                    System.logE("Unable to load dex file: " + file, suppressed);
                    suppressedExceptions.add(suppressed); 
                } 
            } else { 
                zip = file; 
                // ⭐️⭐️⭐️⭐️⭐️若该file是压缩文件,调用loadDexFile去创建一个dex(DexFile类型
                if (!ignoreDexFiles) { 
                    try { 
                        dex = loadDexFile(file, optimizedDirectory, loader, elements); 
                    } catch (IOException suppressed) {
                        suppressedExceptions.add(suppressed); 
                    } 
                } 
            } 
        } else { 
            System.logW("ClassLoader referenced unknown path: " + file); } 
            if ((zip != null) || (dex != null)) { 
                elements[elementsPos++] = new Element(dir, false, zip, dex); 
            } 
        } 
    if (elementsPos != elements.length) { 
        elements = Arrays.copyOf(elements, elementsPos); 
    } 
    return elements; 
}

如上DexPathList类的重点方法makeElements源码,方法中的参数经过上述源码讲解后也能够知名见意了,只有一个需要特别说明:files其实是对dexPath的一个转化,获得了该路径内的所有文件。笔者已在源码中加以注释,主要逻辑就是循环遍历files(由dexPath转化的),文件中可能包含文件夹或压缩文件,分别判断,找到后缀为dex文件,调用loadDexFile加载生成DexFile文件(⭐️注释处),最后将生成的dex文件和路径等信息传入Element构造方法来创建对象,返回Element数组

此方法makeElements逻辑并不复杂,需要格外注意一下内部临时变量dex,它是DexFile类型,代表着dex文件。在makeElements方法中判断file是文件格式或者zip压缩格式时,都会调用此方法来创建DexFile对象,具体有何不同呢?进一步查看loadDexFile方法源码,查看其内部细节:
 

 /*
  *实例化DexFile类
  */ 
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException { 
    if (optimizedDirectory == null) { 
        return new DexFile(file, loader, elements); 
    } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); 
        return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements); 
    } 
}

查看loadDexFile源码,其主要作用就是创建DexFile对象返回,
首先判断写入目录optimizedDirectory是否为null,如果为null表明file确实是一个dex文件,直接创建DexFile,否则会先将其解压获取真正DexFile文件。

DexPathList类的makeElements方法核心作用就是:将指定加载路径dexPath的所有文件遍历获取dex文件,并转换成DexFile类型存储到Element数组中。(Element数组的作用是为了后续DexPathList类的findClass方法铺垫)

下面就来解析最万众瞩目的重点,DexPathList类的findClass方法:

public Class findClass(String name, List<Throwable> suppressed) { 
    //遍历 
    for (Element element : dexElements) { 
        DexFile dex = element.dexFile; 
        if (dex != null) { 
            //通过dex来加载类返回字节码⭐️⭐️⭐️⭐️⭐️ 
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); 
            if (clazz != null) { 
                return clazz; 
            } 
          } 
       } 

    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); 
    } 

    return null; 
}

此方法的逻辑也不难,就是遍历之前makeElements 方法中存储好的Element数组,将Element类型转换为DexFile类型,调用DexFile的内部方法loadClassBinaryName在dex文件中查找获取拼接成class字节码文件返回。

小结

Android中类加载器的PathClassLoader和DexClassloader所调用的findClass方法其实并非是自己实现的,它们内部只实现了本身的构造方法,因此调用的是其父类BaseDexClassLoader中实现的方法,可是最后追述到的真正实现者是DexPathList类!由它来具体实现了findClass方法,而此方法内部具体是调用了DexFile的核心内部方法loadClassBinaryName实现重要功能:在dex文件中查找获取拼接成class字节码文件返回。
 

DexPathList源码


(5)DexFile源码分析——Boss的心腹

下面具体查看DexFile的核心内部方法loadClassBinaryName实现:

 public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) { 
    return defineClass(name, loader, mCookie, this, suppressed); 
}
private static Class defineClass(String name, ClassLoader loader, Object cookie, DexFile dexFile, List<Throwable> suppressed) { 
    Class result = null; 
    try { 
        //⭐️⭐️⭐️⭐️⭐️ 
        result = defineClassNative(name, loader, cookie, dexFile); 
    } catch (NoClassDefFoundError e) { 
        if (suppressed != null) { 
            suppressed.add(e); 
        } 
    } catch (ClassNotFoundException e) { 
        if (suppressed != null) { 
            suppressed.add(e); 
        } 
    } 
    return result; 
}

以上源码可以发现代码的一个设计准则:loadClassBinaryName方法类型是public的,可供外部调用,但其内部只有调用defineClass方法这一行代码,而defineClass方法类型是private的,仅供内部调用,因此它只是借助loadClassBinaryName 方法做了一层封装,保持了私有性!

继续查看defineClass方法源码,逻辑也十分简单,除了异常捕获之外,核心代码只有一行:defineClassNative(name, loader, cookie, dexFile); ,通过它完成类的查找,查看详情:

最后的结果很明显,这是一个native方法,我们无法再向下剖析。若是对dex文件格式颇有了解或者阅读过笔者写过的分析dex格式文章,可知一个dex文件中存储了整个工程中所有的class文件,其文件数据存储在dex文件中的“数据区”。因此也可以推理出defineClassNativenative方法是通过C/C++在dex文件中查找获取拼接成class字节码文件返回。

DexFile源码地址
4 重点总结

以上就是对Android的ClassLoader加载机制源码部分的剖析,其实整个过程并不复杂,只是有些逻辑上的嵌套,涉及到ClassLoader、DexClassLoader 、PathClassLoader 、BaseDexClassLoader 、DexPathList DexFile多个类之间方法互相调用,真正有难度的是最后native方法中的C层处理(此处不深究,有兴趣可自行研究C层)。

(笔者强烈建议认真阅读下面时序图,也许上述一系列的源码分析让你有些云里雾里,但笔者在画完时序图后,骤然理解,颇有“拨开云雾见天日 守得云开见月明”之感!画图实在有助于理解)

结合以上Android类加载时序图,再次回顾一下ClassLoader源码的解读研究过程:

    首先类的加载是在ClassLoader类的loadClass 方法中进行,此方法会判断此类是否被自己或双亲加载过(这也是著名的“双亲委派模式”);
        若加载过则无需重复load,直接返回类实例;
        否则调用findClass方法寻找获取这个类,可是findClass方法在ClassLoader类中是一个空实现,真正实现是在BaseDexClassLoader类中;
            而BaseDexClassLoader类也未具体实现,调用的实则是DexPathList类中的findClass方法;
                DexPathList类中findClass方法最终又调用DexFile中的defineClassNative ,DexFile的一个native方法来完成主要类加载逻辑。

以上是类加载过程涉及到的几个类中方法互相调用最终实现“类加载”的过程,以下是重点方法中实现的逻辑总结:

首先在DexPathList类的构造方法中:将所有的dex文件(File类型)转换成DexFile类型,并且将其转化为Element数组便于findClass方法逻辑处理,然后在findClass 方法中遍历Element数组(Element类型中存储着DexFile类型),获取Element中的DexFile,调用DexFile的内部方法loadClassBinaryName在dex文件中查找获取拼接成class字节码文件返回(loadClassBinaryName是一个 native方法)。

而这整个过程,一系列方法、类之间调用的核心逻辑是:通过指定加载dex路径中遍历文件找到dex文件,然后在存储了整个工程class文件数据中的dex文件中,查找搜索并拼接 class字节码文件返回。
5. 动态加载难点

以上就是ClassLoader中的一个loadClass和findClass的过程,了解之后接下来介绍Android动态加载的难点:

在了解以上源码解析后,发现Android的动态加载不过是使用DexClassLoader指定需要加载的APK路径,思路很简单呀?其实在实际使用中并不尽然,由于Android系统的特点和第三方原因带来了以下限制:

    许多组件类需要注册才能使用。例如Android系统中的Activity、Service等需要在Manifest中注册才可以工作,这意味着即使开发者在工程中加载了一个新的组件,若没有注册也将无法工作。
    资源的动态加载复杂度高。 Android开发的一大特点就是使用资源,将资源通过ID注册好再来使用,因此资源的注册这一步不可或缺,之后才可以通过ID向Resource实例中获取对应资源。这意味着动态加载时运行的新类中若涉及到资源的加载,由于新类资源没有注册的原因,程序会抛出异常。
    Android各版本的差异可能存在加载资源、注册的方式不同的隐患问题,对适配造成影响。

以上总结的问题存在一个共性:即Android程序运行需要一个上下文环境,它可以为Android中的组件提供使用的功能,例如主题、资源、组件查询等等。

因此现在面临的问题就是如何为动态加载到程序中的类或者资源提供这样一个Android上下文环境呢?这也是许多第三发动态加载库Tinker、AndFix核心解决问题关键,学习它们的实现原理着实必要,后续涉及。
 

 作者:lemonGuo
来源:CSDN
原文:https://blog.csdn.net/itermeng/article/details/79350945

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值