Android熱修復------手寫AndFix實現(基於Android7.0)

市面上很多熱修復框架,相信大家也都有對比使用過,其他熱修復框架這裏也不列舉對比了,今天就從阿里的AndFix熱修復框架入手,學習下AndFix裏面的熱修復是如何做的。

源碼地址:https://github.com/alibaba/AndFix
上次更新已經是三四年前的了,基本上沒有維護了,不過還是有學習的價值。 接下來我們就來簡單看下里面到底怎麼做的,然後自己實現一個熱修復。

AndFix實現原理

在這裏插入圖片描述

沒錯,實現原理就一張圖,也可以用一句話來概括,就是方法的替換。 將我們修復過後的方法,把原來的錯誤方法替換調。那麼下次調用的時候就是調用的我們修復過後的方法,這樣程序就不會發生問題了。(這裏只是很簡單的概括了一下,具體大家可以看看AndFix GitHub裏面的介紹更爲詳細)

預習知識:關於JVM虛擬機內存結構
參考資料

1.https://www.cnblogs.com/ityouknow/p/5610232.html

2.https://www.jianshu.com/p/8ba5b90a5c66

3.more。。。。

##1.開始

既然我們已經知道了AndFix是通過方法的替換,那麼我們就來梳理下,這個方法應該如何替換?

1.通過查找資料得知,方法無法通過java代碼直接實現替換,只能通過NDK C++的方式替換。(涉及到jvm虛擬機的相關知識,方法區等)
2.通過文檔中得知,修復要先打包一個修復包,然後進行修復。

好的,那麼我們知道了上面2個條件,我們就開始我們的實現之旅。

##2.步驟說明

1.修復有問題的代碼,然後編譯成Class文件,找出修復好的Class文件
2.將CLass文件打包成Dex文件。
3.將dex放入手機中
4.加載dex文件
5.加載Dex文件中的class文件
6.取出我們需要替換的正確的方法
7.替換方法

###2.1 修復有問題的Class文件

首先我們創建一個有問題的代碼如下

public class Calculation {

public Calculation() {
}

public int calculation() {
    throw new RuntimeException("手動拋出異常");
}
}

然後我們調用上面的代碼如下

button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Calculation calculation = new Calculation();
            int calculation1 = calculation.calculation();
            tv.setText("--->" + calculation1);
        }
    });

這裏通過點擊一個按鈕執行這個類裏面的這個方法,我們看看運行效果

在這裏插入圖片描述
calculation
上圖可以看到頁面的整個佈局,點擊左邊的按鈕,調用了面寫的‘calculation’方法,拋出了一個異常。

在這裏插入圖片描述

現在異常代碼我們已經造好了,現在我們來修復他。我們這裏另外建一個類來修復它。

package com.xiaxiayige.andfixdemo.fix;

import com.xiaxiayige.andfixdemo.Replace;

public class Calculation {

public Calculation() {
}

@Replace(className = "com.xiaxiayige.andfixdemo.Calculation", methodName = "calculation")
public int calculation() {
    return 100;
}
}

上面這個類是我在fix包下面複製原來的類,然後修改calculation方法,讓他返回一個數值100。 去掉了之前手動拋出的異常。

這裏有一點需要注意的是@Replace註解,可能有的人問爲什麼要加這個呢,原來的方法上並沒有這個啊。 這個就需要大家看到上面提到的需要我們找到正確的方法。 這裏看看Replace註解類

@Retention(RetentionPolicy.RUNTIME) //運行時註解
@Target(ElementType.METHOD)  // 應用在方法之上
public @interface Replace {
//需要替換的類路徑名稱
String className(); 
//需要替換的正確的方法名
String methodName(); 
}

上面的註釋已經很好的說明了一切。 我們定義在方法上的註解的原因就是我們加載我們修復包的時候,我們遍歷我們修復包中的class文件,取出裏面所有的方法,然後查看我們要把帶有註解的方法替換給哪個類誰的哪個方法。所以上面修復類的類名和方法名不需要和有問題的類名方法名一致也可以。

2.2打包修復包的Dex文件。(可以運用其他手段打出差分包 bsdiff)

步驟1.我們先build一下項目,然後會生成所有類的class文件。 然後找到我們修復的那個class文件。具體步驟如下

1.點下運行旁邊的小錘子即可或者(build->Make Project)	
2.然後切換項目視圖 如下圖1所示
3.找到右鍵,打開到文件目錄,刪除其他class文件,只保留修復包的class文件(圖2)
4.然後執行一條dx命令,這個dx.bat文件是我們AndroidSDK自帶的一個工具,我們可以在你的sdk目錄下 sdk\build-tools\29.0.1\dx.bat
5.執行命令 dx --dex --output fix.dex 修復包路徑 (見圖3),這樣我們就可以打包一個dex文件了。

圖1

在這裏插入圖片描述

圖2

在這裏插入圖片描述

圖3

在這裏插入圖片描述

注意(這裏我能直接使用dx命令 是因爲我把dx.bat 添加到環境變量了)

繼續

2.3 把Dex文件push到手機sd卡中。

太簡單了,無圖演示

2.4 /2.5/2.6 加載dex文件,取出錯誤的方法和正確的方法

