上一篇博客中,我们通过介绍dex分包原理引出了Android的热补丁技术,而现在我们将解决两个问题。
1. 怎么将修复后的Bug类打包成dex
2. 怎么将外部的dex插入到ClassLoader中
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="click" android:text="小喵叫一声"/>
</RelativeLayout>
MainActivity.class
package com.aitsuki.bugfix;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.aitsuki.bugfix.animal.Cat;
public class MainActivity extends AppCompatActivity {
private Cat mCat;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCat = new Cat();
}
public void click(View view) {
Toast.makeText(this, mCat.say(),Toast.LENGTH_SHORT).show();
}
}
Cat.class
package com.aitsuki.bugfix.animal;
/** * Created by AItsuki on 2016/3/14. */
public class Cat {
public String say() {
return "汪汪汪!";
}
}
假设这是我们公司的开发项目,刚刚上线就发现了严重bug,猫会狗叫。
想修复bug,让用户再立刻更新一次显然很不友好,此时热补丁修复技术就有用了。
在加载dex的代码之前,我们先来制作补丁。
1. 首先我们将Cat类修复,汪汪汪改成喵喵喵,然后重新编译项目。(Rebuild一下就行了)
2. 去保存项目的地方,将Cat.class文件拷贝出来,在这里
3. 新建文件夹,要和该Cat.class文件的包名一致,然后将Cat.class复制到这里,如图
4. 命令行进入到图中的test目录,运行一下命令,打包补丁。如图:
然后test目录是这样的
patch_dex.jar就是我们打包好的补丁了,我们将它放到sdCard中,待会从这里加载补丁。
关于什么用这么复杂的方法打包补丁的说明:
你也可以直接将java文件拷出来,通过javac -d带包编译再转成jar。
但我这么麻烦是有原因的,因为用这种方法你可能会遇到ParseException,原因是jar包版本和dx工具版本不一致。
而从项目中直接将编译好的class直接转成jar就没问题,因为java会向下兼容,打出来的jar包和class版本是一致的。
总而言之,dx版本要和class编译版本对应。
通过上一篇博文,我们知道dex保存在这个位置
BaseDexClassLoader–>pathList–>dexElements
加载外部dex,我们可以在Application中操作。
首先新建一个HotPatchApplication,然后在清单文件中配置,顺便加上读取sdcard的权限,因为补丁就保存在那里。
HotPatchApplication代码如下:
package com.aitsuki.hotpatchdemo;
import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
/** * Created by hp on 2016/4/6. */
public class HotPatchApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 获取补丁,如果存在就执行注入操作
String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
File file = new File(dexPath);
if (file.exists()) {
inject(dexPath);
} else {
Log.e("BugFixApplication", dexPath + "不存在");
}
}
/** * 要注入的dex的路径 * * @param path */
private void inject(String path) {
try {
// 获取classes的dexElements
Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
Object pathList = getField(cl, "pathList", getClassLoader());
Object baseElements = getField(pathList.getClass(), "dexElements", pathList);
// 获取patch_dex的dexElements(需要先加载dex)
String dexopt = getDir("dexopt", 0).getAbsolutePath();
DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
Object obj = getField(cl, "pathList", dexClassLoader);
Object dexElements = getField(obj.getClass(), "dexElements", obj);
// 合并两个Elements
Object combineElements = combineArray(dexElements, baseElements);
// 将合并后的Element数组重新赋值给app的classLoader
setField(pathList.getClass(), "dexElements", pathList, combineElements);
//======== 以下是测试是否成功注入 =================
Object object = getField(pathList.getClass(), "dexElements", pathList);
int length = Array.getLength(object);
Log.e("BugFixApplication", "length = " + length);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/** * 通过反射获取对象的属性值 */
private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
Field field = cl.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
/** * 通过反射设置对象的属性值 */
private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = cl.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
/** * 通过反射合并两个数组 */
private Object combineArray(Object firstArr, Object secondArr) {
int firstLength = Array.getLength(firstArr);
int secondLength = Array.getLength(secondArr);
int length = firstLength + secondLength;
Class<?> componentType = firstArr.getClass().getComponentType();
Object newArr = Array.newInstance(componentType, length);
for (int i = 0; i < length; i++) {
if (i < firstLength) {
Array.set(newArr, i, Array.get(firstArr, i));
} else {
Array.set(newArr, i, Array.get(secondArr, i - firstLength));
}
}
return newArr;
}
}
运行一下Demo,报以下错误。(AndroidStudio 2.0可能不会报错,需要打包的时候才会出现错误,这是Instant run导致的)
dexElements的length = 2,看来我们的patch_dex已经成功添加进去了。
但是从黄色框框和黄色框上面那一段log提示中可以看出,MainActivity引用了Cat,但是发现他们在不同的Dex中。
看到这里可能就会问:
为什么之前那么多项目都采用分包方案,但是却不会出现这个错误呢?
我在这里总结了一个过程,想知道详细分析过程的请看QQ空间开发团队的原文。
根据上面的第六条,我们只要让所有类都引用其他dex中的某个类就可以了。
System.out.println(AntilazyLoad.class);
System.out.println(AntilazyLoad.class);
这行代码,因为此时hack.dex还没有加载进来,AntilazyLoad并不存在。其实整个热补丁技术最难的地方不是原理,不是注入dex,而是字节码的注入。
这需要我们队Gradle构建脚本,Groovy语言有一定的了解。其中的知识量实在是太过庞大,这里推荐几篇博文预习一下。
Gradle学习系列之一——Gradle快速入门
深入理解Android之Gradle——by 阿拉神农
ps:有些朋友可能会发现我的一些图片存在问题…… 比如运行结果那张图,标题是Bugfix。
命令行那张图,进的是blog目录……
因为研究这个热补丁技术的周期比较长,而且是一边写博客,所以有些图片弄错了……