Android 熱修復 AndFix 原理,看這篇就夠了

語言: CN / TW / HK

點選 紫霧凌寒 關注,星標或置頂我們一起成長

前言

之前寫過一篇熱修復的文章,那時候剛開始接觸,照貓畫虎畫的還算比較成功。但是那種修復需要重新啟動APP,也就是在JAVA層實現的熱修復。我們知道目前Android主流的修復還有在Native層實現修復的,就是在Native層替換方法,不用重新啟動APP。今天寫了個Demo,下面主要分享一下它的主要原理。

一、熱修復簡介

目前,熱修復的原理主要有兩種技術,一是不需要啟動APP就能實現修復,在Native層實現的。一種時需要啟動APP,在JAVA層實現的。

  • Native層:andfix sophix  (即時修復  不重啟APP)

  • JAVA層:Tinker robust等(需要啟動APP)

出現異常的根源在於方法

我們的程式出現異常(BUG)的根源是什麼?為什麼會出現異常呢,要出現異常肯定是我們程式中的 某個方法 丟擲了異常,所以異常的根源是 方法 。那麼我們修復包的目的就是去替換異常的方法所在的包名類名下的方法。我們需要準確的找到這個方法,那麼我們怎麼去找這個方法呢?

如何替換已經執行的APK ?

是直接替換執行時的APK載入的有bug的類嗎?顯然不行,因為 Java的懶載入機制,在不啟動APP時新類不能替換老的類 。class類只被ClassLoader載入一次,所以已經有bug的類,再不啟動APP的情況下我們不能直接再虛擬機器中替換。那我們要怎麼去做呢?我們根據JAVA的記憶體執行機制來尋找有沒有突破口。

二、class載入(記憶體執行機制)

Java虛擬機器(JVM)在java程式執行的過程中,會將它所管理的記憶體劃分為若干個不同的資料區域,這些區域有的隨著JVM的啟動而建立,有的隨著使用者執行緒的啟動和結束而建立和銷燬。一個基本的JVM執行時記憶體模型如下所示:

img

我們分別看下它的執行時資料區

  • 方法區:class會被載入到方法區,當JVM使用類載入器定位class檔案,並將其載入到記憶體中,會提取class檔案的型別資訊,並將這些資訊儲存到方法區中,同時,放入方法區中的還有該型別中的類靜態變數。【方法表,靜態變數,】

  • 堆區:Java程式在執行時建立的所有型別物件和陣列都儲存在堆中/JVM會根據new指令在堆中開闢一個確定型別的物件記憶體空間。但是堆中開闢物件的空間並沒有任何人工指令可以回收,而是通過JVM的垃圾回收器負責回收。

  • 棧區:方法的執行是在虛擬機器,Java方法執行儲存在棧區,每個Java方法對應一個棧幀。每啟動一個執行緒,JVM都會為它分配一個Java棧,用於存放方法中的 區域性變數,運算元以及異常資料 等。當執行緒呼叫某個方法時,JVM會根據方法區中該方法的位元組碼組建一個棧幀,並將該棧幀壓入Java棧中,方法執行完畢時,JVM會彈出該方法棧並釋放掉。

  • 本地方法棧(Native 堆):本地方法棧的功能和特點類似於虛擬機器棧,不同的是,本地方法棧服務的物件是JVM執行的native方法,而虛擬機器棧服務的是JVM執行的java方法。

  • 程式計數器(PC暫存器): 程式計數器是一個記錄著當前執行緒所執行的位元組碼的行號指示器。 JAVA程式碼編譯後的位元組碼在未經過JIT(實時編譯器)編譯前,其執行方式是通過“位元組碼直譯器”進行解釋執行。簡單的工作原理為直譯器讀取裝載入記憶體的位元組碼,按照順序讀取位元組碼指令。讀取一個指令後,將該指令“翻譯”成固定的操作,並根據這些操作進行分支、迴圈、跳轉等流程。

當手指觸控APP ICON啟動APP的流程如下:

在這裡插入圖片描述

類載入之前有個,int型符號變數指向class記憶體區域,即將要載入class類資訊。(位元組碼檔案內有方法、成員變數)

