简介
热修复的方案有很多种,其中原理也各不相同。目前开源的比较有名的有阿里AndFix、美团Robust、qq的QZone以及tinker等。今天我们就来分析一下tinker热修复的原理。(这里以Android 6.0的源码来分析,之所以要以Android6.0源码来分析而不是以Android7.0或更新的源码分析,是因为Android7.0引入了混合编译,对热补丁有影响。但无论是6.0还是7.0,tinker热修复最核心的原理是一样的,我们分析tinker是为了理解它的运作机制,从而更好地去使用它。如果有对混合编译感兴趣的可以看文章Android_N混合编译与对热补丁影响解析)
Tinker热修复原理
Android里面加载类主要用到了两个类加载器,一个是PathClassLoader,另一个是DexClassLoader,应用程序中的类一般都是通过PathClassLoader来加载类的,不信你在Activity里面调用getClassLoader()方法,然后看得到的ClassLoader对象的类型是不是PathClassLoader类型,答案是肯定的。我们来看下PathClassLoader类的源码:
package dalvik.system;
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
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);
}
}
复制代码
这是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的源码中对它的介绍:
package dalvik.system;
import java.io.File;
//标注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.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getCodeCacheDir();
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
复制代码
看标注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中做文章。
首先看一张图,来了解一下PathClassLoader的加载机制:
Android在加载一个类的时候,会去众多的dex文件里面有顺序的找,比如要找一个Man.class类,先会从classes.dex里面找,如果没找到,会继续去第二个classes2.dex文件里面找,如果找不到,依次往下一个dex包里面找,如果所有的dex里面都没有,就会抛出异常。
下面我们就来看看源码:由上面PathClassLoader类可知,PathClassLoader继承自BaseDexClassLoader,我们看一下BaseDexClassLoader的源码:
package dalvik.system;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
/**
* @hide
*/
public void addDexPath(String dexPath) {
pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
@Override
protected synchronized Package getPackage(String name) {
if (name != null && !name.isEmpty()) {
Package pack = super.getPackage(name);
if (pack == null) {
pack = definePackage(name, "Unknown", "0.0", "Unknown",
"Unknown", "0.0", "Unknown", null);
}
return pack;
}
return null;
}
/**
* @hide
*/
public String getLdLibraryPath() {
StringBuilder result = new StringBuilder();
for (File directory : pathList.getNativeLibraryDirectories()) {
if (result.length() > 0) {
result.append(':');
}
result.append(directory);
}
return result.toString();
}
@Override public String toString() {
return getClass().getName() + "[" + pathList + "]";
}
}
复制代码
BaseDexClassLoader类的加载主要就是靠findClass方法,看findClass(String name)方法的这一行:
Class c = pathList.findClass(name, suppressedExceptions);
复制代码
由此可知BaseDexClassLoader对于类的加载主要还是委托pathList的findClass()方法,这个pathList是个DexPathList类型。
看BaseDexClassLoader的构造方法:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
复制代码
BaseDexClassLoader的构造方法会传入dexPath,这个是dex文件的路径,然后会根据dexPath创建一个DexPathList并赋值给pathList。
同样的,我们看一下DexPathList文件的源码(DexPathList源码很多,我们只贴出重要的部分):
/*package*/ final class DexPathList {
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;
/** List of native library path elements. */
private final Element[] nativeLibraryPathElements;
/** List of application native library directories. */
private final List<File> nativeLibraryDirectories;
/** List of system native library directories. */
private final List<File> systemNativeLibraryDirectories;
/**
* Exceptions thrown during creation of the dexElements list.
*/
private IOException[] dexElementsSuppressedExceptions;
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
suppressedExceptions,
definingContext);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
//...
}
复制代码
重点来了,我们看DexPathList的findClass方法,它会从内部的dexElements数组里面遍历Element去寻找这个类文件。那么这个dexElements是从哪里来的呢?它是从DexPathList的构造方法里面创建的,根据构造方法传入的dexpath按一定的规则拿到路径下的所有dex包,然后封装成Element对象。那么这个findClass方法就很好理解了,它会去遍历dexElements,一个一个的找是否有要寻找的class,如果有就返回,没有就继续往下一个dex里面找,如果所有的dex都没找到这个类,就抛出异常。
好了,到这里也许大家都应该明白我们tinker是从哪里入手做热更新的吧!如果不明白接着往下看(~~)!
既然我们知道Android系统去加载一个类是按照一定的规则的(规则就是上面讲的加载顺序),那么假如我当前app中有一个Test类的一个方法被调用会导致系统崩溃,我们想要利用类加载机制去修复它,应该怎样去修复呢?
首先我们需要在代码里把这个类的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:
假设我们已经拿到修复好的dex,现在要做替换,那么便先要创建一个classLoader去加载修复好的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的类的替换。但是tinker整个框架远远不是这么简单,因为作为一个框架它要考虑的东西要复杂得多,如文章开头提到的Android N混合编译以及其他如dex的验证机制还有针对Android各个版本的兼容性问题等等。
备注
转载请注明出处,如觉得作者写的还不错或想要需了解更多框架源码剖析,请前往github Android三方框架源码剖析,欢迎star!!!
参考
联系方式
- email: xiasem@163.com