9012年都過去了,你確定還不學安卓的熱修復?(手寫AndFix)

背景介紹

熱修復,乍一聽,感覺好牛逼的樣子,實際上並沒有多麼神祕,爲什麼這樣說呢?且聽我娓娓道來。。。

你發佈了一款安卓應用,早上剛發版,結果發完之後發現有個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。週六的晚上寫到了週日,也是沒誰了,好了,準備洗漱,睡覺。晚安了陌生人。

 

發佈了87 篇原創文章 · 獲贊 248 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章