編輯:關於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這個標志,如果該類引用的另外一個類在另一個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);
看圖就好了:
通過以下命令來實現:
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下
最終我們把hack_dex.jar文件放到項目的assets目錄下:
創建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)
}
上一小節創建的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
反編譯的結果如下圖:
其實你也可以直接在項目中看:
這差不多是最後一步了,也是最核心的一步,提供將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工具轉化,然後路徑一定要對<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPnBhdGNoX2RleC5qYXK+zcrHztLDx7XEsrm2obD8o6zV4sDvztLDx86qwcvR3cq+sNHL/LfFtb3P7sS/tcRhc3NldHPEv8K8z8KjujwvcD4NCjxwPjxpbWcgYWx0PQ=="補丁包" src="http://www.bkjia.com/uploads/allimg/160316/042002D31-8.png" title="\" />
之後,就是測試效果了,看動態圖:
好,到這裡就大公告成了,我們的bug被修復了啦。
本次實踐過程是基於HotFix框架,在這裡感謝開源的作者,因為不滿足於拿作者的東西直接用,然後不知道為什麼,所以筆者把整個過程都跑了一遍,正所謂實踐出真知,原本以為很難的東西通過反復實踐也會變得不那麼難,期間實踐自然不會那麼順利,筆者就遇到一個坑,比如Groovy編譯,hack_dex包中的類找不到等等,但最後都一一解決了,研究完這個熱更新框架,再去研究其他熱更新框架也是一樣的,因為原理都一樣,所以就不糾結研究哪個了,之後筆者也會把這個技術用到項目中去,不用每次發包也是心情愉悅的,最後感謝各位看官耐心看,有啥問題就直接留言吧。
簡單回調機制的基本建立,簡單回調機制建立簡單回調機制的建立主要分為下面幾步: 1.寫一個回調類,寫出需要的構造方法 2.定義一個接口,裡面寫一個抽象方法,方法體(Stri
Android View的事件分發機制探索 概述 Android事件傳遞機制也是Android系統中比較重要的一塊,事件類型有很多種,這裡主要討論Tou
Android 系統API實現數據庫的增刪改查和SQLite3工具的使用,androidsqlite3在《Android SQL語句實現數據庫的增刪改查》中介紹了使用sq
Android中使用開源框架android-image-indicator實現圖片輪播部署,之前的博文中有介紹關於圖片輪播的實現方式,分別為(含超鏈接): 1、《Andr