2. Tinkder热修复原理
2.1 热修复原理讲解
Android里面加载类主要用到了两个类加载器,一个是PathClassLoader,另一个是DexClassLoader,应用程序中的类一般都是通过PathClassLoader来加载类的,不信你在Activity里面调用getClassLoader()方法,然后看得到的ClassLoader对象的类型是不是PathClassLoader类型,答案是肯定的。
- PathClassLoader类的源码:
这是Android 6.0源码里面的PathClassLoader类,注意看类开头的注释:“Android uses this class for its system class loader and for its application class loader”,看到这我们应该明白这个类是干嘛的了吧,意思就是Android将此类用于其系统类加载器及其应用程序类加载器。也就是说,我们的Android应用程序,无论是系统的java类或是你自己写的类,都是通过PathClassLoader来加载的。
-
DexClassLoader
DexClassLoader是干嘛的呢?我们看下DexClassLoader的源码中对它的介绍:
看标注1处“A class loader that loads classes from {@code .jar} and {@code .apk} files containing a {@code classes.dex} entry. This can be used to execute code not installed as part of an application.”,这说明这个类主要用于加载包含在dex和apk文件中的类,这可用于执行未作为应用程序的一部分安装的代码。也就是说,它可以加载那些未被系统安装的类。
-
重点知识
我们Android为什么要再实现两个类加载器而不是用java里面已经实现好的类加载器呢?原因是Android中对虚拟机做了很多优化,传统java的ClassLoader可以加载Class文件,而在Android中并不是这样,无论是dalvik还是art,它们加载的不再是class文件,而是dex文件。大家都知道,我们生成的apk文件解压后会发现里面有classes.dex文件,如果你引入了multidex,解压出的安装包里面会有多个dex文件,而我们今天要讲的tinker热修复原理,就是在这些dex中做文章。
Android在加载一个类的时候,会去众多的dex文件里面有顺序的找,比如要找一个Man.class类,先会从classes.dex里面找,如果没找到,会继续去第二个classes2.dex文件里面找,如果找不到,依次往下一个dex包里面找,如果所有的dex里面都没有,就会抛出异常。
-
PathClassLoader源码追踪
PathClassLoader ->BaseDexClassLoader -> DexPathList->findClass
重点来了,我们看DexPathList的findClass方法,它会从内部的dexElements数组里面遍历Element去寻找这个类文件。那么这个dexElements是从哪里来的呢?它是从DexPathList的构造方法里面创建的,根据构造方法传入的dexpath按一定的规则拿到路径下的所有dex包,然后封装成Element对象。那么这个findClass方法就很好理解了,它会去遍历dexElements,一个一个的找是否有要寻找的class,如果有就返回,没有就继续往下一个dex里面找,如果所有的dex都没找到这个类,就抛出异常。
-
热修复核心加载原理
首先我们需要在代码里把这个类的bug给修复,然后打出修复后的apk包,并把这个类放入修复后的apk的特定dex里(注:把class放入特定的dex并做出这个拆分包是一项略微麻烦的操作,这里我们只需要知道要把这个dex拿到去替换就行,同时tinker也给我们提供了工具),这样我们就能拿到修复好的含有Test类的dex了,接着就是如何把修复好的dex包放到用户手机上,让classloader去加载修复好的dex了。把dex放入用户手机这一步肯定需要一个放dex的服务器,然后app启动的时候根据版本去服务器请求是否有dex,如果有就下载下来放入特定的目录,然后apk下次启动的时候就可以把修复好的dex插入dexElements数组的前面,这样应用程序通过PathClassLoader去加载类就会优先找到修复好的dex里面的Test类,这样bug就被修复了。
-
加载Dex文件
//dex表示已经拿到修复好的dex文件 File dex = context.getDir("dexpath", Context.MODE_PRIVATE); String optimizeDir = dex.getAbsolutePath() + File.separator + "opt_dex"; File fopt = new File(optimizeDir); //创建一个DexClassLoader去加载这个dex DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
然后我们还需要拿到系统的classLoader,通过反射获取到它的dexElements,然后把dexClassLoader的dexElements插入系统classLoader的dexElements前面,这样我们的系统再去找这个Test类,就会优先找到我们修复包里面的Test类,便达到修复bug的目的。
-
热修复核心代码
public void loadDex(Context context) { //dex表示已经拿到修复好的dex文件 File dex = context.getDir("dexpath", Context.MODE_PRIVATE); String optimizeDir = dex.getAbsolutePath() + File.separator + "opt_dex"; File fopt = new File(optimizeDir); //创建一个DexClassLoader去加载这个dex DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader()); //系统的classLoader PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); try { //1.先获取到dexClassLoader里面的DexPathList类型的pathList Class myDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader"); Field myPathListFiled=myDexClazzLoader.getDeclaredField("pathList"); myPathListFiled.setAccessible(true); Object myPathListObject =myPathListFiled.get(dexClassLoader); //2.通过DexPathList拿到dexElements对象 Class myPathClazz=myPathListObject.getClass(); Field myElementsField = myPathClazz.getDeclaredField("dexElements"); myElementsField.setAccessible(true); Object myElements=myElementsField.get(myPathListObject); //3.拿到应用程序使用的类加载器的pathList Class baseDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader"); Field pathListFiled=baseDexClazzLoader.getDeclaredField("pathList"); pathListFiled.setAccessible(true); Object pathListObject = pathListFiled.get(pathClassLoader); //4.获取到系统的dexElements对象 Class systemPathClazz=pathListObject.getClass(); Field systemElementsField = systemPathClazz.getDeclaredField("dexElements"); systemElementsField.setAccessible(true); Object systemElements=systemElementsField.get(pathListObject); //5.新建一个Element[]类型的dexElements实例 Class<?> sigleElementClazz = systemElements.getClass().getComponentType(); int systemLength = Array.getLength(systemElements); int myLength = Array.getLength(myElements); int newSystenLength = systemLength + myLength; Object newElementsArray = Array.newInstance(sigleElementClazz, newSystenLength); //6.按着先加入dex包里面elment的规律依次加入所有的element,这样就可以保证classLoader先拿到的是修复包里面的Test类。 for (int i = 0; i < newSystenLength; i++) { if (i < myLength) { Array.set(newElementsArray, i, Array.get(myElements, i)); }else { Array.set(newElementsArray, i, Array.get(systemElements, i - myLength)); } } //7.将新的dexElements数组放入系统的classLoader里面。 Field elementsField=pathListObject.getClass().getDeclaredField("dexElements"); elementsField.setAccessible(true); elementsField.set(pathListObject,newElementsArray); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } }
反射获取类的变量相信大家如果有反射的知识,一定可以看懂了吧。根据注释里面的7个步骤,我们就可以完成把修复包里面的Test类加载到dexElements的最前面。然后我们只需要在应用程序的进程启动的时候调用这个方法,就可以实现加载Test类的时候加载的是修复包里面的。代码如下:
public class MyAplication extends Application {
@Override
protected void attachBaseContext(Context base) {
MultiDex.install(base);
loadDex(base);
super.attachBaseContext(base);
}
//...
}
分析到这里我们应该都明白tinker热修复的原理了!它的核心思想就是根据classLoader的加载机制在应用程序启动的时候把修复好的dex包加在有bug的dex包的前面实现对有bug的类的替换。