热修复框架AndFix【源码阅读】
前言
AndFix是阿里巴巴開源的Android熱修復框架。其基本原理是利用JNI來實現方法的替換,以實現Android APP的熱修復,即無需發版即可臨時修復在線BUG。
熱修復技術有很多種,AndFix采取的native方法替換方案,優點是即時生效,無性能損耗,缺點是只能修改方法,且兼容性可能有問題。
雖然其原理比較簡單,但要深入理解,還需要對JNI,以及dalvik和Art兩種虛擬機,甚至art的多種版本源碼有比較深入的了解才行。整體難度還是比較大,因此本文并不深入到虛擬機實現細節,只針對JNI的相關部分進行了解。?
源碼地址:https://github.com/alibaba/AndFix
源碼版本:0.5.0?
一. 注冊native方法
AndFix.java的native方法
package com.alipay.euler.andfix; // ... public class AndFix { private static native boolean setup(boolean isArt, int apilevel);private static native void replaceMethod(Method dest, Method src);private static native void setFieldFlag(Field field); }?這幾個native方法是通過動態注冊的,而不是通過靜態注冊的。這兩種注冊方法,據網傳是動態注冊效率更高,不需要每次都去jni通過函數名來查找。
static JNINativeMethod gMethods[] = {/* name, signature, funcPtr */ { "setup", "(ZI)Z",(void*) setup }, { "replaceMethod", "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V",(void*) replaceMethod },{ "setFieldFlag", "(Ljava/lang/reflect/Field;)V",(void*) setFieldFlag }, };這里的三個native方法都根據當前運行時是dalvik還是art來路由到不同的實現函數,甚至art還根據其版本不同路由到針對不同版本art的實現。
| dalvik | /jni/dalvik/dalvik_method_replace.cpp | 
| android 4.4 (api 19) | /jni/dalvik/art_method_replace_4_4.cpp | 
| android 5.0 (> api 19) | /jni/dalvik/art_method_replace_5_0.cpp | 
| android 5.1 (> api 21) | /jni/dalvik/art_method_replace_5_1.cpp | 
| android 6.0 (> api 22) | /jni/dalvik/art_method_replace_6_0.cpp | 
| android 7.0 (> api 23) | /jni/dalvik/art_method_replace_7_0.cpp | 
這里也可以看出來兩點,
- 第一:ART首發于Android 4.4。
- 第二,基本上以后每一版Android的ART都進行了修改,而AndFix這種解決方案兼容性差的問題在這里則體現得比較明顯,一旦Android版本變化,則就必須針對其虛擬機來重寫實現方法。?
雖然針對不同虛擬機及版本有不同的實現,但通過代碼來看,其原理比較一致,不同的實現僅為了調用不同虛擬機的不同API而已。所以下面只研究傳統的dalvik實現方式。?
二. 初始化(setup)
這里面有一個知識點,是如何檢查當前運行時是dalvik還是Art,官方文檔中的原文描述為:
您可以通過調用?System.getProperty("java.vm.version")?來驗證正在使用哪種運行時。 如果使用的是 ART,則該屬性值將是?"2.0.0"?或更高。
代碼實現為:
final String vmVersion = System.getProperty("java.vm.version"); boolean isArt = vmVersion != null && vmVersion.startsWith("2");這代碼其實有點問題,文檔里說明的是art的version為等于或大于2.0.0,但代碼只判斷了是否為2開頭,如果有天art版本號迭代到3了則會出現兼容性問題,不太嚴謹。
jboolean setup(JNIEnv* env, jclass clazz, jboolean isart, jint apilevel);setup函數主要是為了一些初始化工作,在dalvik的實現里,主要是為了獲取?libdvm.so?里面的幾個函數指針,便于后面去調用。
一個是?dvmDecodeIndirectRef?函數。一個是?dvmThreadSelf?函數。?
2.1 dvmDecodeIndirectRef()
先來看dalvik虛擬機里面的?dvmDecodeIndirectRef?函數的定義:
/* * Convert an indirect reference to an Object reference. The indirect * reference may be local, global, or weak-global. * * If "jobj" is NULL, or is a weak global reference whose reference has * been cleared, this returns NULL. If jobj is an invalid indirect * reference, kInvalidIndirectRefObject is returned. * * Note "env" may be NULL when decoding global references. */ Object* dvmDecodeIndirectRef(Thread* self, jobject jobj) {}這個函數把一個jobject轉換成了dalvik里面定義的?Object?對象,在dalvik里面?Object對象,可用于實現:
- Class object
- Array Object
- data object
- String object
可用此函數獲取到?ClassObject?。例如?NewObject函數的源碼:
static jobject NewObject(JNIEnv* env, jclass jclazz, jmethodID methodID, ...) {ScopedJniThreadState ts(env);ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(ts.self(), jclazz);?if (!canAllocClass(clazz) || (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz))) {assert(dvmCheckException(ts.self()));return NULL;}Object* newObj = dvmAllocObject(clazz, ALLOC_DONT_TRACK);jobject result = addLocalReference(ts.self(), newObj);if (newObj != NULL) {JValue unused;va_list args;va_start(args, methodID);dvmCallMethodV(ts.self(), (Method*) methodID, newObj, true, &unused, args);va_end(args);}return result; }?2.2 dvmThreadSelf()
/* * Like pthread_self(), but on a Thread*. */ Thread* dvmThreadSelf() {return (Thread*) pthread_getspecific(gDvm.pthreadKeySelf); }該方法用于獲取當前線程。?
三. 設置成員域權限(setFieldFlag)
該函數的用處是將需要修復的類的所有成員域都設置為?public?。
實現方式比較簡單:
void dalvik_setFieldFlag(JNIEnv* env, jobject field) {Field* dalvikField = (Field*) env->FromReflectedField(field);dalvikField->accessFlags = dalvikField->accessFlags & (~ACC_PRIVATE)| ACC_PUBLIC;LOGD("dalvik_setFieldFlag: %d ", dalvikField->accessFlags); }?四. 替換方法(replaceMethod)
第一步,將用于替換的class設置為已經初始化好了的狀態:
jobject clazz = env->CallObjectMethod(dest, jClassMethod);ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(dvmThreadSelf_fnPtr(), clazz);clz->status = CLASS_INITIALIZED;這里好像并沒有像xposed框架一樣調用?dvmInitClass?函數來真正初始化class,而只是設置了status。
TODO: 為什么不初始化class,為什么又必須要設置status值??
然后將方式直接替換掉:
Method* meth = (Method*) env->FromReflectedMethod(src);Method* target = (Method*) env->FromReflectedMethod(dest);LOGD("dalvikMethod: %s", meth->name); // meth->clazz = target->clazz;meth->accessFlags |= ACC_PUBLIC;meth->methodIndex = target->methodIndex;meth->jniArgInfo = target->jniArgInfo;meth->registersSize = target->registersSize;meth->outsSize = target->outsSize;meth->insSize = target->insSize;meth->prototype = target->prototype;meth->insns = target->insns;meth->nativeFunc = target->nativeFunc;除了 clazz, name, shroty, fastJni, noRef, shouldTrace, registerMap, inProfile 幾個值以外的所有值都被替換成新的方法。?
至于每個字段的含義,可以參考一下 dalvik 的源碼中?Method?的結構體定義:
struct Method {/* the class we are a part of */ ? ClassObject* ? clazz;/* access flags; low 16 bits are defined by spec (could be u2?) */ ? u4 ? ? ? ? ? ? accessFlags;/* ? ? * For concrete virtual methods, this is the offset of the method ? ? * in "vtable". ? ? * ? ? * For abstract methods in an interface class, this is the offset ? ? * of the method in "iftable[n]->methodIndexArray". ? ? */ ? u2 ? ? ? ? ? ? methodIndex;/* ? ? * Method bounds; not needed for an abstract method. ? ? * ? ? * For a native method, we compute the size of the argument list, and ? ? * set "insSize" and "registerSize" equal to it. ? ? */ ? u2 ? ? ? ? ? ? registersSize; /* ins + locals */ ? u2 ? ? ? ? ? ? outsSize;u2 ? ? ? ? ? ? insSize;/* method name, e.g. "<init>" or "eatLunch" */ ? const char* ? ? name;/* ? ? * Method prototype descriptor string (return and argument types). ? ? * ? ? * TODO: This currently must specify the DexFile as well as the proto_ids ? ? * index, because generated Proxy classes don't have a DexFile. We can ? ? * remove the DexFile* and reduce the size of this struct if we generate ? ? * a DEX for proxies. ? ? */ ? DexProto ? ? ? prototype;/* short-form method descriptor string */ ? const char* ? ? shorty;/* ? ? * The remaining items are not used for abstract or native methods. ? ? * (JNI is currently hijacking "insns" as a function pointer, set ? ? * after the first call. For internal-native this stays null.) ? ? */ ? /* the actual code */ ? const u2* ? ? ? insns; ? ? ? ? /* instructions, in memory-mapped .dex */ ? /* JNI: cached argument and return-type hints */ ? int ? ? ? ? ? ? jniArgInfo;/* ? ? * JNI: native method ptr; could be actual function or a JNI bridge. We ? ? * don't currently discriminate between DalvikBridgeFunc and ? ? * DalvikNativeFunc; the former takes an argument superset (i.e. two ? ? * extra args) which will be ignored. If necessary we can use ? ? * insns==NULL to detect JNI bridge vs. internal native. ? ? */? DalvikBridgeFunc nativeFunc;/* ? ? * JNI: true if this static non-synchronized native method (that has no ? ? * reference arguments) needs a JNIEnv* and jclass/jobject. Libcore ? ? * uses this. ? ? */ ? bool fastJni;/* ? ? * JNI: true if this method has no reference arguments. This lets the JNI ? ? * bridge avoid scanning the shorty for direct pointers that need to be ? ? * converted to local references. ? ? * ? ? * TODO: replace this with a list of indexes of the reference arguments. ? ? */ ? bool noRef;/* ? ? * JNI: true if we should log entry and exit. This is the only way ? ? * developers can log the local references that are passed into their code. ? ? * Used for debugging JNI problems in third-party code. ? ? */? bool shouldTrace;/* ? ? * Register map data, if available. This will point into the DEX file ? ? * if the data was computed during pre-verification, or into the ? ? * linear alloc area if not. ? ? */ ? const RegisterMap* registerMap;/* set if method was called during method profiling */ ? bool ? ? ? ? ? inProfile; };?結語
除了Java代碼和NDK代碼以外,其實還有一塊比較重要,就是自動生產patch的工具,理解它需要對dex文件由比較深入的了解,而且阿里并沒有直接開源該工具,而且這個工具已經有盡2年多沒有更新過。?
總之,對于AndFix的實現機制的研究網上還是比較多的,主要是因為該框架的原理比較直接粗暴,比較好理解。但其實從細節來看,如果自己開發這樣的一個框架,需要對 dalvik 虛擬機, ART,Dex文件格式,JNI等知識都有一個比較全面而深入的了解才可能做出這樣一個看似簡單的解決方案,因此也說明了對于android底層的了解在很多情況下都是有比較大的幫助的,特別是在實現一些比較高級的功能時,例如熱修復這種。這點還是比較值得學習的。?
參考資料:
- AndFix項目源碼
- Xposed項目源碼
總結
以上是生活随笔為你收集整理的热修复框架AndFix【源码阅读】的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: JVM源码阅读-本地库加载流程和原理
- 下一篇: JVM源码阅读-Dalvik类的加载
