内容简介:bug一般是一个或多个class出现了问题,在一个理想的状态下,我们只需将修复好的这些个class更新到用户手机上的app中就可以修复这些bug了。要怎么才能动态更新这些class呢?其实,不管是哪种热修复方案,肯定是如下几个步骤:这里的在加载一个字节码文件时,会询问当前的classLoader是否已经加载过此字节码文件。如果加载过,则直接返回,不再重复加载。如果没有加载过,则会询问它的Parent是否已经加载过此字节码文件,同样的,如果已经加载过,就直接返回parent加载过的字节码文件,而如果整个继承线
bug一般是一个或多个class出现了问题,在一个理想的状态下,我们只需将修复好的这些个class更新到用户手机上的app中就可以修复这些bug了。要怎么才能动态更新这些class呢?其实,不管是哪种热修复方案,肯定是如下几个步骤:
- 下发补丁(内含修复好的class)到用户手机,即让app从服务器上下载(网络传输)
- app通过 某种方式 ,使补丁(apk、dex、jar等文件)中的class被app调用(本地更新)
这里的 某种方式 ,对本篇而言,就是使用Android的类加载器,通过类加载器加载这些修复好的class,覆盖对应有问题的class,理论上就能修复bug了。
二、类加载机制
1、双亲委派模型
在加载一个字节码文件时,会询问当前的classLoader是否已经加载过此字节码文件。如果加载过,则直接返回,不再重复加载。如果没有加载过,则会询问它的Parent是否已经加载过此字节码文件,同样的,如果已经加载过,就直接返回parent加载过的字节码文件,而如果整个继承线路上的classLoader都没有加载过,才由child类加载器(即,当前的子classLoader)执行类的加载工作。
1)特点:
如果一个类被classLoader继承线路上的任意一个加载过,那么在以后整个系统的生命周期中,这个类都不会再被加载,大大提高了类的加载效率。
2)作用:
- 类加载的共享功能
一些Framework层级的类一旦被顶层classLoader加载过,会缓存到内存中,以后在任何地方用到,都不会去重新加载。
- 类加载的隔离功能
共同继承线程上的classLoader加载的类,肯定不是同一个类,这样可以避免某些开发者自己去写一些代码冒充核心类库,来访问核心类库中可见的成员变量。如java.lang.String在应用程序启动前就已经被系统加载好了,如果在一个应用中能够简单的用自定义的String类把系统中的String类替换掉的话,会有严重的安全问题。
验证多个类是同一个类的成立条件:
- 相同的className
- 相同的packageName
- 被相同的classLoader加载
3)loadClass()
通过loadClass()这个方法来验证双亲委派模型
找到ClassLoader这个类中的loadClass()方法,它调用的是另一个2个参数的重载loadClass()方法。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
复制代码
找到最终这个真正的loadClass()方法,下面便是该方法的源码:
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) {
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.
c = findClass(name);
}
}
return c;
}
复制代码
可以看到,如前面所说,加载一个类时,会有如下3步:
- 检查当前的classLoader是否已经加载琮这个class,有则直接返回,没有则进行第2步。
- 调用父classLoader的loadClass()方法,检查父classLoader是否有加载过这个class,有则直接返回,没有就继续检查上上个父classLoader,直到顶层classLoader。
- 如果所有的父classLoader都没有加载过这个class,则最终由当前classLoader调用findClass()方法,去dex文件中找出并加载这个class。
二、Android中的ClassLoader
1、类加载器类型
Android跟 java 有很大的渊源,基于jvm的java应用是通过ClassLoader来加载应用中的class的,Android对jvm优化过,使用的是dalvik虚拟机,且 class文件会被打包进一个dex文件中 ,底层虚拟机有所不同,那么它们的类加载器当然也是会有所区别。
Android中最主要的类加载器有如下4个:
- BootClassLoader:加载Android Framework层中的class字节码文件(类似java的Bootstrap ClassLoader)
- PathClassLoader:加载已经安装到系统中的Apk的class字节码文件(类似java的App ClassLoader)
- DexClassLoader:加载制定目录的class字节码文件(类似java中的Custom ClassLoader)
- BaseDexClassLoader:PathClassLoader和DexClassLoader的父类
一个app一定会用到 BootClassLoader 、 PathClassLoader 这2个类加载器,可通过如下代码进行验证:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.e(TAG, "classLoader = " + classLoader);
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.e(TAG, "classLoader = " + classLoader);
}
}
}
复制代码
上面代码中可以通过上下文拿到当前类的类加载器(PathClassLoader),然后通过getParent()得到父类加载器(BootClassLoader),这是由于Android中的类加载器和java类加载器一样使用的是双亲委派模型。
2、PathClassLoader与DexClassLoader的区别
一般的源码在Android Studio中可以查到,但 PathClassLoader 和 DexClassLoader 的源码是属于系统级源码,所以无法在Android Studio中直接查看。可以到androidxref.com这个网站上直接查看,下面会列出之后要分析的几个类的源码地址。
以下是Android 5.0中的部分源码:
1)使用场景
先来介绍一下这两种Classloader在使用场景上的区别
- PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
- DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。
2)代码差异
下面来看一下PathClassLoader与DexClassLoader的源码的差别,都非常简单
// PathClassLoader
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);
}
}
复制代码
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
复制代码
通过比对,可以得出2个结论:
- PathClassLoader与DexClassLoader都继承于BaseDexClassLoader。
- PathClassLoader与DexClassLoader在构造函数中都调用了父类的构造函数,但DexClassLoader多传了一个optimizedDirectory。
3、BaseDexClassLoader
通过观察PathClassLoader与DexClassLoader的源码我们就可以确定,真正有意义的处理逻辑肯定在BaseDexClassLoader中,所以下面着重分析BaseDexClassLoader源码。
1)构造函数
先来看看BaseDexClassLoader的构造函数都做了什么:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
}
复制代码
- dexPath:要加载的 程序文件 (一般是dex文件,也可以是jar/apk/zip文件)所在目录。
- optimizedDirectory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的)。
- libraryPath:加载程序文件时需要用到的库路径。
- parent:父加载器
tip:从一个完整App的角度来说, 程序文件 指定的就是apk包中的classes.dex文件;但从热修复的角度来看,程序文件指的是补丁。
因为PathClassLoader只会加载已安装包中的dex文件,而DexClassLoader不仅仅可以加载dex文件,还可以加载jar、apk、zip文件中的dex。jar、apk、zip其实就是一些压缩格式,要拿到压缩包里面的dex文件就需要解压,所以,DexClassLoader在调用父类构造函数时会指定一个解压的目录。
2)findClass()
类加载器肯定会提供有一个方法来供外界找到它所加载到的class,该方法就是findClass(),不过在PathClassLoader和DexClassLoader源码中都没有重写父类的findClass()方法,但它们的父类BaseDexClassLoader就有重写findClass(),所以来看看BaseDexClassLoader的findClass()方法都做了哪些操作,代码如下:
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 实质是通过pathList的对象findClass()方法来获取class
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的findClass()方法实际上是通过DexPathList对象(pathList)的findClass()方法来获取class的,而这个DexPathList对象恰好在之前的BaseDexClassLoader构造函数中就已经被创建好了。所以,下面就来看看DexPathList类中都做了什么。
4、DexPathList
1)构造函数
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
...
}
复制代码
这个构造函数中,保存了当前的类加载器definingContext,并调用了makeDexElements()得到Element集合。
通过对splitDexPath(dexPath)的源码追溯,发现该方法的作用其实就是将dexPath目录下的所有程序文件转变成一个File集合。而且还发现,dexPath是一个用冒号(":")作为分隔符把多个程序文件目录拼接起来的字符串(如:/data/dexdir1:/data/dexdir2:...)。
那接下来无疑是分析makeDexElements()方法了,因为这部分代码比较长,我就贴出关键代码,并以注释的方式进行分析:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1.创建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍历所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.将Element集合转成Element数组返回
return elements.toArray(new Element[elements.size()]);
}
复制代码
在这个方法中,看到了一些眉目,总体来说,DexPathList的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)封装成一个个Element对象,最后添加到Element集合中。
其实,Android的类加载器(不管是PathClassLoader,还是DexClassLoader),它们最后只认dex文件,而loadDexFile()是加载dex文件的核心方法,可以从jar、apk、zip中提取出dex,但这里先不分析了,因为第1个目标已经完成,等到后面再来分析吧。
2)findClass()
再来看DexPathList的findClass()方法:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// 遍历出一个dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找类名与name相同的类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
复制代码
结合DexPathList的构造函数,其实DexPathList的findClass()方法很简单,就只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
为什么是调用DexFile的loadClassBinaryName()方法来加载class?这是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件。这可以从Element这个类的源码和dex文件的内部结构看出。
三、热修复的实现原理
经过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Joomla 3.8.10 发布,修复 Windows 自动加载器漏洞
- 模块加载打包工具 Webpack 4.16.1 发布,修复 Bug
- GitLab 11.3.5 发布,修复加载 MR 时的问题
- v-charts 1.17.2 发布,修复 common.min 加载问题
- 介绍同步加载、异步加载、延迟加载[原创]
- .net加载失败的程序集重新加载
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Impractical Python Projects
Lee Vaughan / No Starch Press / 2018-11 / USD 29.95
Impractical Python Projects picks up where the complete beginner books leave off, expanding on existing concepts and introducing new tools that you’ll use every day. And to keep things interesting, ea......一起来看看 《Impractical Python Projects》 这本书的介绍吧!