Android中的类加载机制

一.概述

通过本篇文章的学习,你将学会:
1.java中类加载机制
2.Android中类加载机制

二.java类加载机制

java类加载流程

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。如下图所示:

image.png

这是一个类从加载到使用及卸载的全部生命周期。
加载
根据一个类的全限定名(如cn.edu.hdu.test.HelloWorld.class)来读取此类的二进制字节流到JVM内部,转换为一个与目标类型对应的java.lang.Class对象,也可以从ZIP包中读取(比如从jar包和war包中读取)。
验证
为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包括文件格式验证、元数据验证、字节码验证和符号引用验证。
准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(类变量即static修饰的变量,且是数据类型的零值,除非被final修饰,比如static int a=1,初始值为0 ,如果是static final int=1,初始值就是1,由于还没有产生对象,实例变量将不再此操作范围内)。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。初始化阶段是执行类构造器<client>方法的过程。
父类委托
先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
java类被jvm加载的过程
image.png

静态代码块执行前,静态变量就已经执行了;非静态代码块执行前,非静态变量就已经执行了。
父类的静态成员变量—->父类静态代码块—->子类静态成员变量—->子类静态代码块—>父类非静态变量—->父类非静态代码块—->父类构造方法—->子类非静态变量—->子类非静态代码块—->子类构造方法。
注意
在类第一次调用时,静态代码块只执行这一次。
静态代码块和静态方法只能调用静态变量,因为它们执行时非静态变量还没初始化;
非静态代码块和非静态方法可以调用任何(静态+非静态)变量。

类加载器
  • 启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类
  • 扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类
  • 应用程序类加载器,Application ClassLoader,加载ClassPath中的类库
  • 自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类


    image.png
双亲委派模型

双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(所有加载请求最终会委派到顶层的Bootstrap ClassLoader加载器中),如果父类加载器无法完成这个加载(该加载器的搜索范围中没有找到对应的类),子类尝试自己加载。这样的好处可以避免一个类被多次加载。
我们看一下loadClass方法:

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          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) {
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  c = findClass(name);
                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加载时进行解析
              resolveClass(c);
          }
          return c;
      }
  }

缓存 -> 父类加载器 -> 没有父类 -> 启动类加载器 -> 自己的 findClass() 方法查找和加载

三.Android类加载机制

在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流。而Dalvik虚拟机如同Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。
Android平台的类加载器
Android中类加载器有BootClassLoader,URLClassLoader,PathClassLoader,DexClassLoader,BaseDexClassLoader,等都最终继承自java.lang.ClassLoader。如图:

image.png

ClassLoader
ClassLoader中重要的方法是loadClass(String name),其他的子类都继承了此方法且没有进行复写。
image.png

从上图可以看出在加载类时首先判断这个类是否之前被加载过,如果有则直接返回,如果没有则首先尝试让parent ClassLoader进行加载,加载不成功才在自己的findClass中进行加载,这和java虚拟机中常见的双亲委派模型一致的。
BootClassLoader
和java虚拟机中不同的是BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用。
URLClassLoader
只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。
PathClassLoader
PathClassLoader是用来加载Android系统类和应用的类,只能加载系统中已经安装过的apk,并且不建议开发者使用。
DexClassLoader
DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载,就是通过这个实现动态加载技术,动态加载技术分为两种,一种是动态加载So包,一种就是动态加载dex/jar/apk文件,安卓动态加载技术默认指的就是第二种方式,出于安全问题,Android并不允许直接加载手机外部存储这类noexec(不可执行)存储路径上的可执行文件,都要先把他们拷贝到data/packagename/内部储存文件路径,确保库不会被第三方应用恶意修改或拦截,然后再将他们加载到当前的运行环境并调用需要的方法执行相应的逻辑,从而实现动态调用。
上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理。
一般我们都是用这个DexClassLoader来作为动态加载的加载器。
注意:PathClassLoader只能加载已安装的apk的dex,其实这说的应该是在dalvik虚拟机上,在art虚拟机上PathClassLoader可以加载未安装的apk的dex(在art平台上已验证)。

ClassLoader加载class的过程

// BaseDexClassLoader
//创建
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
//加载
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException { 
    Class clazz = pathList.findClass(name);
    if (clazz == null) { 
        throw new ClassNotFoundException(name); 
    } 
    return clazz;
}

// PathClassLoader.java,optimizedDirectory永远为null
//创建
public class PathClassLoader extends BaseDexClassLoader {
    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.java
//创建
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

//DexPathList的findClass
public Class findClass(String name) { 
    for (Element element : dexElements) { 
        DexFile dex = element.dexFile;
        if (dex != null) { 
            Class clazz = dex.loadClassBinaryName(name, definingContext); 
          if (clazz != null) { 
              return clazz; 
          } 
        } 
    } 
    return null;
}

//DexFile的loadClassBinaryName
public Class loadClassBinaryName(String name, ClassLoader loader) { 
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);

从上图可以看到:
在创建类加载器过程
optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,optimizedDirectory必须是一个内部存储路径,DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory;而PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的。
加载类的过程
BaseDexClassLoader中有个DexPathList实例pathList,pathList中包含一个DexFile的数组dexElements,dexElements数组就是这些dex文件的集合,dex文件就是内部存储路径optimizedDirectory缓存起来的。如果不分包一般这个数组只有一个Element元素,也就只有一个DexFile文件,而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。简单来说就是:
1.在DexClassLoader的findClass 方法中通过一个DexPathList对象findClass()方法来获取class
2.在DexPathList的findClass 方法中,对之前构造好dexElements数组集合进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
动态加载技术
基于ClassLoader的动态加载技术的一个特点就是,如果程序不重新启动,加载过一次的类就无法重新加载。因此,如果使用ClassLoader来动态升级APP或者动态修复BUG,都需要重新启动APP才能生效。很容易可以想到热修复实现的一种方式,假设现在代码中的某一个类或者是某几个类有bug,那么我们可以在修复完bug之后,将这些个类打包成一个补丁文件,然后通过这个补丁文件封装出一个Element对象,并且将这个Element对象插到原有dexElements数组的最前端,这样当DexClassLoader去加载类时,优先会从我们插入的这个Element中找到相应的类,虽然那个有bug的类还存在于数组中后面的Element中,但由于双亲加载机制的特点,这个有bug的类已经没有机会被加载了,这样一个bug就在没有重新安装应用的情况下修复了。微信的Tinker框架通过修复好的class.dex 和原有的class.dex比较差生差量包补丁文件patch.dex,在手机上这个patch.dex又会和原有的class.dex 合并生成新的文件fix_class.dex,用这个新的fix_class.dex 整体替换原有的dexPathList的中的内容。

除了微信的Tinker,其他的动态加载技术还可以使用jni hook的方式修改程序的执行代码。前者是在虚拟机上操作的,而后者做的已经是Native层级的工作了,直接修改应用运行时的内存地址,所以使用jni hook的方式时,不用重新应用就能生效,比如阿里的dexposed和AndFix。

四.总结

以上就是关于类加载机制的知识点,如有不足或者错误的地方请指正。在技术这块,我们需要多看更需要多写,我们只有不断学习,不断进步才能不被淘汰。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容