JAndFix

Welcome to the JAndFix wiki!

* [部分數據內容來自Hotfix](https://yq.aliyun.com/articles/74598)

``

# JAndFix

### 簡述

* JAndFix是一種基于Java實現的輕量級Android實時熱修復方案,它并不需要重新啟動就能生效。JAndFix是在AndFix的基礎上改進實現,AndFix主要是通過jni實現對method(ArtMethod)結構題內容的替換。JAndFix是通過Unsafe對象直接操作Java虛擬機內存來實現替換。

### 原理

* 為何JAndfix能夠做到即時生效呢?

通常的熱修復技術,如Tinker,Wuna等修復技術方案,嚴格說來是冷修復技術,其修復都要在下次啟動后才能生效,原因是因為Android的虛擬機在加載完Class后是不會卸載的,即使patch下發成功ClassLoader也只會加載老的dex的類,因此需要在類加載前把patch加到虛擬機里,所以說是冷修復技術。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 declaring_class_;

// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.

GcRoot dex_cache_resolved_methods_;

// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.

GcRoot> 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_;

... ...

}

```

看完ArtMethod的結構后,我們需要了解虛擬機尋找方法的過程,其本質是通過entry_point_from_interpreter_去解釋執行DexCode的代碼,entry_point_from_jni_ 執行native的代碼,entry_point_from_quick_compiled_code_ oat后的本地機器代碼。也就說我們要修改方法的邏輯,其本質就是要修改這些參數的值。知道原理后用Unsafe來實現就很簡單了,就是將src 的artmethod所在地址的內容替換為dest artMethod所在地址的內容,詳情如下:

```

//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(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的實際地址。是不是很簡單。

### Git

* [JAndFix:https://github.com/alibaba/JAndFix](https://github.com/alibaba/JAndFix)

### 參考

* [Unsafe](http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/)

* [AndFix](https://github.com/alibaba/AndFix)

* [Hotfix](https://yq.aliyun.com/articles/74598)

### License

[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html])

Copyright (c) 2017, alibaba-inc.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容