騰訊hotfix分析

weixin_34389926發表於2016-09-01

在逆向騰訊某產品時,發現很多方法的前面都有這麼一段程式碼:

 if(NotDoVerifyClasses.DO_VERIFY_CLASSES) {
     System.out.print(AntiLazyLoad.class);
 }

NotDoVerifyClasses和AntiLazyLoad在dex中居然找不到,這勾起我的興趣。先來debug看看,到底是從哪裡載入的這兩個類:

1784193-e2d2e4cc71280f49.png

ok,發現是從files下的verify.jar載入的,找到這個jar發編譯一下:

package com.tencent;

public class AntiLazyLoad {
    public AntiLazyLoad() {
        super();
    }
}
package com.tencent;

public class NotDoVerifyClasses {
    public static boolean DO_VERIFY_CLASSES;

    static {
        NotDoVerifyClasses.DO_VERIFY_CLASSES = false;
    }

    public NotDoVerifyClasses() {
        super();
    }
}

程式碼很簡單,感覺像是個樁,但這個有什麼用?

從檔案路徑我們知道有hotfix,應該和熱修復有關,深入研究一下。

我們先來看看另外一個問題:在前面的截圖中,我們看到classloader是PathClassLoader,其pathList中居然被增加一個files目錄下的jar,這個是怎麼做到的?一般來說動態載入應該是DexClassLoader才對。

通過搜尋verify.jar,發現了關鍵程式碼:

public static boolean inject(Context ctx, String dexPath, String arg5, String odexPath, String clazzName, boolean arg8) {
    boolean v0 = false;
    if(dexPath != null && (new File(dexPath).exists())) {
        if(DexUtil.isAliyunOs()) {
            try {
                DexUtil.injectInAliyunOs(ctx, dexPath, arg5, odexPath, clazzName, arg8);
                v0 = true;
            }
            catch(Throwable v0_1) {
                PatchLog.e("SystemClassLoaderInjector", "fail to inject", v0_1);
                throw v0_1;
            }
        }
        else if(!DexUtil.hasBaseDexClassLoader()) {
            try {
                DexUtil.injectBelowApiLevel14(ctx, dexPath, arg5, odexPath, clazzName, arg8);
                v0 = true;
            }
            catch(Throwable v0_1) {
                PatchLog.e("SystemClassLoaderInjector", "fail to inject", v0_1);
                throw v0_1;
            }
        }
        else {
            try {
                DexUtil.injectAboveEqualApiLevel14(ctx, dexPath, arg5, odexPath, clazzName, arg8);
                v0 = true;
            }
            catch(Throwable v0_1) {
                PatchLog.e("SystemClassLoaderInjector", "fail to inject", v0_1);
                throw v0_1;
            }
        }
    }

    return v0;
}

不同版本做了不同的Classloader注入方案,我是4.4的手機,對應看看injectAboveEqualApiLevel14

private static void injectAboveEqualApiLevel14(Context ctx, String dexPath, String libPath, String odexPath, String arg9, boolean arg10) {
    ClassLoader oldClassloader = ctx.getClassLoader();
    DexClassLoader dexClassLoader = new DexClassLoader(dexPath, odexPath, libPath, ctx.getClassLoader());
    Object v2 = DexUtil.combineArray(DexUtil.getDexElements(DexUtil.getPathList(oldClassloader)), 
            DexUtil.getDexElements(DexUtil.getPathList(dexClassLoader)), arg10);
    Object v0_1 = DexUtil.getPathList(oldClassloader);
    DexUtil.setField(v0_1, v0_1.getClass(), "dexElements", v2);
    if(!TextUtils.isEmpty(((CharSequence)arg9))) {
        dexClassLoader.loadClass(arg9);
        PatchLog.e("DexUtil", "load clas " + arg9 + " success ");
    }
}

原理弄明白了:先通過DexClassLoader載入files下的jar,然後反射獲取其dexElements,然後合併到PathClassLoader的dexElements中,很巧妙的做法,其他注入方案不再詳述。

上述這個過程在Application的attachBaseContext中就完成,即app一執行就會載入。也就是說,加入某個時候更新了files下的verify.jar,在下次啟動app時,修改後的verify.jar程式碼就會被載入。 前面最開始看到有很多方法執行前就呼叫的樁,如果精心去設計NotDoVerifyClasses和AntiLazyLoad的程式碼,在方法體執行前就能被執行,確實可以做到熱修復。

然而,其hotfix目錄下不僅僅是上面的程式碼,上面也沒有講述其是如何從伺服器端拉取熱修復的程式碼的, hotfix下程式碼如下:

1784193-ab9f003672eab348.png

程式碼有點多,我耐著性子看完了,截圖中的這一坨程式碼是為了做補丁管理的,其中就包括從服務端去拉取補丁,然後儲存到files下的hotfix目錄。假如服務端返回的補丁名字叫做verify,那麼就會覆蓋verify.jar,我想騰訊他們在推送熱修復補丁時,肯定是推送這樣一個補丁。

我記得阿里之前開源了一個修改自xposed的熱修復框架:dexposed,其利用xposed的hook原理,可以做到hook任何一個java方法,效果也是不錯的,但xposed因為很多適配問題,所以要產品化效果不太好。

騰訊的這種熱修復方案是通過硬編碼方式植入樁,假如植入方式能夠工具化,也是一個不錯的選擇,至少沒有適配問題。

關於上面的熱修復原理,騰訊有官方介紹,發現自己之前有一些理解不到位的地方,原理參考:
https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a&scene=1&srcid=1106Imu9ZgwybID13e7y2nEi#wechat_redirect

搜尋了一下熱修復,原來是2015年很火的一個技術(孤陋寡聞了),現在已經有很多成熟的方案,上述的hotfix只是其中之一,想了解更多可以自行去搜尋。

1784193-04df0a5349bbd0fb.png

相關文章