熱修復(fù)主要有三個(gè)步驟:
1 生成差異補(bǔ)丁
2 加載差異補(bǔ)丁
3 替換方法
1.1 生成差異補(bǔ)丁
阿里提供的差量補(bǔ)丁生成工具https://github.com/alibaba/AndFix/raw/master/tools/apkpatch-1.0.3.zip
2.1 加載差異包
判斷版本 如果是當(dāng)前版本严拒,加載所有的差量包,否則刪除所有的差量包
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
initPatchs();
}
}
這里的initPatchs 會(huì)把本地的補(bǔ)丁全部add到PatchManager 的變量mPatchs中然后在loadPatch方法中加載所有的補(bǔ)丁
/**
* load patch,call when application start
*
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
接下來(lái)我們來(lái)看下mAndFixManager.fix() 的方法是怎么實(shí)現(xiàn)的
關(guān)鍵代碼如下
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
通過(guò)classLoader找到補(bǔ)丁中需要替換的class,然后調(diào)fixClass方法,來(lái)完成方法的替換
下面我們?cè)诳聪耭ixClass是怎么實(shí)現(xiàn)的:
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}
通過(guò)apkpatch生成的補(bǔ)丁,在需要替換的方法上加上@MethodReplace的注解祠饺,找到注解的方法,然后再調(diào)用replaceMethod就可以實(shí)現(xiàn)方法的替換了
3.1 方法的替換
繼續(xù)貼源碼
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
上面的mFixClass 的put 和get方法是為了減少多次重復(fù)的loadClass,關(guān)鍵方法是addReplaceMethod這個(gè)方法的具體實(shí)現(xiàn)是用native方法做的章办。
下面重點(diǎn)講下這個(gè)方法的實(shí)現(xiàn)原理。這里需要引入一個(gè)概念A(yù)rtMethod滨彻,說(shuō)到ArtMethod 不得不說(shuō)Art藕届,在Android4.4 之前的運(yùn)行時(shí)環(huán)境是Dalvik虛擬機(jī),之后的是Android Runtime ,Android 中的每個(gè)方法在art中都對(duì)應(yīng)著一個(gè)ArtMethod結(jié)構(gòu)體亭饵,用面向?qū)ο蟮恼f(shuō)法就是休偶,每個(gè)方法其實(shí)都是一個(gè)ArtMethod對(duì)象,在該對(duì)象中保存著方法的類辜羊,訪問(wèn)權(quán)限踏兜,執(zhí)行地址等词顾,所以如果要替換方法,其實(shí)就要把該方法的所有ArtMethod對(duì)象的屬性全部替換成另外一個(gè)就可以了碱妆。
AndFix 中實(shí)現(xiàn)ArtMethod方法的替換是這樣實(shí)現(xiàn)的
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 23) {
replace_7_0(env, src, dest);
} else if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else if (apilevel > 19) {
replace_5_0(env, src, dest);
}else{
replace_4_4(env, src, dest);
}
}
我們可以看到肉盹,在不同Android版本的Art虛擬機(jī)里,Java對(duì)象對(duì)應(yīng)底層的數(shù)據(jù)結(jié)構(gòu)是不同的山橄,因此需要根據(jù)不同版本分別處理垮媒,分別替換不同的函數(shù)。
以replace_7_0 為例
void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
// reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_ =
// reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_ -1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->hotness_count_ = dmeth->hotness_count_;
smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_types_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
LOGD("replace_7_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}
通過(guò)env->FromReflectedMethod航棱,可以由Java層的Method對(duì)象得到這個(gè)方法對(duì)應(yīng)Native層的ArtMethod的真正起始地址睡雇。然后替換這個(gè)ArtMethod中的所有成員變量就完成了熱修復(fù)邏輯。以后調(diào)用這個(gè)方法時(shí)就會(huì)直接走到新方法的實(shí)現(xiàn)中了饮醇。這種方法雖然可以實(shí)現(xiàn)它抱,但是在特殊情況下回出現(xiàn)替換失敗的情況,如廠商對(duì)ArtMethod就行了修改朴艰。
網(wǎng)上有大神提供了優(yōu)化方案观蓄,就是對(duì)ArtMethod進(jìn)行整體替換
也就是把原先這樣的逐一替換:
更改為整體替換:
因此Andfix這一系列繁瑣的替換:
target->declaring_class_ = meth->declaring_class_;
target->access_flags_ = meth->access_flags_;
target->dex_code_item_offset_ = meth->dex_code_item_offset_;
target->dex_method_index_ = meth->dex_method_index_;
target->method_index_ = meth->method_index_;
target->hotness_count_ = meth->hotness_count_;
...
更改為:
memcpy(target,meth, sizeof(ArtMethod));
這其實(shí)也就是Sophix實(shí)現(xiàn)的熱修復(fù)方案。這樣即使ArtMethod被改動(dòng)祠墅,只要我們知道了ArtMethod的size侮穿,也就依然可以實(shí)現(xiàn)方法的替換。在Art虛擬機(jī)中毁嗦,每個(gè)類的ArtMethod在內(nèi)存中是緊密排列在一起的亲茅,所以要拿到ArtMethod的size其實(shí)也是很簡(jiǎn)單的,只要我們構(gòu)建一個(gè)類狗准,其中包含兩個(gè)靜態(tài)方法克锣,我們來(lái)比較這兩個(gè)方法的起始地址差值,就可以拿到ArtMethod的大小了腔长。
public class MeasureArtMethodSize {
public static final void M1() {
}
public static final void M2() {
}
}
具體c代碼的實(shí)現(xiàn)
JNIEXPORT jlong JNICALL
Java_com_example_edz_myapplication_hotfix_HotFixManager_measureMethodSize(JNIEnv *env,
jobject instance,
jclass c) {
size_t firMid = reinterpret_cast<size_t>(env->GetStaticMethodID(c, "M1", "()V"));
size_t nexMid = reinterpret_cast<size_t>(env->GetStaticMethodID(c, "M2", "()V"));
return nexMid - firMid;
}
通過(guò)比較兩個(gè)方法的內(nèi)存地址差異就可以拿到artMethod的大小了袭祟,接下來(lái)替換方法也就簡(jiǎn)單了
JNIEXPORT void JNICALL
Java_com_example_edz_myapplication_hotfix_HotFixManager_replaceMethod(JNIEnv *env, jobject instance,
jobject oldMethod,
jobject newMethod,
jlong size) {
jmethodID smeth = env->FromReflectedMethod(oldMethod);
jmethodID dmeth = env->FromReflectedMethod(newMethod);
memcpy(smeth, dmeth, size);
}
通過(guò)memcpy 就可以實(shí)現(xiàn)方法的替換了。以上方法其實(shí)可以使用純java來(lái)實(shí)現(xiàn)
Java中有個(gè)隱藏的類sun.misc.Unsafe,通過(guò)這個(gè)類可以實(shí)現(xiàn)獲取內(nèi)存地址捞附,和memcpy的方法巾乳,具體實(shí)現(xiàn)方式大家可以自行去查閱,網(wǎng)上實(shí)現(xiàn)方法很好找鸟召。