載入過程由檔案變成記憶體的過程

  1. 載入ActivityThread 生成方法表

  2. 載入main()函式

  3. 虛擬機器將main()函式壓棧,生成一個棧幀,壓入棧區。

  4. 載入Application.class,生成它的方法表。

  5. 建立Application的物件,存放在堆區。每個物件都指向一個符號變數(類)

  6. Object.getClass()得到變數對應的類,最終是通過native方法,最終執行呼叫 klass 變數,他存放在堆區,指向符號變數,符號變數指向物件所在的記憶體區域(方法表,成員表)。

  7. 執行 ApplicationonCreate() 方法,他是一個物件方法,執行一個物件方法他會從物件出發,去傳送一個事件。根據符號變數找打方法表,找到 onCreate() 方法,並生成一個 onCreate() 棧幀,壓入棧區。

類什麼時候被載入到記憶體?

1、Application app   
2、=  new Application();

執行到第一行在方法區開闢一個符號變數,這個符號變數為int型別。並不會將 Application 類載入到記憶體。當執行第二行時才會被載入到記憶體。 類的初始化只有在主動引用這時候才會被載入到記憶體,如new建立 | 反射 Class.fromName()|JNI.findClass()、序列化

如何實現替換有bug的方法?

  1. 根據以上原理我們明白,Java層不能實現方法的替換,那麼我們另闢蹊徑,通過 Native層操控虛擬機器記憶體 ,這就是我們前面所說的突破口。

  2. 由於java物件可以建立多個,我們不能替換某一個物件而不替換其他物件的方法,所以我們需要找打一個方法替換所有物件的方法。那就是需要在方法表中替換有bug 的方法。

  3. 方法在虛擬機器中叫 ArtMethod 結構體,它是Native層的。方法表其實就是一個List集合。方法最終是轉換為 ArtMethod 結構體被執行。一個方法被壓棧多次這個方法就是遞迴呼叫。

  4. FindClass(實現父委託機制,bootstrapClassLoad) 呼叫 LookUpClass --->DefineClass(定義一個Class型別,定義klass,所有型別全部置空,之後再載入類)--->LinkClass(載入類資訊。從/data/app/包名,Apk中的dex檔案中來)--->LinkSuperClass(父類先被載入到記憶體)--->LinkMethods(父類載入方法){LinkInterfaceMethods 拿到方法數,形成方法表,例項一個ArrayList<`ArtMethod`>,每個方法例項一個 ArtMethod 結構體}

類是抽象的,必須要有一個記憶體載體{klass,每個類都有一個,並且是唯一的.}

三、手寫實現Andfix

寫一個bug類

首先我們要自己寫一個bug類, BugClasstest() 方法丟擲一個異常

/**
 * bug測試類
 */
public class BugClass {
    public int test(){
        //測試bug
        throw new RuntimeException("這是一個異常!");
    }
}

在Activity中呼叫異常的方法

比如說點選某個按鈕,這裡就不寫了。

實現修復

我們實現修復,也就是之前說的替換虛擬機器中記憶體中的方法表裡的方法,那麼怎麼替換呢?一個APK中有成千上萬個方法,就某一個有異常,我們怎麼區分呢?那就是用註解來區分。

1.定義註解

package com.example.bthvi.mycloassloaderapplication;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
    //修復哪一個class
    String clazz();
    //修復哪一個方法
    String method();
}

2.修復對應方法所,並給他加上註解

import com.example.bthvi.mycloassloaderapplication.Replace;

/**
 * bug測試類
 */
public class BugClass {

    @Replace(clazz = "com.example.bthvi.mycloassloaderapplication.xxx.BugClass",method = "test")
    public int test(){
        return 1;
    }
}

3.生成dex檔案查分包

怎麼生成dex檔案,前面一篇文章以及說過了:Android學習——手把手教你實現Android熱修復,這裡就不多做說明了。

4.實現修復工具類

  • 首先我們要拿到對應的已經修復的dex檔案,專案中我們肯定是從網路和獲取,這裡我們之還是定義在本地資料夾下。

  • 其次我們載入這個Dex檔案,拿到它的所有的類,遍歷類中的方法,根據註解得到哪些方法時候需要修復的。

  • 再根據註解中的類名方法名通過反射得到已經載入的有bug的方法。

  • 呼叫Native方法替換有bug的方法。

