Android熱補丁動態修復實踐


前言

好幾個月之前關於Android App熱補丁修復火了一把,源於QQ空間團隊的一篇文章安卓App熱補丁動態修復技術介紹,然后各大廠的開源項目都出來了,本文的實踐基於HotFix,也就是QQ空間技術團隊那篇文章所應用的技術,筆者會把整個過程的細節和思路在文章中詳說,研究這個的出發點也是為了能緊急修復app的bug,而不需要重復發包,不需要用戶重新下載app就能把問題解決,個人覺得這個還是蠻有價值的,雖然老板不知道….。

項目結構

項目結構

這里筆者創建一個新的項目”HotFixDemo”,帶大家一步一步來完成Hotfix這個框架實現,這個項目包含以下module:
- app :我們的Android應用程序Module。
- buildsrc :使用Groovy實現的項目,提供了一個類,用來實現修改class文件的操作。
- hackdex :提供了一個類,后面會用來打包成hack.dex,也是buildsrc里面實現在所有類的構造函數插入的一段代碼所引用到的類。
- hotfixlib :這個module最終會被app關聯,里面提供實現熱補丁的核心方法

這個Demo里面的代碼跟HotFix框架基本無異,主要是告訴大家它實現的過程,如果光看代碼,不實踐是無法把它應用到你自己的app上去的,因為有很多比較深入的知識需要你去理解。

先說原理

關於實現原理,QQ空間那篇文章已經說過了,這里我再重新闡述一遍:
- Android使用的是PathClassLoader作為其類的加載器
- 一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex排列成一個有序的dexElements數組
- 當找類的時候會遍歷dexElements數組,從dex文件中找類,找到則返回,否則繼續下一個dex文件查找
- 熱補丁的方案,其實就是將有問題的類單獨打包成一個dex文件(如:patch.dex),然后將這個dex插入到dexElements數組的最前面去。

ok,這個就是HotFix對app進行熱補丁的原理,其實就是用ClassLoader加載機制,覆蓋掉有問題的方法,然后我們所謂的補丁就是將有問題的類打成一個包

再說問題

當然要實現熱補丁動態修復不會很容易,我們首要解決的一個問題是:

當虛擬機啟動時,當verify選項被打開時,如果static方法、private方法、構造函數等,其中的直接引用(第一層關系)到的類都在同一個dex文件中,那么該類會被打上CLASS_ISPREERIFIED標記

如下圖所示:
CLASS_ISPREERIFIED

如果一個類被打上了CLASS_ISPREERIFIED這個標志,如果該類引用的另外一個類在另一個dex文件,就會報錯。簡單來說,就是你在打補丁之前,你所修復的類已經被打上標記,你通過補丁去修復bug的時候這個時候你就不能完成校驗,就會報錯。

解決問題

要解決上一節所提到的問題就要在apk打包之前就阻止相關類打上CLASS_ISPREERIFIED標志,解決方案如下:
在所有類的構造函數插入一段代碼,如:

public class BugClass {
public BugClass() {
System.out.println(AntilazyLoad.class);
}

public String bug() {
return "bug class";
}
}

其中引用到的AntilazyLoad這個類會單獨打包成hack.dex,這樣當安裝apk的時候,classes.dex內的類都會引用一個不相同的dex中的AntilazyLoad類,這樣就解決CLASS_ISPREERIFIED標記問題了。

實現細節

上面幾節講完原理、之后拋出了問題,再提出解決方案,相信大家對整個熱補丁修復框架有了一定的認識,至少我們知道它到底是怎么一回事。下面來講實現細節:

創建兩個類

package com.devilwwj.hotfixdemo;

/**
* com.devilwwj.hotfixdemo
* Created by devilwwj on 16/3/8.
*/

public class BugClass {

public String bug() {
return "bug class";
}
}
package com.devilwwj.hotfixdemo;

/**
* com.devilwwj.hotfixdemo
* Created by devilwwj on 16/3/8.
*/

public class LoadBugClass {
public String getBugString() {
BugClass bugClass = new BugClass();
return bugClass.bug();
}
}

我們需要做的是在這兩個類的class文件的構造方法中插入一段代碼:

System.out.println(AntilazyLoad.class);

創建hackdex模塊並創建AntilazyLoad類

看圖就好了:

hackdex模塊

將AntilazyLoad單獨打成hack_dex.jar包

通過以下命令來實現:

jar cvf hack.jar com.devilwwj.hackdex/*

這個命令會將AntilazyLoad類打包成hack.jar文件

dx --dex --output hack_dex.jar hack.jar

這個命令使用dx工具對hack.jar進行轉化,生成hack_dex.jar文件

dx工具在我們的sdk/build-tools下
dx工具

最終我們把hack_dex.jar文件放到項目的assets目錄下:

hack_dex.jar

使用javassist實現動態代碼注入

創建buildSrc模塊,這個項目是使用Groovy開發的,需要配置Groovy SDK才可以編譯成功。
在這里下載Groovy SDK:http://groovy-lang.org/download.html,下載之后,配置項目user Library即可。

它里面提供了一個方法,用來向指定類的構造函數注入代碼:

 /**
* 植入代碼
* @param buildDir 是項目的build class目錄,就是我們需要注入的class所在地
* @param lib 這個是hackdex的目錄,就是AntilazyLoad類的class文件所在地
*/

