背景介紹
熱修復,乍一聽,感覺好牛逼的樣子,實際上並沒有多麼神祕,爲什麼這樣說呢?且聽我娓娓道來。。。
你發佈了一款安卓應用,早上剛發版,結果發完之後發現有個bug沒有修復,會直接導致整個應用崩潰,這時候你該怎麼辦呢?難道再馬上重新打包發版嗎?顯然是不現實的,那麼這時候熱修復就來了,幫你打上一個補丁(沒錯,我認爲熱修復就像給衣服打補丁。。。),然後在你應用啓動的時候直接進行修補,這樣就可以不用發版了啊。
聽上去感覺有點懵,怎麼打補丁,應用怎麼提前知道我哪裏代碼出問題了?什麼是熱修復?這都是啥?我是誰?我在哪???
不着急,咱們慢慢來,先來看一個目前來說整個市場上的熱修復方案的特性吧。
預熱
上面囉嗦了一大堆,其實最重要的就是上面這張圖,這張圖也比較老了,現在都Android 10 了。。。我還弄的7的圖。。。將就看吧,意思能表達清楚就行。
目前的熱修復大致分爲兩個方案:一種是native層的,代表的是阿里的AndFix(停更好幾年)和Sophix(不開源),另外一種就是java層的,代表的是騰訊的Tinker(開源)。今天準備模仿的是阿里的AndFix。
既然要模仿AndFix,那麼就來說一下AndFix的優勢吧:首先它打出的修復包要比Tinker打出的小很多(精確到方法),其次它的性能消耗代價要小,最重要的是:它及時生效,無需退出應用重新進入即可修復。
我們都知道:Java方法的執行一定有相應的入口(包括普通執行,亦或通過反射執行)。那麼可以思考一下AndFix是怎樣工作的?安卓中Java文件編譯成class後會打成dex包,方法即存在於dex包中。dex包是在虛擬機中執行的,虛擬機是c/c++編寫的,虛擬機在執行方法時在安卓源碼中存在着成員變量表和方法表,而方法表中存在着一個結構體,我們的方法都是由這個結構體來保存執行的,這個結構體就是ArtMethod。那麼我們需要做的就是:在native層進行方法的替換,將錯誤的方法替換爲正確的方法即可。
當然,虛擬機在安卓4.4以下和5.0以上有了翻天覆地的變化,在4.4及以前,虛擬機爲Davik,它採用的是JIT(即時編譯);5.0以上虛擬機爲Art,採用的是AOT(預編譯)。兩者區別就是Art安裝應用時慢,加載快,Davik安裝應用快,加載慢。(細心的肯定發現了安卓4.4及以前的安卓版本安裝應用要比現在快很多)。但是今天不考慮Davik,因爲現在的手機基本沒有4.4及以下的版本了,就不做適配了。這裏還要說的是,AndFix熱修復基於的是安卓源碼中的結構體(art_method.h),所以說國內某些廠商對安卓系統進行魔改了,有可能修復失敗;還有就是每一個版本的安卓系統中的源碼都不同,需要適配來進行解決,否則會修復失敗。
開始編碼
我也沒想到我能寫出上面那麼多字,好了,終於到了編碼的時候了。來新建一個c++的項目:
直接選擇這個:
咱們先來模仿一個崩潰,直接拋出異常:
/**
* @ProjectName: Andfix
* @Package: com.zj.andfix
* @Author: jiang zhu
* @Date: 2020/1/2 21:25
*/
public class Caclutor {
public void test(Context context){
throw new RuntimeException("報錯了");
}
}
在MainActivity中進行調用,模仿現實中的崩潰:
public void test(View view) {
Caclutor caclutor = new Caclutor();
caclutor.test(this);
}
再來模仿寫一個解決完bug的類:
/**
* @ProjectName: Andfix
* @Package: com.zj.andfix
* @Author: jiang zhu
* @Date: 2020/1/2 21:25
*/
public class Caclutor {
public void test(Context context){
//throw new RuntimeException("報錯了");
Toast.makeText(context, "修復成功了", Toast.LENGTH_SHORT).show();
}
}
接下來要寫一個註解,我們要獲取到是哪個類和哪個方法出了問題:
/**
* @ProjectName: Andfix
* @Package: com.zj.andfix
* @Author: jiang zhu
* @Date: 2020/1/2 21:18
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Replace {
//類的全限定名
String path();
//方法名
String method();
}
寫好註解之後在修復類中加上註解:
@Replace(path = "com.zj.andfix.Caclutor",method = "test")
public void test(Context context){
//throw new RuntimeException("報錯了");
Toast.makeText(context, "修復成功了", Toast.LENGTH_SHORT).show();
}
接下來就到了最重要的一步,打出修復包,咱們先把錯誤的代碼打一個apk包(release),然後再把修復好的代碼打一個aok包。咱們需要打的是一個dex文件,需要使用到安卓sdk中的工具,進入你的sdk/build-tools/版本/dx.bat,這個dx.bat就是咱們需要使用的工具。想要全局使用dx.bat需要配置全局變量:
然後在path中也同樣配置一下,就可以在cmd中直接進行使用了。打開cmd,命令是:
dx --dex --output 要打包的路徑/名字.dex 源文件路徑(即你通過build出的class文件)
執行完命令之後生成了修復包,咱們把這個修復包直接放入測試機的根目錄,真實開發中肯定放在私密目錄。
最最重要的來了
咱們需要一個工具類來加載咱們的修復包,需要用到上下文,所以可以直接傳入:
/**
* @ProjectName: Andfix
* @Package: com.zj.andfix
* @Author: jiang zhu
* @Date: 2020/1/2 21:39
*/
public class DexManager {
private Context context;
static {
System.loadLibrary("native-lib");
}
public void setContext(Context context) {
this.context = context;
}
}
別忘了加載native-lib。接下來需要一個方法來加載我們的修復包:
public void load(File file) {
try {
DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
new File(context.getCacheDir(), "opt").getAbsolutePath(),
Context.MODE_PRIVATE);
Enumeration<String> entry= dexFile.entries();
while (entry.hasMoreElements()) {
// 全類名
String className = entry.nextElement();
Class realClazz=dexFile.loadClass(className, context.getClassLoader());
if (realClazz != null) {
fixClass(realClazz);
}
// Class.forName(className);
}
} catch (Exception e) {
e.printStackTrace();
}
}
下面簡單說一下上面方法的意思:先通過傳進來的File文件獲取到一個DexFile文件,然後遍歷裏面所有的類,獲取到修復包中類的全限定名,通過loadClass獲取到修復類,如果類不爲空,則進行修復,下面是fixClass方法的代碼:
private void fixClass(Class realClazz) {
//加載方法 Method
Method[] methods = realClazz.getMethods();
for (Method rightMethod : methods) {
Replace replace = rightMethod.getAnnotation(Replace.class);
if (replace == null) {
continue;
}
String clazzName = replace.path();
String methodName = replace.method();
try {
Class wrongClazz=Class.forName(clazzName);
//Method right wrong
Method wrongMethod=wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
replace(wrongMethod, rightMethod);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面的代碼首先獲取到類中所有的方法,然後進行遍歷,獲取方法上咱們定義的註解,如果有自定義註解的畫,獲取類的全限定名和方法名,獲取到正確的方法和錯誤的方法。接下來就交給了replace方法:
public native void replace(Method wrongMethod, Method rightMethod);
replace方法是一個native方法,需要寫c++來實現了,到這裏咱們需要引入安卓源碼中的ArtMethod.h頭文件了(上面講到過,注意,只需引入結構體的代碼,其他刪掉即可,全部引用的話代碼太多,一層套一層,會把源碼都搬過來的。。。),下面是ArtMethod.h頭文件的代碼,大家可以直接進行復制,或者去最新的安卓源碼中去複製:
#include <stdint.h>
namespace art{
namespace mirror{
class Object{
uint32_t klass_;
uint32_t monitor_;
};
class ArtMethod:public Object{
public:
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
uint32_t dex_cache_resolved_methods_;
uint32_t dex_cache_resolved_types_;
uint32_t declaring_class_;
};
}
}
萬事俱備,之前東風,最後需要的就是在c++中進行方法的替換了:
extern "C"
JNIEXPORT void JNICALL
Java_com_zj_andfix_DexManager_replace(JNIEnv *env, jobject thiz, jobject wrongMethod,
jobject rightMethod) {
art::mirror::ArtMethod *wrong= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(wrongMethod));
art::mirror::ArtMethod *right= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(rightMethod));
// wrong=right;
wrong->declaring_class_ = right->declaring_class_;
wrong->dex_cache_resolved_methods_ = right->dex_cache_resolved_methods_;
wrong->access_flags_ = right->access_flags_;
wrong->dex_cache_resolved_types_ = right->dex_cache_resolved_types_;
wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
wrong->dex_method_index_ = right->dex_method_index_;
wrong->method_index_ = right->method_index_;
}
至此,AndFix基本原理已經實現。“別光寫不練啊,運行試試啊!”
好嘞,咱們來看一下運行效果吧:
文末
本來只是想簡單總結一下,沒想到越寫越多,本來還打算寫一下阿里的正宗的AndFix的使用流程,放到下一篇文章吧,之後再寫寫騰訊的Tinker。週六的晚上寫到了週日,也是沒誰了,好了,準備洗漱,睡覺。晚安了陌生人。