一般产品上线周期比较长,而且如果不是强制更新,无法做到100%的用户都更新,如果上线之后,产品出现bug,那么怎么办?一般都是再发一个版本,或者等到下一个版本再解决。如果再发一个版本,显然是不靠谱的,用户安装也有厌倦的时候,说不定直接把app给卸载了,而且给用户的体验也不好。如果等到下一个版本再修复,那么用户每次使用都出现这个bug,显然是不能接受的,这都造成用户量流失,给我们用户造成不良的影响。
那么有没有一种办法是不需要发布版本,不需要用户安装,而且又能够及时觉得这个问题的呢?当然是有的,那就是使用热修复技术。
热修复技术一般来说有两种途径来解决:
1、阿里系的 AndFix,原理是从底层C的二进制来入手的。
2、腾讯系的 tinker,原理是Java类加载机制来入手的。 当然热修复技术不单单这两种,各大公司都开源自己的热修复技术,如滴滴的 VirtualAPK,美团的Robust等等。
既然有那么多的热修复项目开源,读者可以下载下来学习,看看实现的原理,本文主要是讲解从Java类加载器机制来讲解热修复。
首先我们理解DexClassLoader和PathClassLoader,他们都是用来加载应用程序的dex文件,但DexClassLoader是指可以加载指定的某个dex文件,而且具有限制性,那就是必须要在应用程序的目录下面的dex文件。
dexPath:包含classes和resources的jar/apk文件的路径; libraryPath:包含本地的目录列表,可以为null; parent:父类加载器。
dexPath:包含classes和resources的jar/apk文件的路径; optimizedDirectory:写入优化的dex文件的目录,一定不能是为空; libraryPath:包含本地的目录列表,可以为null; parent:父类加载器。
DexClassLoader和PathClassLoader类中都只有构造方法,而且构造方法都super到父类了。我们看下BaseDexClassLoader的类加载器的构造方法。
BaseDexClassLoader类中包括构造方法,findClass(找到类)、findResourece,findLibrary等等方法,这里主要看下构造方法。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
复制代码
主要这里new了一个DexPathList对象,并且把参数传过去了,DexPathList到底是干什么的呢?我们去探个究竟。
类的简介中我们知道,DexPathList其实就是与类加载器(ClassLoader)相关联的一对条目列表。而这些列表包含了dex、jar、zip和apk
文件,而提高了使用列表来查找类和资源的方法。这里需要说一下的是平时我们看源码一定要主要看作者的注释,从注释里我们看知道类或者方法的功能。
从DexPathList源码中我们注意到一个属性就是dexElements
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;
复制代码
这个就是存放dex/resource的集合,我们注意到该类的findClass方法:
findClass的时候就是遍历dexElements,取出DexFile,找到我们需要的类并返回。也就是说我们可以通过dexElements找到我们出bug的类,因为dexElements就是存放Element元素,而Element中包含DexFile,DexFile中可以找到bug类,找到bug类,然后我们替换掉bug类所在的dex文件就可以了。而且再dexElements介绍中,已经说明了可以通过反射获取得到dexElements。
那么我们思考一下:既然DexClassLoader和PathClassLoader都可以加载dex文件,那么我们能不能使用多个dex文件?也就是第一个版本使用的是classes.dex,修复后的补丁包classes2.dex,在补丁包中包涵了我们修复class文件。然后将这两个文件合并,将修复的class替换原来出bug的class,接下来通过反射拿到dexElements,将修复好的dex插入到dexElements的集合,插入的位置就是出现bug的class所在的dex
的前面。这样子做最本质的实现原理就是:类加载器去加载某个类的时候,是去dexElements里面从头往下查找的。
需要多个dex的需要multidex支持。 1、再app的build.gradle中添加
defaultConfig {
multiDexEnabled true
}
buildTypes {
release {
multiDexKeepFile file('dex.keep')
println "dex keep"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
复制代码
2、在application的attachBaseContext添加MultiDex.install(base);
据上面的原理,下面是实现的热修复的代码
FixDexUtils:
public class FixDexUtils {
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(MyConstants.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合并之前的dex
doDexInject(context,fileDir,loadedDex);
}
//使用合并的dexElements替换出错的dexElements
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);
}
// fileDir = {File@15225} "/data/data/com.dex.main/app_odex"
// 0 = {File@15240} "/data/data/com.dex.main/app_odex/opt_dex"
// 1 = {File@15241} "/data/data/com.dex.main/app_odex/classes2.dex"
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();
}
try {
//1.使用PathClassLoader加载应用程序的dex(系统的加载器)
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
//2.通过DexClassLoader加载指定的修复的dex文件。
DexClassLoader classLoader = new DexClassLoader(
dex.getAbsolutePath(),//String dexPath,
fopt.getAbsolutePath(),//String optimizedDirectory,读取文件优化的目录
null,//String libraryPath,
pathLoader//ClassLoader parent
);
//3.合并
Object dexObj = getPathList(classLoader);
Object pathObj = getPathList(pathLoader);
Object mDexElementsList = getDexElements(dexObj);
Object pathDexElementsList = getDexElements(pathObj);
//合并完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重写给PathList里面的lement[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"dexElements");
}
/**
* 两个数组合并
* 有bug的dex还是保留,只是把没bug的插入到有bug的前面即可
* @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);//新建一个数组,总长度为两个数组的总和,即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;//返回总的数组
}
}
复制代码
MyApplication:
public class MyApplication extends Application{
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
// TODO Auto-generated method stub
MultiDex.install(base);
FixDexUtils.loadFixedDex(base);
super.attachBaseContext(base);
}
}
复制代码
MyConstants:
public class MyConstants {
public static final String DEX_DIR = "odex";
}
复制代码
MyTestClass:
public class MyTestClass {
public void testFix(Context context){
int i = 10;
int a = 0;
Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
}
}
复制代码
MainActivity:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void test(View v){
MyTestClass myTestClass = new MyTestClass();
myTestClass.testFix(this);
}
public void fix(View v){
fixBug();
}
private void fixBug() {
//目录:/data/data/packageName/odex
File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
//往该目录下面放置我们修复好的dex文件。
String name = "classes2.dex";
String filePath = fileDir.getAbsolutePath()+File.separator+name;
File file= new File(filePath);
if (fileDir.exists()) {
if (file.exists()) {
file.delete();
}
}
//搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
InputStream is = null;
FileOutputStream os = null;
try {
String dpath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator;
is = new FileInputStream(dpath+name);
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len=is.read(buffer))!=-1){
os.write(buffer,0,len);
}
File f = new File(filePath);
if(f.exists()){
Toast.makeText(this ,"dex 重写成功", Toast.LENGTH_SHORT).show();
}
//热修复
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
activity_main:
<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">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="test"
android:onClick="test"
android:layout_centerInParent="true"
android:id="@+id/btn_test"
/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="fix"
android:onClick="fix"
android:layout_below="@id/btn_test"
/>
</RelativeLayout>
复制代码
编译dex文件
1、修复好bug,然后rebuild一下,获取java文件对应的所有class文件(即找到修复好的java文件所对应的calss文件):app\build\intermediates\classes\debug\包路径\类名字,然后右键show in Explorer。把文件复制出来,复制的时候连同整个包名路径(即带包名的整个文件夹)都复制出来。(也可以找到class文件之后选择压缩,注意压缩时选择绝对路径,再解压可保留原有的包目录)
2、cmd到sdk的build-tools\sdk版本
目录下。
3、使用命令dx --dex --output=D:\Users\dex\classes2.dex D:\Users\dex
命令解释: --output=D:\Users\dex\classes2.dex 指定输出路径和文件名 D:\Users\dex` 最后指定去打包哪个目录下面的class字节文件(注意:要包括全路径的文件夹,也可以有多个class)
可以看到我这个dex文件夹下有包含全包名路径的文件夹,该文件夹里面有class文件。
当我们点击第一次进来点击test的时候,会闪退,第二次进来,我们点击fix时候,已经把bug修复好了,这时候就不会闪退了。
参考文章: 《Android4.4.2 DexClassLoader源码分析》
BaseDexClassLoader源码连接:BaseDexClassLoader
DexPathList源码连接:DexPathList