package com.example.bthvi.mycloassloaderapplication;
import android.content.Context;
import android.os.Environment;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Enumeration;

import dalvik.system.DexFile;
/**
 *@author bthvi
 *@time 2019/7/20
 *@desc 不用啟動APP實現熱修復
 */
public class FixDexManager {
    private final static String TAG = "FixDexUtil";
    private static final String DEX_SUFFIX = ".dex";
    private static final String APK_SUFFIX = ".apk";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    public static final String DEX_DIR = "odex";
    private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
    private Context context;

    public FixDexManager(Context context) {
        this.context = context;
    }

    public void isGoingToFix() {
        File externalStorageDirectory = Environment.getExternalStorageDirectory();

        // 遍歷所有的修復dex , 因為可能是多個dex修復包
        File fileDir = externalStorageDirectory != null ?
                new File(externalStorageDirectory,"007"):
                new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(這個可以任意位置)

        File[] listFiles = fileDir.listFiles();
        if (listFiles != null){
            System.out.println("TAG==目錄下檔案數量="+listFiles.length);
            for (File file : listFiles) {
                System.out.println("TAG==檔名稱="+file.getName());
                if (file.getName().startsWith("fix") &&
                        (file.getName().endsWith(DEX_SUFFIX))) {
                    loadDex(file);// 開始修復
                    //有目標dex檔案, 需要修復
                }
            }
        }
    }
    /**
     * 載入Dex檔案
     * @param file
     */
    public void loadDex(File file) {
        try {
            DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                    new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
            //當前的dex裡面的class 類名集合
            Enumeration<String> entry=dexFile.entries();
            while (entry.hasMoreElements()) {
                //拿到Class類名
                String clazzName= entry.nextElement();
                //通過載入得到類  這裡不能通過反射,因為當前的dex沒有載入到虛擬機器記憶體中
                Class realClazz= dexFile.loadClass(clazzName, context.getClassLoader());
                if (realClazz != null) {
                    fixClazz(realClazz);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 修復有bug的方法
     * @param realClazz
     */
    private void fixClazz(Class realClazz) {
        //得到類中所有方法
        Method[] methods=realClazz.getMethods();
        //遍歷方法 通過註解 得到需要修復的方法
        for (Method rightMethod : methods) {
            //拿到註解
            Replace replace = rightMethod.getAnnotation(Replace.class);
            if (replace == null) {
                continue;
            }
            //得到類名
            String clazzName=replace.clazz();
            //得到方法名
            String methodName=replace.method();
            try {
                //反射得到本地的有bug的方法的類
                Class wrongClazz=  Class.forName(clazzName);
                //得到有bug的方法(注意修復包中的方法引數名和引數列表必須一致)
                Method wrongMethod = wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
                //呼叫native方法替換有bug的方法
                replace(wrongMethod, rightMethod);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }



    }

    public native static void replace(Method wrongMethod, Method rightMethod) ;
}

5.實現Native方法替換

我們前面說了,方法在虛擬機器中是以 ArtMethod 結構體存在的,那麼我們替換就是要去替換舊的方法的 ArtMethod 物件的所有屬性。

#include <jni.h>
#include <string>
#include "art_method.h"

extern "C"
JNIEXPORT void JNICALL
Java_com_example_bthvi_mycloassloaderapplication_FixDexManager_replace(JNIEnv *env, jclass type, jobject wrongMethod,
                                             jobject rightMethod) {

    //ArtMethod存在於Android 系統原始碼中,只需要匯入我們需要的部分(art_method.h)
    art::mirror::ArtMethod *wrong=  (art::mirror::ArtMethod *)env->FromReflectedMethod(wrongMethod);
    art::mirror::ArtMethod *right=  (art::mirror::ArtMethod *)env->FromReflectedMethod(rightMethod);
    //    method   --->class ----被載入--->ClassLoader
    //錯誤的成員變數替換為正確的成員變數
    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->method_index_ = right->method_index_;
    wrong->dex_method_index_ = right->dex_method_index_;

}

這裡由於要用到 ArtMethod 所以我們要從原始碼中拿到 ArtMethod ,原始碼中 ArtMethod 引用太多的系統原始碼我們這裡簡化一下,只要宣告我們需要的變數即可。

namespace art {
    namespace mirror {
        class Object{
            // The Class representing the type of the object.
            uint32_t klass_;
            // Monitor and hash code information.
            uint32_t monitor_;

        };
        //簡化ArtMethod  只需要關注我們需要的,只需要成員變數宣告
        class ArtMethod : public Object {
        public:

            uint32_t access_flags_;
            uint32_t dex_code_item_offset_;
            // Index into method_ids of the dex file associated with this method
            //方法再dex中的索引
            uint32_t method_dex_index_;
            uint32_t dex_method_index_;
            //在方法表的索引
            uint32_t method_index_;
            const void *native_method_;

            const uint16_t *vmap_table_;
            // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
            uint32_t dex_cache_resolved_methods_;
            //方法 自發
            // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
            uint32_t dex_cache_resolved_types_;


            // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
            // The class we are a part of.
            //所屬的函式
            uint32_t declaring_class_;
        };
    }
}

這裡還用到了 Object 所以我們還是要宣告 Object 這個的原始碼太多這裡就不復制了,我們也不需要特意去搞懂這些底層原始碼,只需要關注我們需要的就行。到這裡我們就實現了Native層的熱修復。

Andfix的相容性

前面我們說的就是Andfix的原理及簡單實現,但是 Andfix相容性比較差 。它的相容性差是為什麼呢?我們這裡主要的原理是替換 ArtMethod 結構體的成員變數,這個結構體是初始化方法表時虛擬機器建立的, Google對於不同的系統版本 ArtMethod 結構體的成員變數都有做變動 如下:我們看下Android 6.0和7.0中 ArtMethod 的不同點[簡單找一兩個]。

//  Android 6.0系統原始碼中ArtMethod 精簡版  去掉註釋
class ArtMethod {
public:
    uint32_t declaring_class_;
    uint32_t dex_cache_resolved_methods_;
    uint32_t dex_cache_resolved_types_;
    uint32_t access_flags_;
    uint32_t dex_code_item_offset_;
    uint32_t dex_method_index_;
    uint32_t method_index_;

    struct PtrSizedFields {
        // Method dispatch from the interpreter invokes this pointer which may cause a bridge into
        // compiled code.
        void* entry_point_from_interpreter_;
        // Pointer to JNI function registered to this method, or a function to resolve the JNI function.
        void* entry_point_from_jni_;
        // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
        // the interpreter.
        void* entry_point_from_quick_compiled_code_;
    } ptr_sized_fields_;
};

//  Android 7.0系統原始碼中ArtMethod 精簡版  去掉註釋
class ArtMethod {
public:

    uint32_t declaring_class_;
    uint32_t access_flags_;
    uint32_t dex_code_item_offset_;
    uint32_t dex_method_index_;
    uint16_t method_index_;
    uint16_t hotness_count_;
    struct PtrSizedFields {
        // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
        ArtMethod** dex_cache_resolved_methods_;

        // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
        void* dex_cache_resolved_types_;

        // Pointer to JNI function registered to this method, or a function to resolve the JNI function,
        // or the profiling data for non-native methods, or an ImtConflictTable.
        void* entry_point_from_jni_;

        // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
        // the interpreter.
        void* entry_point_from_quick_compiled_code_;
    } ptr_sized_fields_;

};

從上面的原始碼中我們明顯看到 ArtMethod 結構體中成員變數的改變,如 method_index_ 在6.0是32位在7.0中就是16位了。還有 PtrSizedFields 結構體的成員變數也有修改。所以這就使的AndFix的相容性很差,要想相容所有版本就得對不同版本去做相容適配。

由於AndFix的相容性和它是免費開源的,阿里在sophix出來之後就以及不再維護AndFix了。 Sophix 它的方案可以說是比較完美了,它是結合了JAVA層和Native層的兩者的有點,它的原理介紹大家可以看看這本書:《深入探索Android熱修復技術原理》

技術交流,歡迎加我微信:ezglumes ,拉你入技術交流群。

掃碼關注公眾號【音影片開發進階】,一起學習多媒體音影片開發~~~

喜歡就點個 「在看」  ▽