1. 程式人生 > >JAndFix: 基於Java實現的Android實時熱修復方案

JAndFix: 基於Java實現的Android實時熱修復方案

簡述

  • JAndFix是一種基於Java實現的Android實時熱修復方案,它並不需要重新啟動就能生效。JAndFix是在AndFix的基礎上改進實現,AndFix主要是通過jni實現對method(ArtMethod)結構題內容的替換。JAndFix是通過Unsafe物件直接操作Java虛擬機器記憶體來實現替換。

原理

  • 為何JAndfix能夠做到即時生效呢? 原因是這樣的,在app執行到一半的時候,所有需要發生變更的Class已經被載入過了,在Android上是無法對一個Class進行解除安裝的。而騰訊系的方案,都是讓Classloader去載入新的類。如果不重啟,原來的類還在虛擬機器中,就無法載入新類。因此,只有在下次重啟的時候,在還沒走到業務邏輯之前搶先載入補丁中的新類,這樣後續訪問這個類時,就會Resolve為新的類。從而達到熱修復的目的。JAndfix採用的方法是,在已經載入了的類中直接拿到Method(ArtMethod)在JVM的地址,通過Unsafe直接修改Method(ArtMethod)地址的內容,是在原來類的基礎上進行修改的。我們這就來看一下JAndfix的具體實現。

虛擬機器呼叫方法的原理

為什麼這樣替換完就可以實現熱修復呢?這需要從虛擬機器呼叫方法的原理說起。在Android 6.0,art虛擬機器中ArtMethod的結構是這個樣子的:

@art/runtime/art_method.h

class ArtMethod FINAL {
 ... ...

 protected:
  // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
  // The class we are a part of.
  GcRoot<mirror::Class> declaring_class_;

  // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
  GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;

  // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
  GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;

  // Access flags; low 16 bits are defined by spec.
  uint32_t access_flags_;

  /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */

  // Offset to the CodeItem.
  uint32_t dex_code_item_offset_;

  // Index into method_ids of the dex file associated with this method.
  uint32_t dex_method_index_;

  /* End of dex file fields. */

  // Entry within a dispatch table for this method. For static/direct methods the index is into
  // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
  // ifTable.
  uint32_t method_index_;

  // Fake padding field gets inserted here.

  // Must be the last fields in the method.
  // PACKED(4) is necessary for the correctness of
  // RoundUp(OFFSETOF_MEMBER(ArtMethod, ptr_sized_fields_), pointer_size).
  struct PACKED(4) 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_;

... ...
}

這其中最重要的欄位就是entry_point_from_interprete_和entry_point_from_quick_compiled_code_了,從名字可以看出來,他們就是方法的執行入口。我們知道,Java程式碼在Android中會被編譯為Dex Code。 art中可以採用解釋模式或者AOT機器碼模式執行。

解釋模式,就是取出Dex Code,逐條解釋執行就行了。如果方法的呼叫者是以解釋模式執行的,在呼叫這個方法時,就會取得這個方法的entry_point_from_interpreter_,然後跳轉過去執行。

而如果是AOT的方式,就會先預編譯好Dex Code對應的機器碼,然後執行期直接執行機器碼就行了,不需要一條條地解釋執行Dex Code。如果方法的呼叫者是以AOT機器碼方式執行的,在呼叫這個方法時,就是跳轉到entry_point_from_quick_compiled_code_執行。

AndFix的方法替換其本質是ArtMethod指標所指內容的替換。 Art Method--Art 6.0

變成了這樣的整體替換 Art Method--Art 6.0

由Unsafe來實現相當於:

	//src means source ArtMethod Address,dest mean destinction ArtMethod Address
    private void replaceReal(long src, long dest) throws Exception {
        int methodSize = MethodSizeUtils.methodSize();
        int methodIndexOffset = MethodSizeUtils.methodIndexOffset();
        //methodIndex need not replace,becase the process of finding method in vtable need methodIndex
        int methodIndexOffsetIndex = methodIndexOffset / 4;
        //why 1? index 0 is declaring_class, declaring_class need not replace.
        for (int i = 1, size = methodSize / 4; i < size; i++) {
            if (i != methodIndexOffsetIndex) {
                int value = UnsafeProxy.getIntVolatile(dest + i * 4);
                UnsafeProxy.putIntVolatile(src + i * 4, value);
            }
        }
    }

