Andfix源碼分析

熱修復(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)方法很好找鸟召。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末想鹰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子药版,更是在濱河造成了極大的恐慌,老刑警劉巖喻犁,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件槽片,死亡現(xiàn)場(chǎng)離奇詭異何缓,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)还栓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門碌廓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人剩盒,你說(shuō)我怎么就攤上這事谷婆。” “怎么了辽聊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵纪挎,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我跟匆,道長(zhǎng)异袄,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任玛臂,我火速辦了婚禮烤蜕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘迹冤。我一直安慰自己讽营,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布泡徙。 她就那樣靜靜地躺著橱鹏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锋勺。 梳的紋絲不亂的頭發(fā)上蚀瘸,一...
    開(kāi)封第一講書(shū)人閱讀 52,785評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音庶橱,去河邊找鬼贮勃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛苏章,可吹牛的內(nèi)容都是我干的寂嘉。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼枫绅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼泉孩!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起并淋,我...
    開(kāi)封第一講書(shū)人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤寓搬,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后县耽,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體句喷,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镣典,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唾琼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兄春。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锡溯,靈堂內(nèi)的尸體忽然破棺而出赶舆,到底是詐尸還是另有隱情,我是刑警寧澤祭饭,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布芜茵,位于F島的核電站,受9級(jí)特大地震影響甜癞,放射性物質(zhì)發(fā)生泄漏夕晓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一悠咱、第九天 我趴在偏房一處隱蔽的房頂上張望蒸辆。 院中可真熱鬧,春花似錦析既、人聲如沸躬贡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拂玻。三九已至,卻和暖如春宰译,著一層夾襖步出監(jiān)牢的瞬間檐蚜,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工沿侈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闯第,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓缀拭,卻偏偏與公主長(zhǎng)得像咳短,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蛛淋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361