其实关于为什么会产生热修复,热修复怎么用,这些网上一大篇博客,本篇博客的内容是 :手写一个Android热修复Demo
要手写Demo ,首先需要掌握 两点:反射和ClassLoader。关于ClassLoader,不了解的可以看下上篇博客 Android中的类加载器——ClassLoader。
反射知识,可以看下这个博客:Android 插件化开发——基础底层知识(反射), 这里直接贴出我们Demo 中需要用到的反射工具类:
/**
* FileName:SharedReflectUtils
* Create By:liumengqiang
* Description:TODO
*/
public class SharedReflectUtils {
/**
* 反射获取一个属性
*
* @param instance PathClasLoader
* @param name
* @return
*/
public static Field getField(Object instance, String name) throws NoSuchFieldException {
for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) {
try {
Field declaredField = cls.getDeclaredField(name);
//设置权限
declaredField.setAccessible(true);
return declaredField;
} catch (NoSuchFieldException e) {
}
}
throw new NoSuchFieldException("Field:" + name + " not found in " + instance.getClass());
}
/**
* 反射获取一个方法
*
* @param instance PathClasLoader
* @param name
* @return
*/
public static Method getMethod(Object instance, String name,Class<?>... parameterTypes) throws NoSuchMethodException {
for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) {
try {
Method declaredMethod = cls.getDeclaredMethod(name,parameterTypes);
//设置权限
declaredMethod.setAccessible(true);
return declaredMethod;
} catch (NoSuchMethodException e) {
}
}
throw new NoSuchMethodException("Method:" + name + " not found in " + instance.getClass());
}
}
这个工具类 的作用是:递归自己以及自己的父类查找方法或者属性,如果找到 就返回。
热修复核心原理
其实说到热修复原理,上一篇博客,说到Element的时候,我们捎带提了一下,现在重新 捋一下思路:
loadClass查找类的时候 ,不论是PathClassLoader还是DexClassLoader都会走其父类的BaseDexClassLoader构造方法,而在其构造方法内创建了一个变量:pathList:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
//初始化pathList
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}
而真正调用loadClass的时候,会执行抽象类ClassLoader.java的loadClass的方法,先查找缓存,再从查找父亲,最后再调用自己的findClass方法查找,而在ClassLoader中,findClass方法里面 就抛出了个异常,把实现交给了子类,所以在BaseDexClassLoader中,看下findClass方法:
@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;
}
可以看到,实际上调用的 是pathList的findClass方法,在这个方法内,维护了个Element数组,里面 存储的 是DexFile文件,我们的APk打包的时候会生成dex文件,而APK安装的时候,会将APK中的dex文件优化转换成dexopt, 而Element 中存储的就是经过优化后的文件:DexFile,
public Class<?> findClass(String name, List<Throwable> suppressed) {
//遍历dexElements,
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
遍历Element数组,从前往后遍历,如果找到了class,那么就直接返回,不管之后是否还存在第二个相同的class。如下图所示:
那么,此时我们可以猜想,假如我们的APK中的a文件出了问题,那么我们可以修改a文件,然后打成补丁包patch .dex,放入到Element的最前面,那么当APP运行的时候,从前往后查找,这样的话就先查找我们的额patch.dex文件,那么就会找到a.class ,这样的话就直接返回,不再继续查找。这样我们的BUG就修复了,因为获得到的是修复之后的a.class!
怎样把生成的补丁包插入到Element数组中?
关于这个问题,在捋一下loadClass流程
- 首先通过递归反射获取到pathList变量,
- 然后在通过反射获取到pathList中的Element数组
- 将我们的 patch.dex 或者 path.jar通过反射调用pathList的makeDexElements方法,获取到新的Element数组
- 将新老数组Element合并成一个新的Element数组replaceArray
- 在将数组replaceArray塞回到pathList对象中。
好,总体流程就是上面的,那么可能有的小伙伴有疑问了:makeDexElements方法是什么鬼?
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
........
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
.......
}
可以看到通过调用makeDexElements方法创建了dexElements对象(也就是Element数组)。
注意:Android6.0之前是:makePathElements ; 而Android6.0之前是:makeDexElements方法创建Element数组,因此 我们创建Element的时候,要不同Android版本,不同处理!
开始手撸代码
首先我们需要创建一个加载补丁包patch.jar的工具类:
/**
* 执行修复BUG
*/
private static void installPatch23(Context context, String path) {
//获取私有目录缓存
File cacheDir = context.getCacheDir();
ClassLoader classLoader = context.getClassLoader();
try{
//获取ClassLoader的pathList属性
Field pathListField = SharedReflectUtils.getField(classLoader, "pathList");
Object pathList = pathListField.get(classLoader);
//获取pathList 中的dexElements数组
Field dexElementsField = SharedReflectUtils.getField(pathList, "dexElements");
Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
Method makePathElementsMethod = SharedReflectUtils.getMethod(pathList, "makePathElements", List.class, File.class, List.class);
ArrayList<File> files = new ArrayList<>();
files.add(new File(path));
//把新的补丁包通过makePathElementsMethod方法转化成Element数组
Object[] newDexElements = (Object[]) makePathElementsMethod.invoke(null, files, cacheDir, new ArrayList<IOException>());
//创建新的数组
Object[] replaceElements = (Object[]) Array.newInstance(oldDexElements.getClass().getComponentType(), oldDexElements.length + newDexElements.length);
//拷贝补丁包 数组和老数组,复制到新数组中,其中补丁包 数组要插入到新数组的 第零个
System.arraycopy(newDexElements, 0, replaceElements, 0, newDexElements.length);
System.arraycopy(oldDexElements, 0, replaceElements, newDexElements.length, oldDexElements.length);
//重新设置pathList属性中的Elements数组
dexElementsField.set(pathList, replaceElements);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
可以看到上述的代码顺序主要就是上一节分析的步骤。至此我们的 加载补丁包的方法已经写好了。我们什么时候加载 补丁包呢? 答案是在自定义Application的attachBaseContext中初始化的,因为attachBaseContext方法执行的时间比onCreate早 :
public class BaseApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
EnjoyFix.installPatch23(this, "/sdcard/patch.jar");
}
}
注意:这里我把补丁包放在sdcard根目录了,当然了这个目录可以自己定义。
还有一点就是要注意打开存储权限!!!
然后创建一个测试类 :
/**
* FileName:Test
* Create By:liumengqiang
* Description:TODO
*/
public class Test {
private static final String TAG = "Test";
public static int test() {
return 1/0;
}
}
然后我们在MainActivity中调用Test的test方法:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Test.test();
}
}
这样的话,然后 运行,看一下, 崩溃了:
这样的话 我们修改test方法:
/**
* FileName:Test
* Create By:liumengqiang
* Description:TODO
*/
public class Test {
private static final String TAG = "Test";
public static int test() {
return 1;
}
}
这样的话代码 就可以了,然后使用点击Build -> make module ‘app’, 用于编译生成 class文件 。然后我们使用
dx --dex --output=output.dex input.class
生成补丁包 ,
dx: 我们的SDK目录下的文件:比如我的是:
C:\Users\mayn\AppData\Local\Android\Sdk\build-tools\28.0.3\dx
** --dex** 制定是生成dex文件
output.dex: 输出的补丁包名称
input.class 就是你要生成补丁包的 class类文件 或者目录,
例如:我这里执行的是:
C:\Users\mayn\AppData\Local\Android\Sdk\build-tools\28.0.3\dx --dex --output = patch.jar com/bitcoin/juwan/androidfixproject/Test.class
然后拷贝patch.jar 放入 /sdcard/目录下 , 此时执行我们的app:
修复成功!
至此手写热修复就算完成了,文中如有错误,欢迎指正。
总结:实际上热修复的核心原理 就是ClassLoader加载机制,通过替换Element数组,达到修复BUG的目的。