so easy,JAndFix就這樣完成了方法替換。值得一提的是,由於忽略了底層ArtMethod結構的差異,對於所有的Android版本都不再需要區分,而統一以Unsafe實現即可,程式碼量大大減少。即使以後的Android版本不斷修改ArtMethod的成員,只要保證ArtMethod陣列仍是以線性結構排列,就能直接適用於將來的Android 8.0、9.0等新版本,無需再針對新的系統版本進行適配了。事實也證明確實如此,當我們拿到Google剛發不久的Android O(8.0)開發者預覽版的系統時。

對比方案

名字 公司 實現語言 及時生效 方法替換 方法的增加減少 Android版本 機型 效能損耗 補丁大小 回滾 易用性
JAndFix 阿里天貓 JAVA 支援 支援 不支援 4.0+ ALL 支援
AndFix 阿里支付寶 C 支援 支援 不支援 4.0+ 極少部分不支援 支援
Tinker 騰訊 JAVA 不支援 支援 支援 ALL ALL 不支援
Robust 美團 JAVA 支援 不支援 不支援 ALL ALL 支援 差(需要反射呼叫,需要打包外掛支援)
Dexposed 個人 C 支援 支援 不支援 4.0+ 部分不支援 支援 差(需要反射呼叫)

如何使用

     try {
            Method method1 = Test1.class.getDeclaredMethod("string");
            Method method2 = Test2.class.getDeclaredMethod("string");
            MethodReplaceProxy.instance().replace(method1, method2);
        } catch (Exception e) {
            e.printStackTrace();
        }

Running DEMO

  • 把整個專案放入你的IDE即可(Android Studio)

注意

Proguard

-keep class com.tmall.wireless.jandfix.MethodSizeCase { *;}

解釋實現

  • 我以Android Art 6.0的實現來解釋為什麼這樣實現就可實現方法替換
package com.tmall.wireless.jandfix;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * Created by jingchaoqinjc on 17/5/15.
 */

public class MethodReplace6_0 implements IMethodReplace {

    static Field artMethodField;

    static {
        try {
            Class absMethodClass = Class.forName("java.lang.reflect.AbstractMethod");
            artMethodField = absMethodClass.getDeclaredField("artMethod");
            artMethodField.setAccessible(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    public void replace(Method src, Method dest) {
        try {
            long artMethodSrc = (long) artMethodField.get(src);
            long artMethodDest = (long) artMethodField.get(dest);
            replaceReal(artMethodSrc, artMethodDest);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void replaceReal(long src, long dest) throws Exception {
    	// ArtMethod struct size
        int methodSize = MethodSizeUtils.methodSize();
        int methodIndexOffset = MethodSizeUtils.methodIndexOffset();
        //methodIndex need not replace,becase the process of finding method in vtable
        int methodIndexOffsetIndex = methodIndexOffset / 4;
        //why 1? index 0 is declaring_class, declaring_class need not replace.
        for (int i = 1, size = methodSize / 4; i < size; i++) {
            if (i != methodIndexOffsetIndex) {
                int value = UnsafeProxy.getIntVolatile(dest + i * 4);
                UnsafeProxy.putIntVolatile(src + i * 4, value);
            }
        }
    }
}

1.declaring_class不能替換,為什麼不能替換,是因為JVM去呼叫方式時很多地方都要對declaring_class進行檢查。替換declaring_class會導致未知的錯誤。 2.methodIndex 不能替換,因為public proected等簡介定址的訪問許可權,本質在尋找方法的時候會查詢virtual_methods_,而virtual_methods_是個ArtMethod陣列物件,需要通過methodIndex來查詢,如果你的methodIndex不對會導致方法定址出錯。 3.為什麼AbstractMethod類中對應的artMethod屬性的值可以作為c層ArtMethod的地址直接使用?看原始碼:

@@art/mirror/abstract_method.cc
ArtMethod* AbstractMethod::GetArtMethod() {
  return reinterpret_cast<ArtMethod*>(GetField64(ArtMethodOffset()));
}

@@art/mirror/abstract_method.h
static MemberOffset ArtMethodOffset() {
    return MemberOffset(OFFSETOF_MEMBER(AbstractMethod, art_method_));
  }

從原始碼可以看出C層在獲取ArtMethod的地址,實際上就是把AbstractMethod的artMethod強制轉換成了ArtMethod*指標,及我們在Java拿到的artMethod就是c層ArtMethod的實際地址。是不是很簡單。

參考

原文地址: https://github.com/cmzy/JAndFix