Android 热修复技术主要可以分为两类:
一类是利用 Java hook 的技术来替换要修复的方法。代表有阿里的 DeXposed、Andfix。
一类是利用 Java 类加载机制优先返回修复的类。代表有 Tinker、HotFix、Nuwa、RocooFix、Robust。
这两类都有着自己的优缺点,事实上从来都没有最好的方案,只有最适合自己的。
热修复原理
热修复实现的利用了 Java 的类加载机制,关键点是 dexElments,代码如下:
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
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;
}
寻找大致过程 PathClassLoader ->BaseDexClassLoader.findClass(String name)->PathList.findClass(name, suppressedExceptions)。
大致相关的 5 个类:
PathClassLoader 用来加载应用程序的 dex。
/**
* 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 {
}
DexClassLoader 这个类是可以用来从 .jar 文件和 .apk 文件中加载 classes.dex。 (必须要在应用程序的目录下面。最终是加载jar、apk 文件里的 dex 文件)。
/**
* 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 {
BaseDexClassLoader PathClassLoader和DexClassLoader继承这个类。BaseDexClassLoader查找类的方法:
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
//.... code
@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;
}
}
PathClassLoader:只能加载已经安装到 Android 系统中的 apk 文件(/data/app 目录),是 Android 默认使用的类加载器。
DexClassLoader:可以加载任意目录下的 dex/jar/apk/zip 文件,比 PathClassLoader 更灵活,是实现热修复的重点。
- PathClassLoader 与 DexClassLoader 都继承于 BaseDexClassLoader。
- PathClassLoader 与 DexClassLoader 在构造函数中都调用了父类的构造函数,但 DexClassLoader 多传了一个optimizedDirectory (optimizedDirectory 是 dex 文件的输出目录(因为在加载 jar/apk/zip 等压缩格式的程序文件时会解压出其中的 dex 文件,该目录就是专门用于存放这些被解压出来的 dex 文件的)。
findClass() 方法中用到了 pathList.findClass(name),附 DexPathList 类代码:
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;
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
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;
}
DexFile.loadClassBinaryName() 方法:
public final class DexFile {
/**
* See {@link #loadClass(String, ClassLoader)}.
*
* This takes a "binary" class name to better match ClassLoader semantics.
*
* @hide
*/
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile)
throws ClassNotFoundException, NoClassDefFoundError;
}
总结:
一个 ClassLoader 可以包含多个 dex 文件,每个 dex 文件是一个 Element,多个 dex 文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历 dex 文件,然后从当前遍历的 dex 文件中找类,如果找类则返回,如果找不到从下一个 dex 文件继续查找。
热修复原理:
ClassLoader 加载类会遍历 dexElments 数组,从 dex 文件中不断寻找你要的 class,找到后立即返回 class,后面的 dex 文件不再读取。热修复就是把有问题 class 打包成 fixDex 文件,插入 dexElements 靠前的位置,让修复的 class 优先被找到。