一、前言
最近看了很多第三方的热修复框架的实现,比如阿里的AndFix,对于我们在自己的app里面接入SDK很是方便,至于内部的实现基本不需要我们怎么关注都可以。如此,我们就真的变成搬砖的码农,所以,不行,我们得尝试自己手动来实现一遍安卓的热修复,究其是如何实现的。
二、实现原理
在动手前,我们对其原理得有个大概的了解。我们知道,Java的虚拟机JVM运行代码时,加载的是.class字节码文件,而Android的Dalvik/ART虚拟机加载的是Dex文件,不过他们的工作机制是一样的,都经过ClassLoader这个类加载器,只不过,Android重新定义了两个类
DexClassLoader和PathClassLoader去解析类,他们是继承BaseDexClassLoader类的,关于这两个类的介绍:
1)、PathClassLoader:官方文档解析 Android uses
this class for its system class loader and for its application
class loader(s),使用该类作为系统类和应用类的加载器,也即只能加载已经安装到Android系统的apk文件
2)、
DexClassLoader:可加载jar、apk和dex文件,可以从存储外部加载。
本文我们通过DexClassLoader来来加载dex文件,然后通过Java类反射的原理来实现热修复的功能,先来看一段DexClassLoader 使用的代码:
/**
* 第一个参数:是dex压缩文件的路径
* 第二个参数:是旧dex解压优化后生成新dex存放的目录(注意:API文档说明,要求我们不能把dex存放在外部存储器中)
* 第三个参数:本地依赖库目录,可以为null
* 第四个参数:是上一级的类加载器
*/
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputDirs,null,getClassLoader());
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.lsy.hotfix.Bugs");
Bugs bugs = (Bugs) clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
上面这段代码可以看到通过DexClassLoader类,我们可以从一个Dex文件中提取一个简单类出来。
三、实现步骤
1、首先修改我们要修复的的类,这里很简单,就toast出来一句提示
public class Bugs {
public static void showToast(Context context){
Toast.makeText(context, "已修复", Toast.LENGTH_SHORT).show();
}
}
2、把修改后的类,打包成dex文件
把.class字节码文件打包成dex文件,我们需要用到Android工具包里面的dx打包工具,该工具在我们sdk安装目录下的build-tools/22.0.1(对应一个版本的)的文件夹里面。
这个时候,我们cmd在windows的命令行窗口输入dx命令还是不认的,因为如果我们希望全局快捷使用,还得把这个目录配置到我们的环境变量中去,配置完环境变量之后,再到窗口输入dx命令,就可以看到
看dx工具的使用还挺多的参数,那么我们只需要知道其基本的用法就够了
dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]
这里,我使用下面的命令进行生成,特别注意,要用绝对路径
patch.dex文件已经生成。三、补丁类替换
补丁生成后,我们这里把它放到手机外部存储的根目录,(通常如果结合后台服务器的话,我们可以把dex文件放到远程服务器,然后下载下来。)
接下来就是进行类的替换,这应该是本篇的重点所在。我们所有的类被调用都是通过ClassLoader类加载器的findClass()方法来进行查找的,在Android系统中类被加载顺序是:
ClassLoader ——> BaseDexClassLoader ——> PathClassLoader ——> DexClassLoader
其中,根据上面,PathClassLoader 和 DexClassLoader 是Android系统继承BaseDexClassLoader实现的,而已被安装的类会在PathClassLoader找到,所以,我们要把补丁的类替代有问题的类,那么就要把从DexClassLoader加载的到合拼到PathClassLoader的类前面去,这样,就会优先执行修复的类。具体点,我们看下源码:
1)、BaseDexClassLoader的源码:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
2)、DexPathList核心代码
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
/**
* 加载dex或资源文件,存放在一个element数组返回
*/
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if the
* zip file turns out to be resource-only (that is, no classes.dex file in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
3)、PathClassLoader继承
BaseDexClassLoader,其
构造器
源码:
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
4)、DexClassLoader同样也是继承BaseDexClassLoader,其构造器源码:
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
从分析上面这几个加载器的源码,我们要替代类的做法,就是将DexClassLoader中的
dexElements
与PathClassLoader中的
dexElements
进行合拼,并且把补丁dex的类放到新
dexElements最前面,这样,加载的Bugs类就是最新补丁的类了。
下面是替换类的详细实现代码:
package com.lsy.hotfix;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import android.annotation.TargetApi;
import android.content.Context;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public final class HotFix {
/**
* 修复指定的类
* @param context 上下文对象
* @param patchDexFile dex文件
* @param patchClassName 被修复类名
*/
public static void patch(Context context, String patchDexFile, String patchClassName) {
if (patchDexFile != null && new File(patchDexFile).exists()) {
try {
if (hasLexClassLoader()) {
injectInAliyunOs(context, patchDexFile, patchClassName);
} else if (hasDexClassLoader()) {
injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
} else {
injectBelowApiLevel14(context, patchDexFile, patchClassName);
}
} catch (Throwable th) {
}
}
}
private static boolean hasLexClassLoader() {
try {
Class.forName("dalvik.system.LexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static boolean hasDexClassLoader() {
try {
Class.forName("dalvik.system.BaseDexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException, NoSuchFieldException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader();
String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
Class cls = Class.forName("dalvik.system.LexClassLoader");
Object newInstance =
cls.getConstructor(new Class[] {String.class, String.class, String.class, ClassLoader.class}).newInstance(
new Object[] {context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});
cls.getMethod("loadClass", new Class[] {String.class}).invoke(newInstance, new Object[] {patchClassName});
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
setField(obj, PathClassLoader.class, "mLexs",
combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
}
@TargetApi(14)
private static void injectBelowApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader();
DexClassLoader dexClassLoader =
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());
dexClassLoader.loadClass(str2);
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
"mRawDexPath")
));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
"mFiles")
));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
"mZips")));
setField(obj, PathClassLoader.class, "mDexs",
combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
"mDexs")));
obj.loadClass(str2);
}
private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
Object a2 = getPathList(pathClassLoader);
//新的dexElements对象重新设置回去
setField(a2, a2.getClass(), "dexElements", a);
pathClassLoader.loadClass(str2);
}
/**
* 通过反射先获取到pathList对象
* @param obj
* @return
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 从上面获取到的PathList对象中,进一步反射获得dexElements对象
* @param obj
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//设置为可访问
return declaredField.get(obj);
}
private static void setField(Object obj, Class cls, String str, Object obj2)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//设置为可访问
declaredField.set(obj, obj2);
}
//合拼dexElements
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
int length = Array.getLength(obj2);
int length2 = Array.getLength(obj) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
Array.set(newInstance, i, Array.get(obj, i - length));
}
}
return newInstance;
}
private static Object appendArray(Object obj, Object obj2) {
Class componentType = obj.getClass().getComponentType();
int length = Array.getLength(obj);
Object newInstance = Array.newInstance(componentType, length + 1);
Array.set(newInstance, 0, obj2);
for (int i = 1; i < length + 1; i++) {
Array.set(newInstance, i, Array.get(obj, i - 1));
}
return newInstance;
}
}
四、APP调用Patch
public void onClick(View v){
if(v.getId() == R.id.btn_show){
Bugs.showToast(this);
}else if(v.getId() == R.id.btn_fix){
String dexPath = Environment.getExternalStorageDirectory()+"/patch.dex";
HotFix.patch(this, dexPath,"com.lsy.hotfix.Bugs");
System.out.println("已修复"+dexPath);
}
}
到此结束!