JAVA进阶——类加载

曾几何时,一个已上线的app中如果出现了bug,若想要即时修复这个bug,就必须重新打新版本的APK包,然后再发布到应用市场让用户升级,发布一个新版本的app中间的手续可能还远不止这些,给用户的体验比较差。但是在热修复技术出现之后,我们能够大大简化修复bug的流程,提升用户体验。热修复技术简单来说,就是下发补丁(内含修复好的class)到用户手机,即让app从服务器上下载,比如王者荣耀、和平精英等手游都会时不时地让用户下载补丁,然后app再通过“某种手段”,使补丁中的class被app调用(本地更新),从而实现bug的修复,这就避免了重新将app发布到应用商城让用户再次安装。在安卓中,类加载就是实现热修复比较常见的一种手段,本文重点讨论类加载技术,因为它是安卓app热修复的基础,在后续的博文中,将会专题讨论热修复技术。

目录

一、类加载是什么?

二、ClassLoader介绍

2.1 ClassLoader以及实现类介绍

2.2 什么是.Dex?

2.3 如何将.class文件转换为.dex文件?

2.4 如何使用类加载器?

三、双亲委派机制

3.1 什么是父加载器

3.2 双亲委派机制是什么

3.3 为什么需要双亲加载机制?

总结


一、类加载是什么?

我们知道,编写的.java源文件首先会被java编译器编译成.class文件,然后将.class文件加载到内存,并且对里面的数据进行校验、解析和初始化,最后转换为可以被JVM直接使用的类型,这就是类加载机制。

JAVA中的类从被加载到虚拟机内存开始,直到被卸载为止,大致会经历以下周期:

1、加载

将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。

2、验证
主要是验证信息是否符合JVM规范,是否存在安全方面的问题。
3、 准备
为类变量(静态变量)分配内存并设置初始值,此时设置的初始值为默认值,具体赋值在初始化阶段完成。​​​
4、 解析

解析过程是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用。

5、初始化

初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

我们这次主要讨论的是加载的阶段。

二、ClassLoader介绍

2.1 ClassLoader以及实现类介绍

java中有一个叫ClassLoader的抽象类,它的主要作用就是用来加载 .class 文件,提供给程序运行时使用。ClassLoader主要有以下几个实现类:

  • BootClassLoader

    用于加载Android Framework层的class文件。比如安卓中的Activity就是用BootClassLoader来加载的,想知道某个类是由哪个加载器加载的,我们可以使用getClassLoader方法来得知。

  • PathClassLoader

    用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex。比如我们的安卓应用中通常会自己创建一个MainActivity,这个我们自己创建的类就是由PathClassLoader来加载的

我们用代码简单地来获取一下几个类的类加载器:

  • DexClassLoader

    用于加载指定的dex,以及jar、zip、apk中的classes.dex

下面是DexClassLoader和PathClassLoader的源码,我们可以看到,他们都继承自一个共同父类BaseDexClassLoader,二者并没有去重写父类的方法,只是增加了几个构造方法。其中DexClassLoader的构造方法比PathClassLoader多了一个路径参数optimizedDirectory,这个路径下的文件会被创建成File对象传给super,其他的地方两个加载器基本一致。

那么上面的路径具体是存放的什么文件呢?这就要从.Dex说起。

2.2 什么是.Dex?

我们都知道在JAVA中,代码源文件会被编译成.class文件,然后交给JVM执行。而在安卓中,由于使用的是一种特殊的JVM也就是DVM,所以不能够直接运行.class文件,需要将所有的.class文件压缩为.Dex文件,最终才可以在 Android 运行时环境执行。我们直接用Android Studio双击打开.Apk文件,就能看到.Dex文件:

我们上面说的optimizedDirectory路径是否就是指定的Dex的路径呢?并不完全是,因为DVM虚拟机在加载一个.Dex文件时,需要先对.Dex文件进行验证和优化等操作,优化后的.Dex文件变成了 oDex(Optimized dex) 文件。再看看我们需要传入的路径名optimizedDirectory,没错,这个参数就是传入的oDex的路径。那使用PathClassLoader为啥不传这个参数咧,因为它使用默认的路径:/data/dalvik-cache。这样看来,DexClassLoader和PathClassLoader这俩类加载器除了一个可以指定oDex路径,一个使用默认路径,其他的貌似也没啥大的区别。

2.3 如何将.class文件转换为.dex文件?

上面说到,.dex文件实际上是.class文件打包压缩后得到的,那么我们自己如何将.class打包为Dex呢?我们可以使用dx工具来操作,dx工具在SDK安装目录下的build-tools文件夹中。 

找到dx工具后,我们可以配置环境变量,将这个路径添加到Path中,便于全局使用这个工具,详细过程不再赘述:

接着我们在Android Studio中找到build得到的.class文件的路径,将com下(需要从连带完整的包名路径开始复制)的所有内容复制到任意文件夹,我这里是复制到了D盘的Temp文件夹:

接着,直接在该界面的导航栏输入cmd,回车,就能在cmd中快速进入这个目录:

然后我们使用dx --dex --output=outPut.dex com\example\javalib\DexTestClass.class,就可以将指定的.class文件打包成.dex文件。这条命令中的output=outPut.dex是指定生成.dex文件的路径和文件名,后面的com\example\javalib\DexTestClass.class是指定要将哪个.class文件打包为.Dex,简单来说,前面的路径是指定输出的.dex文件路径,后面的路径是指定输入的.class文件路径:

