热修复原理分析
什么是热修复
热修复是指在用户无感知情况下对已安装的app使用补丁进行紧急修复。优点,效率高,用户体验好。
热修复框架
android热修复技术主要可以分为两类:
这两类都有着自己的优缺点,事实上从来都没有最好的方案,只有最适合自己的。
热修复原理
热修复实现的利用了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文件中加载classed.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;
-
}
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文件继续查找。(出自安卓App热补丁动态修复技术介绍)
热修复原理:
ClassLoader加载类会遍历dexElments数组,从dex文件中不断寻找你要的class,找到后立即返回class,后面的dex文件不再读取。热修复就是把有问题class打包成fixDex文件,插入dexElements靠前的位置,让修复的class优先被找到。
手动实现热修复
1步:通过使用dx.bat将修复的class打包成fix.dex。
命令 : dx --dex --output=E:\patch\dex\fix.dex E:\patch\cls
--output=E:\patch\dex\fix.dex dex输出路径
E:\patch\cls 打包指定目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)
3歩:将fix.dex文件搬到应用目录下,如/data/data/pkgName/odex/
4歩:通过反射将fix.dex添加到dexElements比较靠前的位置(要在bug class所在的dex前面)。
这里借助工具类FixDexUtils.
5歩:自定义MyApplication,onCreate方法中调用FixDexUtils.loadFixedDex(this);
FixDexUtils代码:
-
public class FixDexUtils {
-
private static final String DEX_DIR = "odex";
-
private static HashSet<File> loadedDex = new HashSet<File>();
-
-
static {
-
loadedDex.clear();
-
}
-
-
public static void loadFixedDex(Context context) {
-
if (context == null) {
-
return;
-
}
-
//遍历所有的修复的dex
-
File fileDir = context.getDir(DEX_DIR, Context.MODE_PRIVATE);
-
File[] listFiles = fileDir.listFiles();
-
for (File file : listFiles) {
-
if (file.getName().startsWith("classes") && file.getName().endsWith(".dex")) {
-
loadedDex.add(file);
-
}
-
}
-
//合并之前的dex
-
doDexInject(context, fileDir, loadedDex);
-
}
-
-
private static void doDexInject(final Context appContext, File filesDir, HashSet<File> loadedDex) {
-
String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
-
File fopt = new File(optimizeDir);
-
if (!fopt.exists()) {
-
fopt.mkdirs();
-
}
-
//1.加载应用程序的dex
-
//2.加载指定的修复的dex文件。
-
//3.合并
-
try {
-
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
-
for (File dex : loadedDex) {
-
//2.加载指定的修复的dex文件。
-
DexClassLoader classLoader = new DexClassLoader(
-
dex.getAbsolutePath(),
-
fopt.getAbsolutePath(),
-
null,
-
pathLoader
-
);
-
///通过反射获得classLoader、pathLoader的pathList
-
Object dexObj = getPathList(classLoader);
-
Object pathObj = getPathList(pathLoader);
-
//通过反射获得classLoader、pathLoader的DexElements
-
Object mDexElementsList = getDexElements(dexObj);
-
Object pathDexElementsList = getDexElements(pathObj);
-
//将classLoader、pathLoader的DexElements合并
-
Object dexElements = combineArray(mDexElementsList, pathDexElementsList);
-
//重新给pathLoader的PathList里面的dexElements赋值
-
setField(pathObj, pathObj.getClass(), "dexElements", dexElements);
-
}
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-
-
private static void setField(Object obj, Class<?> cl, String field, Object value) throws Exception {
-
Field localField = cl.getDeclaredField(field);
-
localField.setAccessible(true);
-
localField.set(obj, value);
-
}
-
-
private static Object getField(Object obj, Class<?> cl, String field)
-
throws Exception {
-
Field localField = cl.getDeclaredField(field);
-
localField.setAccessible(true);
-
return localField.get(obj);
-
}
-
-
//反射获得BaseDexClassLoader的pathList
-
private static Object getPathList(Object baseDexClassLoader) throws Exception {
-
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
-
}
-
-
//反射DexPathList的dexElements
-
private static Object getDexElements(Object obj) throws Exception {
-
return getField(obj, obj.getClass(), "dexElements");
-
}
-
-
/**
-
* 两个数组合并
-
*
-
* @param arrayLhs
-
* @param arrayRhs
-
* @return
-
*/
-
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
-
Class<?> localClass = arrayLhs.getClass().getComponentType();
-
int i = Array.getLength(arrayLhs);
-
int j = i + Array.getLength(arrayRhs);
-
Object result = Array.newInstance(localClass, j);
-
for (int k = 0; k < j; ++k) {
-
if (k < i) {
-
Array.set(result, k, Array.get(arrayLhs, k));
-
} else {
-
Array.set(result, k, Array.get(arrayRhs, k - i));
-
}
-
}
-
return result;
-
}
-
}
代码:热修复简单实例
参考
Android dex分包方案
安卓App热补丁动态修复技术介绍
Alibaba-AndFix
Bug热修复框架原理及源码解析
Android 热修复Nuwa的原理及Gradle插件源码解析
Android中热修复框架Robust原理解析+并将框架代码从"闭源"变成"开源"(下篇)