這裏我們假設你已經把dex文件push到手機中了,這裏就開始我們加載dex,然後取出裏面的class文件

 private void fix() {
    //1.得到dex文件 記得自己去申請權限請求 記得自己去申請權限請求 記得自己去申請權限請求
    File dexFilePath = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "fix.dex");
    try {
		//加載dex文件
        DexFile dexFile = DexFile.loadDex(dexFilePath.getAbsolutePath(), new File(getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
		
        Enumeration<String> entries = dexFile.entries();
        while (entries.hasMoreElements()) {
            String element = entries.nextElement();
            //獲取到Class
            Class aClass = dexFile.loadClass(element, getClassLoader());
			//獲取到class中的所有方法
            Method[] declaredMethods = aClass.getMethods();
            for (Method dest : declaredMethods) {
 				//獲取帶有指定註解的方法
                Replace annotation = dest.getAnnotation(Replace.class);
                if (annotation == null) {
                    continue;
                }
                //獲取要替換方法的類
                String className = annotation.className();
                //獲取要替換類方法的方法
                String methodName = annotation.methodName();
                try {
                    //原始類 含有錯誤的方法的類。
                    Class errorClass = Class.forName(className);
                    Class<?>[] parameterTypes = dest.getParameterTypes();
                    System.out.println("parameterTypes = " + parameterTypes);
                    Method srcMethod = errorClass.getDeclaredMethod(methodName, parameterTypes);
                    System.out.println("errorMethod = " + srcMethod);
                    // 執行native方法
                    replaceMethod(srcMethod, dest);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }

            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

這裏定義了一個native方法

//正確的
 public native void replaceMethod(Method errorMethod, Method rightMethod);

然後我們再去寫C++代碼實現方法的替換

首先我們要拷貝一個art_method.h的頭文件到我們的項目中,這個文件在哪兒呢 ,大家可以在Android源碼中找到。這裏我們通過
http://androidxref.com/	然後去找,具體的路徑在 源碼路徑/art/runtime/art_method.h ,然後拷貝下來後需要做一些處理,刪除我們不需要的東西,這裏就很重要了,哪些東西刪,哪些不刪呢。 這裏經過測試,大家只保留裏面定義的一些屬性就行了,其他一些方法類全部刪掉。 (可以參考AndFix項目 哈哈),這裏貼一份我的

基於Android 7.0 的ArtMethod,刪減之後的文件

namespace art {

union JValue;

class OatQuickMethodHeader;

class ProfilingInfo;

class ScopedObjectAccessAlreadyRunnable;

class StringPiece;

class ShadowFrame;

namespace mirror {
    class Array;

    class Class;

    class IfTable;

    class PointerArray;
}  // namespace mirror
class ArtMethod {
public:
    uint32_t access_flags_;

    uint32_t dex_code_item_offset_;
    uint32_t dex_method_index_;
    uint16_t method_index_;
    uint16_t hotness_count_;


    uint16_t method_dex_index_;
    const void *native_method_;
    const uint16_t *vmap_table_;
    uint32_t dex_cache_resolved_methods_;
    uint32_t dex_cache_resolved_types_;
    uint32_t declaring_class_;
};

}

這裏主要保證的是ArtMethod這裏面的一些變量是不能少的,比如 access_flags_ method_dex_index_v這樣一些和方法有關係的變量。

然後我們來編寫替換方法的代碼了。

3.方法替換

	extern "C"
JNIEXPORT void JNICALL
Java_com_xiaxiayige_andfixdemo_MainActivity_replaceMethod(JNIEnv *env, jobject instance,
                                                          jobject srcMethod,
                                                          jobject destMethods) {
	//獲取到方法
    art::ArtMethod *wrongMethod =(art::ArtMethod *) env->FromReflectedMethod(srcMethod);
    art::ArtMethod *rightMethod =(art::ArtMethod *) env->FromReflectedMethod(destMethods);

	//把正確的方法屬性變量賦值給錯誤的
    wrongMethod->access_flags_=rightMethod->access_flags_;
    wrongMethod->dex_code_item_offset_=rightMethod->dex_code_item_offset_;
    wrongMethod->dex_method_index_=rightMethod->dex_method_index_;
    wrongMethod->method_index_=rightMethod->method_index_;
    wrongMethod->hotness_count_=rightMethod->hotness_count_;

}	

記得文件前面需要引用#include "art_method.h"頭文件 ,不然會找不到ArtMethod類。

然後我們就可以跑一下代碼看下情況了

在這裏插入圖片描述

Good,程序已經被修復了,正常運行。

再從頭到尾梳理一遍把。

修復錯誤方法--->打包dex文件---->解析dex文件----->找出錯誤的類,取出錯誤的方法------>通過ArtMethod實現方法的替換(其實是內存地址的一系列替換,將錯誤的方法的地址指向正確的方法的地址)

4 最後

我們來看看AndFix是怎麼做的。

在這裏插入圖片描述
在這裏插入圖片描述

可以看到最後也是實現的是將新方法的一些數據替換給老的。

5 缺點

我這裏是基於Android7.0來做的,所以在其他手機上運行會有問題,可以看到的是AndFix也是定義了各種版本的c文件,用來兼容不同手機的方法替換。 所以每新出一個系統我們就要去做一次兼容還是挺麻煩的。

這裏AndFix之支持到了7.0的系統,我們也可以根據此思路,去兼容8.0,9.0的系統提供給自己使用。

源碼地址:AndFixDemo

多想想別人怎麼實現,實現思路是什麼,學會閱讀源碼是一個好習慣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章