public static void process(String buildDir, String lib) {
println(lib);
ClassPool classes = ClassPool.getDefault()
classes.appendClassPath(buildDir)
classes.appendClassPath(lib)

// 將需要關聯的類的構造方法中插入引用代碼
CtClass c = classes.getCtClass("com.devilwwj.hotfixdemo.BugClass")
if (c.isFrozen()) {
c.defrost()
}
println("====添加構造方法====")
def constructor = c.getConstructors()[0];
constructor.insertBefore("System.out.println(com.devilwwj.hackdex.AntilazyLoad.class);")
c.writeFile(buildDir)

CtClass c1 = classes.getCtClass("com.devilwwj.hotfixdemo.LoadBugClass")
if (c1.isFrozen()) {
c1.defrost()
}
println("====添加構造方法====")
def constructor1 = c1.getConstructors()[0];
constructor1.insertBefore("System.out.println(com.devilwwj.hackdex.AntilazyLoad.class);")
c1.writeFile(buildDir)

}

配置app項目的build.gradle

上一小節創建的module提供相應的方法來讓我們對項目的類進行代碼注入,我們需要在build.gradle來配置讓它自動來做這件事:

apply plugin: 'com.android.application'

task('processWithJavassist') << {
String classPath = file('build/intermediates/classes/debug')// 項目編譯class所在目錄
com.devilwwj.patch.PatchClass.process(classPath, project(':hackdex').buildDir.absolutePath + "/intermediates/classes/debug") // 第二個參數是hackdex的class所在目錄
}

android {
compileSdkVersion 23
buildToolsVersion "23.0.1"

defaultConfig {
applicationId "com.devilwwj.hotfixdemo"
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
applicationVariants.all { variant ->
variant.dex.dependsOn << processWithJavassist // 在執行dx命令之前將代碼打入到class
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile project(':hotfixlib')
}

這時候我們run項目,反編譯build/output/apk下的app-debug.apk文件,你就可以看到代碼已經成功植入了。

mac下的反編譯工具:
https://sourceforge.net/projects/jadx/?source=typ_redirect

反編譯的結果如下圖:

反編譯結果

其實你也可以直接在項目中看:

代碼注入結果

創建hotfixlib模塊,並關聯到項目中

這差不多是最后一步了,也是最核心的一步,提供將heck_dex.jar動態插入到dexElements的方法。

核心代碼:

package com.devilwwj.hotfixlib;

import android.annotation.TargetApi;
import android.content.Context;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

/**
* com.devilwwj.hotfixlib
* Created by devilwwj on 16/3/9.
*/

public final class HotFix {
public static void patch(Context context, String patchDexFile, String patchClassName) {
if (patchDexFile != null && new File(patchDexFile).exists()) {
try {
if (hasLexClassLoader()) {
injectAliyunOs(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) {
e.printStackTrace();
return false;
}

}

private static boolean hasDexClassLoader() {
try {
Class.forName("dalvik.system.BaseDexClassLoader");
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
}
}

private static void injectAliyunOs(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);
}

/**
* 將dex注入dexElements數組中
* @param context
* @param str
* @param str2
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/

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);
setField(a2, a2.getClass(), "dexElements", a);
pathClassLoader.loadClass(str2);
}

/**
* 通過PathClassLoader拿到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");
}


/**
* 通過反射拿到指定對象
* @param obj
* @param cls
* @param str
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/

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);
}


/**
* 通過反射設置屬性
* @param obj
* @param cls
* @param str
* @param obj2
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/

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);
}

/**
* 合並數組
* @param obj
* @param obj2
* @return
*/

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;
}

/**
* 添加到數組
* @param obj
* @param obj2
* @return
*/

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 = 0; i < length + 1; i++) {
Array.set(newInstance, i, Array.get(obj, i - 1));
}
return newInstance;
}
}

准備補丁,最后測試結果

補丁是我們程序修復bug的包,如果我們已經上線的包出現了bug,你需要緊急修復,那你就找到有bug的那個類,將它修復,然后將這個修復的class文件打包成jar包,讓服務端將這個補丁包放到指定位置,你的就程序就可以將這補丁包下載到sdcard,之后就是程序自動幫你打補丁把問題修復。

比如我們上面提到的BugClass:
未修復之前:

public class BugClass {

public String bug() {
return "bug class";
}
}

修復之后:

public class BugClass {

public String bug() {
return "小巫將bug修復啦!!!";
}
}

你要做的就是替換這個類,怎么做?

先打包:

打包命令

記住:一定要經過dx工具轉化,然后路徑一定要對

patch_dex.jar就是我們的補丁包,這里我們為了演示把它放到項目的assets目錄下:

補丁包

之后,就是測試效果了,看動態圖:

打補丁過程

好,到這里就大公告成了,我們的bug被修復了啦。

總結

本次實踐過程是基於HotFix框架,在這里感謝開源的作者,因為不滿足於拿作者的東西直接用,然后不知道為什么,所以筆者把整個過程都跑了一遍,正所謂實踐出真知,原本以為很難的東西通過反復實踐也會變得不那么難,期間實踐自然不會那么順利,筆者就遇到一個坑,比如Groovy編譯,hack_dex包中的類找不到等等,但最后都一一解決了,研究完這個熱更新框架,再去研究其他熱更新框架也是一樣的,因為原理都一樣,所以就不糾結研究哪個了,之后筆者也會把這個技術用到項目中去,不用每次發包也是心情愉悅的,最后感謝各位看官耐心看,有啥問題就直接留言吧。

參考:
http://blog.csdn.net/lmj623565791/article/details/49883661
http://www.jianshu.com/p/56facb3732a7

1元表真心


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com