好幾個(gè)月之前關(guān)于AndroidApp熱補(bǔ)丁修復(fù)火了一把代态,源于QQ空間團(tuán)隊(duì)的一篇文章安卓App熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)介紹呐萌,然后各大廠的開(kāi)源項(xiàng)目都出來(lái)了,本文的實(shí)踐基于HotFix涎才,也就是QQ空間技術(shù)團(tuán)隊(duì)那篇文章所應(yīng)用的技術(shù)溪食,筆者會(huì)把整個(gè)過(guò)程的細(xì)節(jié)和思路在文章中詳說(shuō),研究這個(gè)的出發(fā)點(diǎn)也是為了能緊急修復(fù)app的bug区岗,而不需要重復(fù)發(fā)包略板,不需要用戶(hù)重新下載app就能把問(wèn)題解決,個(gè)人覺(jué)得這個(gè)還是蠻有價(jià)值的慈缔,雖然老板不知道….。
這里筆者創(chuàng)建一個(gè)新的項(xiàng)目”HotFixDemo”种玛,帶大家一步一步來(lái)完成Hotfix這個(gè)框架實(shí)現(xiàn)藐鹤,這個(gè)項(xiàng)目包含以下module:
app :我們的Android應(yīng)用程序Module瓤檐。
buildsrc :使用Groovy實(shí)現(xiàn)的項(xiàng)目,提供了一個(gè)類(lèi)娱节,用來(lái)實(shí)現(xiàn)修改class文件的操作挠蛉。
hackdex :提供了一個(gè)類(lèi),后面會(huì)用來(lái)打包成hack.dex肄满,也是buildsrc里面實(shí)現(xiàn)在所有類(lèi)的構(gòu)造函數(shù)插入的一段代碼所引用到的類(lèi)谴古。
hotfixlib :這個(gè)module最終會(huì)被app關(guān)聯(lián),里面提供實(shí)現(xiàn)熱補(bǔ)丁的核心方法
這個(gè)Demo里面的代碼跟HotFix框架基本無(wú)異稠歉,主要是告訴大家它實(shí)現(xiàn)的過(guò)程掰担,如果光看代碼,不實(shí)踐是無(wú)法把它應(yīng)用到你自己的app上去的怒炸,因?yàn)橛泻芏啾容^深入的知識(shí)需要你去理解带饱。
關(guān)于實(shí)現(xiàn)原理,QQ空間那篇文章已經(jīng)說(shuō)過(guò)了阅羹,這里我再重新闡述一遍:
Android使用的是PathClassLoader作為其類(lèi)的加載器
一個(gè)ClassLoader可以包含多個(gè)dex文件勺疼,每個(gè)dex文件是一個(gè)Element,多個(gè)dex排列成一個(gè)有序的dexElements數(shù)組
當(dāng)找類(lèi)的時(shí)候會(huì)遍歷dexElements數(shù)組捏鱼,從dex文件中找類(lèi)执庐,找到則返回,否則繼續(xù)下一個(gè)dex文件查找
熱補(bǔ)丁的方案导梆,其實(shí)就是將有問(wèn)題的類(lèi)單獨(dú)打包成一個(gè)dex文件(如:patch.dex)耕肩,然后將這個(gè)dex插入到dexElements數(shù)組的最前面去。
ok问潭,這個(gè)就是HotFix對(duì)app進(jìn)行熱補(bǔ)丁的原理猿诸,其實(shí)就是用ClassLoader加載機(jī)制,覆蓋掉有問(wèn)題的方法狡忙,然后我們所謂的補(bǔ)丁就是將有問(wèn)題的類(lèi)打成一個(gè)包梳虽。
當(dāng)然要實(shí)現(xiàn)熱補(bǔ)丁動(dòng)態(tài)修復(fù)不會(huì)很容易,我們首要解決的一個(gè)問(wèn)題是:
當(dāng)虛擬機(jī)啟動(dòng)時(shí)灾茁,當(dāng)verify選項(xiàng)被打開(kāi)時(shí)窜觉,如果static方法、private方法北专、構(gòu)造函數(shù)等禀挫,其中的直接引用(第一層關(guān)系)到的類(lèi)都在同一個(gè)dex文件中,那么該類(lèi)會(huì)被打上CLASS_ISPREERIFIED標(biāo)記
如下圖所示:
如果一個(gè)類(lèi)被打上了CLASS_ISPREERIFIED這個(gè)標(biāo)志拓颓,如果該類(lèi)引用的另外一個(gè)類(lèi)在另一個(gè)dex文件语婴,就會(huì)報(bào)錯(cuò)。簡(jiǎn)單來(lái)說(shuō),就是你在打補(bǔ)丁之前砰左,你所修復(fù)的類(lèi)已經(jīng)被打上標(biāo)記匿醒,你通過(guò)補(bǔ)丁去修復(fù)bug的時(shí)候這個(gè)時(shí)候你就不能完成校驗(yàn),就會(huì)報(bào)錯(cuò)缠导。
要解決上一節(jié)所提到的問(wèn)題就要在apk打包之前就阻止相關(guān)類(lèi)打上CLASS_ISPREERIFIED標(biāo)志廉羔,解決方案如下:
在所有類(lèi)的構(gòu)造函數(shù)插入一段代碼,如:
public class BugClass {
public BugClass() {
System.out.println(AntilazyLoad.class);
}
public String bug() {
return "bug class";
}
}
其中引用到的AntilazyLoad這個(gè)類(lèi)會(huì)單獨(dú)打包成hack.dex僻造,這樣當(dāng)安裝apk的時(shí)候憋他,classes.dex內(nèi)的類(lèi)都會(huì)引用一個(gè)不相同的dex中的AntilazyLoad類(lèi),這樣就解決CLASS_ISPREERIFIED標(biāo)記問(wèn)題了髓削。
上面幾節(jié)講完原理竹挡、之后拋出了問(wèn)題,再提出解決方案蔬螟,相信大家對(duì)整個(gè)熱補(bǔ)丁修復(fù)框架有了一定的認(rèn)識(shí)此迅,至少我們知道它到底是怎么一回事。下面來(lái)講實(shí)現(xiàn)細(xì)節(jié):
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();
}
}
我們需要做的是在這兩個(gè)類(lèi)的class文件的構(gòu)造方法中插入一段代碼:
System.out.println(AntilazyLoad.class);
創(chuàng)建hackdex模塊并創(chuàng)建AntilazyLoad類(lèi)
看圖就好了:
將AntilazyLoad單獨(dú)打成hack_dex.jar包
通過(guò)以下命令來(lái)實(shí)現(xiàn):
jar cvf hack.jar com.devilwwj.hackdex/*
這個(gè)命令會(huì)將AntilazyLoad類(lèi)打包成hack.jar文件
dx --dex --output hack_dex.jar hack.jar
這個(gè)命令使用dx工具對(duì)hack.jar進(jìn)行轉(zhuǎn)化旧巾,生成hack_dex.jar文件
dx工具在我們的sdk/build-tools下
最終我們把hack_dex.jar文件放到項(xiàng)目的assets目錄下:
使用javassist實(shí)現(xiàn)動(dòng)態(tài)代碼注入
創(chuàng)建buildSrc模塊耸序,這個(gè)項(xiàng)目是使用Groovy開(kāi)發(fā)的,需要配置Groovy SDK才可以編譯成功鲁猩。
在這里下載Groovy SDK:http://groovy-lang.org/download.html坎怪,下載之后,配置項(xiàng)目user Library即可廓握。
它里面提供了一個(gè)方法搅窿,用來(lái)向指定類(lèi)的構(gòu)造函數(shù)注入代碼:
/**
* 植入代碼
* @param buildDir 是項(xiàng)目的build class目錄,就是我們需要注入的class所在地
* @param lib 這個(gè)是hackdex的目錄隙券,就是AntilazyLoad類(lèi)的class文件所在地
*/
public static void process(String buildDir, String lib) {
println(lib);
ClassPool classes = ClassPool.getDefault()
classes.appendClassPath(buildDir)
classes.appendClassPath(lib)
// 將需要關(guān)聯(lián)的類(lèi)的構(gòu)造方法中插入引用代碼
CtClass c = classes.getCtClass("com.devilwwj.hotfixdemo.BugClass")
if (c.isFrozen()) {
c.defrost()
}
println("====添加構(gòu)造方法====")
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("====添加構(gòu)造方法====")
def constructor1 = c1.getConstructors()[0];
constructor1.insertBefore("System.out.println(com.devilwwj.hackdex.AntilazyLoad.class);")
c1.writeFile(buildDir)
}
上一小節(jié)創(chuàng)建的module提供相應(yīng)的方法來(lái)讓我們對(duì)項(xiàng)目的類(lèi)進(jìn)行代碼注入男应,我們需要在build.gradle來(lái)配置讓它自動(dòng)來(lái)做這件事:
apply plugin: 'com.android.application'
task('processWithJavassist') << {
String classPath = file('build/intermediates/classes/debug')// 項(xiàng)目編譯class所在目錄
com.devilwwj.patch.PatchClass.process(classPath, project(':hackdex').buildDir.absolutePath + "/intermediates/classes/debug") // 第二個(gè)參數(shù)是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 // 在執(zhí)行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')
}
這時(shí)候我們r(jià)un項(xiàng)目,反編譯build/output/apk下的app-debug.apk文件娱仔,你就可以看到代碼已經(jīng)成功植入了沐飘。
mac下的反編譯工具:
https://sourceforge.net/projects/jadx/?source=typ_redirect
反編譯的結(jié)果如下圖:
其實(shí)你也可以直接在項(xiàng)目中看:
創(chuàng)建hotfixlib模塊,并關(guān)聯(lián)到項(xiàng)目中
這差不多是最后一步了牲迫,也是最核心的一步耐朴,提供將heck_dex.jar動(dòng)態(tài)插入到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數(shù)組中
* @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);
}
/**
* 通過(guò)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");
}
/**
* 通過(guò)pathList取得dexElements對(duì)象
* @param obj
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
/**
* 通過(guò)反射拿到指定對(duì)象
* @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);
}
/**
* 通過(guò)反射設(shè)置屬性
* @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);
}
/**
* 合并數(shù)組
* @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;
}
/**
* 添加到數(shù)組
* @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;
}
}
準(zhǔn)備補(bǔ)丁盹憎,最后測(cè)試結(jié)果
補(bǔ)丁是我們程序修復(fù)bug的包筛峭,如果我們已經(jīng)上線的包出現(xiàn)了bug,你需要緊急修復(fù)陪每,那你就找到有bug的那個(gè)類(lèi)影晓,將它修復(fù)镰吵,然后將這個(gè)修復(fù)的class文件打包成jar包,讓服務(wù)端將這個(gè)補(bǔ)丁包放到指定位置俯艰,你的就程序就可以將這補(bǔ)丁包下載到sdcard捡遍,之后就是程序自動(dòng)幫你打補(bǔ)丁把問(wèn)題修復(fù)锌订。
比如我們上面提到的BugClass:
未修復(fù)之前:
public class BugClass {
public String bug() {
return "bug class";
}
}
修復(fù)之后:
public class BugClass {
public String bug() {
return "小巫將bug修復(fù)啦V裎铡!辆飘!";
}
}
你要做的就是替換這個(gè)類(lèi)啦辐,怎么做?
先打包:
記昨谙睢:一定要經(jīng)過(guò)dx工具轉(zhuǎn)化芹关,然后路徑一定要對(duì)
patch_dex.jar就是我們的補(bǔ)丁包,這里我們?yōu)榱搜菔景阉诺巾?xiàng)目的assets目錄下:
之后紧卒,就是測(cè)試效果了侥衬,看動(dòng)態(tài)圖:
好,到這里就大公搞成了跑芳,我們的bug被修復(fù)了啦轴总。
本次實(shí)踐過(guò)程是基于HotFix框架,在這里感謝開(kāi)源的作者博个,因?yàn)椴粷M(mǎn)足于拿作者的東西直接用怀樟,然后不知道為什么,所以筆者把整個(gè)過(guò)程都跑了一遍盆佣,正所謂實(shí)踐出真知往堡,原本以為很難的東西通過(guò)反復(fù)實(shí)踐也會(huì)變得不那么難,期間實(shí)踐自然不會(huì)那么順利共耍,筆者就遇到一個(gè)坑虑灰,比如Groovy編譯,hack_dex包中的類(lèi)找不到等等痹兜,但最后都一一解決了穆咐,研究完這個(gè)熱更新框架,再去研究其他熱更新框架也是一樣的佃蚜,因?yàn)樵矶家粯佑褂椋跃筒患m結(jié)研究哪個(gè)了,之后筆者也會(huì)把這個(gè)技術(shù)用到項(xiàng)目中去谐算,不用每次發(fā)包也是心情愉悅的熟尉,最后感謝各位看官耐心看,有啥問(wèn)題就直接留言吧洲脂。
參考:
http://blog.csdn.net/lmj623565791/article/details/49883661
http://www.reibang.com/p/56facb3732a7