2.4 如何使用类加载器?

介绍了这么多种类加载器以及一堆概念,还没自己用加载加载一个类试试,下面我们就来试试吧。

我们接下来准备使用DexClassLoader来加载一个类,要想知道怎么加载,得回顾一下构造方法的四个参数:

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

dexPath:顾名思义,也就是dex的存放路径

optimizedDirectory:上文解释过,oDex的存放路径,但需要注意的是,这个路径不能是SD卡的路径,只能是应用私有目录。这里我们可以用getApplicationInfo().dataDir来获取路径,也可以是getCacheDir等方法获取到的应用私有路径。

librarySearchPath:native库路径,可以传null。

parent:父加载器,至于父加载器是什么,下文的“双亲委派机制”中会详细阐述。

弄清楚了各个参数的含义,我们就可以在代码中尝试使用DexClassLoader来加载一个类了:

package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import dalvik.system.DexClassLoader;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DexClassLoader loader = new DexClassLoader("/outPut.dex", getApplicationInfo().dataDir, null, this.getClass().getClassLoader());
        Class clazz = null;
        try {
            clazz = loader.loadClass("com.example.myapplication.ClassLoadTest");
            Log.d("MainActivity:加载的类名为", clazz.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

我们先利用上文所介绍的方法,生成了outPut.dex文件,然后按照上文介绍的传参方法构造了一个DexClassLoader,并使用loadClass方法尝试加载这个类,从logcat输出信息中可以看到,类已经被成功加载:

三、双亲委派机制

上一章说到,构建一个DexClassLoader加载器,需要传入一个parent参数,那么parent到底是什么呢?一个.dex文件又是如何被类加载器加载出来的呢?想弄清这些问题就需要了解JAVA中的双亲委派机制

3.1 什么是父加载器

首先我们来了解父加载器的概念。其实构建DexClassLoader需要传的parent参数就是指的DexClassLoader的父加载器,那什么是父加载器呢?有人可能会想起第二中我们提到的BaseDexClassLoader,因为DexClassLoader是继承自BaseDexClassLoader,所以BaseDexClassLoader理所应当就是DexClassLoader的父加载器,然而并不是这样。我们需要明确的是,BaseDexClassLoader只是在类的继承关系中是DexClassLoader的父类,而它并不是DexClassLoader的父加载器,父类和父加载器不是同一个概念,注意不要混淆

3.2 双亲委派机制是什么

某个类加载器在接受到加载类的任务时,它会先将加载任务委托给父类加载器,如果父类加载器可以完成类加载任务,就成功返回,父类完不成加载任务就继续找父类的父类来完成……,只有当“长辈”们都无法完成此加载任务时,才自己去加载。简单来说,就是有活儿来了咱先不干,找长辈们干,如果长辈们都干不了我才自己去干。

我们上文中所提到的BootClassLoader就是PathClassLoader的父加载器,想要获得某个加载器的父加载器,可以使用getParent方法:

package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import dalvik.system.DexClassLoader;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ClassLoader mainActivityClassLoader = getClassLoader();
        ClassLoader activityClassLoader = Activity.class.getClassLoader();
        DexClassLoader dexClassLoader = new DexClassLoader("/outPut.dex", getApplicationInfo().dataDir, null, this.getClass().getClassLoader());
        Class clazz = null;
        try {
            clazz = dexClassLoader.loadClass("com.example.myapplication.ClassLoadTest");
            Log.d("MainActivity:加载的类名为", clazz.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        Log.d("MainActivity:", "MainActivity的类加载器是:"+mainActivityClassLoader+",他的父加载器是:"+mainActivityClassLoader.getParent());
        Log.d("MainActivity:", "ActivityClassLoader的类加载器是:"+activityClassLoader+",他的父加载器是:"+activityClassLoader.getParent());
        Log.d("MainActivity:", "dexClassLoader的父加载器是:"+dexClassLoader.getParent());

    }
}

上述代码主要输出结果整理如下:

MainActivity的类加载器是:dalvik.system.PathClassLoader,他的父加载器是:java.lang.BootClassLoader@c9ed822

ActivityClassLoader的类加载器是:java.lang.BootClassLoader@c9ed822,他的父加载器是:null

dexClassLoader的父加载器是:dalvik.system.PathClassLoader

3.3 为什么需要双亲委派加载机制?

1、安全 

如果现在有人想要修改核心底层代码,比如FrameWork层的某些类,或者Object类,他们可能想编写一个同名的类并且自己通过类加载器去骗过编译器瞒天过海,有了双亲委派机制后,这种手法就成了无效的,因为这些底层的核心类肯已经被父加载器(比如BootStrapClassLoader)加载过了,不会再次加载一次,从而防止了某些别有用心的人植入危险代码。

2、避免重复加载

当一个类已经被父类加载器加载过了,就使用父类加载好的即可。即使子加载器也能加载这个类,但是再多加载一遍也是浪费资源,完全没必要。


总结

以上是我学习类加载技术后整理的部分内容,如有错误还请各位大神指出,同时也欢迎各位大佬来讨论和交流技术,本人邮箱hbutys@vip.qq.com

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值