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