熱修復框架原始碼剖析(上)

拉丁吳發表於2016-10-09

前言

在一個多月前,我寫過一篇熱修復初探,主要介紹了各種被廣泛討論和使用的熱修復的技術實現原理,在那篇文章中,我也說自己會繼續研究基於dex分包的熱修復技術的原始碼。

基於dex分包的熱修復技術應該是QQ空間團隊最先提出來的,可是他們只是通過技術文章分享了實現原理,其本身的原始碼並沒有公開,所以QQ的熱修復實現細節以及編碼風格是沒有機會觀摩了,但是還是有很多團隊基於QQ空間介紹的原理實現了熱修復並且公開了原始碼,比如@dodola大神的RocooFix和AnoleFix(沒錯,他弄了倆),還有一個是在餓了麼工作的Android前輩開發的Amigo

因為這位前輩特意在我的熱修復初探這篇文章下面留言向我宣傳他的框架,所以首先我想來分析他的熱修復實現細節。不過他自己也已經寫了原始碼解讀,雖然由於目前的程式碼的更新導致他的原始碼解讀和原始碼有部分差異,但總體來說邏輯是一致的。所以實際上我沒有必要在這裡詳細的分析他的框架,只挑主要的來講。

Amigo熱修復框架剖析

Amigo github: github.com/eleme/Amigo

總得來說,從我看程式碼的情況來看,這是一個比較完備的,可以應用的熱修復框架,從檢測apk,到取出資原始檔,dex檔案,再到插入dex包到dexElements中,在重啟apk一系列過程都比較完善,考慮周到。所以,在這裡我只想講一件Amigo具體是如何將dex插入到dexElements中的,因為這個才是基於dex分包的熱修復技術的關鍵,不過他的修復方式和QQ空間團隊提出的de還是有一點不同。

Amigo.java

 @Override
    public void onCreate() {
        super.onCreate();
        ......
        ......
        ......
        Log.e(TAG, "demoAPk.exists-->" + demoAPk.exists() + ", this--->" + this);

        ClassLoader originalClassLoader = getClassLoader();

        try {
            SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_MULTI_PROCESS);

            if (checkUpgrade(sp)) {
                Log.e(TAG, "upgraded host app");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!demoAPk.exists()) {
                Log.e(TAG, "demoApk not exist");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!isSignatureRight(this, demoAPk)) {
                Log.e(TAG, "signature is illegal");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!checkPatchApkVersion(this, demoAPk)) {
                Log.e(TAG, "patch apk version cannot be less than host apk");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(sp)) {
                Log.e(TAG, "none main process and patch apk is not released yet");
                runOriginalApplication(originalClassLoader);
                return;
            }

            // only release loaded apk in the main process
            runPatchApk(sp); //這是最重要的一句話
            ......
            ......
            ......
    }複製程式碼

在Amigo這個類的onCreate方法裡呼叫了runPatchApk(),開始準備替換apk.再檢視這個runPatchApk()方法

  private void runPatchApk(SharedPreferences sp) throws LoadPatchApkException {
        try {
            String demoApkChecksum = getCrc(demoAPk);
            boolean isFirstRun = isPatchApkFirstRun(sp);
            Log.e(TAG, "demoApkChecksum-->" + demoApkChecksum + ", sig--->" + sp.getString(NEW_APK_SIG, ""));
            if (isFirstRun) {
                //clear previous working dir
                Amigo.clearWithoutApk(this);

                //start a new process to handle time-tense operation
                ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA);
                String layoutName = appInfo.metaData.getString("amigo_layout");
                String themeName = appInfo.metaData.getString("amigo_theme");
                int layoutId = 0;
                int themeId = 0;
                if (!TextUtils.isEmpty(layoutName)) {
                    layoutId = (int) readStaticField(Class.forName(getPackageName() + ".R$layout"), layoutName);
                }
                if (!TextUtils.isEmpty(themeName)) {
                    themeId = (int) readStaticField(Class.forName(getPackageName() + ".R$style"), themeName);
                }
                Log.e(TAG, String.format("layoutName-->%s, themeName-->%s", layoutName, themeName));
                Log.e(TAG, String.format("layoutId-->%d, themeId-->%d", layoutId, themeId));

                ApkReleaser.work(this, layoutId, themeId);
                Log.e(TAG, "release apk once");
            } else {
                checkDexAndSoChecksum();
            }
            //建立一個繼承自PathClassLoader的類的物件,把補丁APK的路徑傳入構造一個載入器
            AmigoClassLoader amigoClassLoader = new AmigoClassLoader(demoAPk.getAbsolutePath(), getRootClassLoader());
            //這個方法是將該app所對應的ActivityThread物件中LoadApk的載入器通過反射的方式替換掉。
            setAPKClassLoader(amigoClassLoader);
            //這個就是準備替換dex的方法
            setDexElements(amigoClassLoader);
            //顧名思義,設定載入本地庫
            setNativeLibraryDirectories(amigoClassLoader);
            //下面是載入一些資原始檔
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = getDeclaredMethod(AssetManager.class, "addAssetPath", String.class);
            addAssetPath.setAccessible(true);
            addAssetPath.invoke(assetManager, demoAPk.getAbsolutePath());
            setAPKResources(assetManager);

            runOriginalApplication(amigoClassLoader);
        } catch (Exception e) {
            throw new LoadPatchApkException(e);
        }
    }複製程式碼

在此,我們先不進入setDexElements(amigoClassLoader)這個方法,先看看設定類載入器的setAPKClassLoader(amigoClassLoader)方法,因為這也是很難忽略的一個關鍵點,因此,我們先看看他是怎麼設定載入器的

 private void setAPKClassLoader(ClassLoader classLoader)
            throws IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException {
        //把getLoadedApk()返回的物件中“mClassLoader”屬性替換成我們剛才自己new的類載入器
        writeField(getLoadedApk(), "mClassLoader", classLoader);
    }複製程式碼

writeFiled這個方法的主要功能就是通過反射的機制,把我們的classloader設定到mClassLoader中去,關鍵是getLoadedApk()到底是什麼鬼?

 private static Object getLoadedApk()
            throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException {
        //instance()返回一個“android.app.ActivityThread”類,readField是讀取ActivityThread類中的mPackages屬性
        Map<String, WeakReference<Object>> mPackages = (Map<String, WeakReference<Object>>) readField(instance(), "mPackages", true);
        //而這個mPackage屬性中包含有一個LoadedApk
        for (String s : mPackages.keySet()) {
            WeakReference wr = mPackages.get(s);
            if (wr != null && wr.get() != null) {
                //最終應該返回了一個LoadedApk
                return wr.get();
            }
        }
        return null;
    }複製程式碼

好了,最終得到了LoadedApk物件,這個物件其實很重要,一個 apk載入之後所有資訊都儲存在此物件(比如:DexClassLoader、Resources、Application),一個包對應一個物件,以包名區別,而我們正好就用我們自己的類載入器物件替換掉這個LoadedApk物件中的classloader,就可以載入我們自己的apk了。由於我們自己的amigoClassLoader實際上繼承自PathClassLoader,所以智慧載入特定目錄下的apk,也就是說,我們的補丁apk需要放在特定目錄下才行。

好了,扯了這麼遠,我們還是趕緊回到正題,替換dex實現熱修復。繼續從setDexElements(amigoClassLoader)往下走

private void setDexElements(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException {
        //getPathList這是通過反射的方式去讀取BaseDexClassLoader中的pathList物件,這個物件中有一個dexElements陣列,包裹了執行的APK中的所有的dex。
        Object dexPathList = getPathList(classLoader);
        //檔案目錄下,補丁apk的dex檔案物件陣列
        File[] listFiles = dexDir.listFiles();

        List<File> validDexes = new ArrayList<>();
        for (File listFile : listFiles) {
            if (listFile.getName().endsWith(".dex")) {
                //新增到列表中
                validDexes.add(listFile);
            }
        }
        //建立一個一樣大的檔案陣列
        File[] dexes = validDexes.toArray(new File[validDexes.size()]);
        //通過反射讀取dexPathList物件中的原本的dexElements陣列物件
        Object originDexElements = readField(dexPathList, "dexElements");
        //返回dexElements陣列中元素的型別
        Class<?> localClass = originDexElements.getClass().getComponentType();
        int length = dexes.length;
        //然後根據這個型別建立一個同樣大的新陣列
        Object dexElements = Array.newInstance(localClass, length);
        for (int k = 0; k < length; k++) {
            為陣列賦值
            Array.set(dexElements, k, getElementWithDex(dexes[k], optimizedDir));
        }
        //最後,通過反射的方式把這個新陣列放到dexPathList這個物件中去。
        writeField(dexPathList, "dexElements", dexElements);
    }複製程式碼

好了,現在對於dex的替換基本上完成了,最後是一些重啟或者重新執行Application的工作。假如對於BaseDexClassLoader,dexPathList,dexElements這些還不是很清楚,可以看一看我之前的那篇文章熱修復初探,裡面有相關的介紹。

小結

如果你真的認真看了我的上一篇文章熱修復初探的話,你會發現這個框架其實跟我介紹了那種基於dex分包的熱修復原理還有一些出入,因為這是整體把所有的dex包的替換掉,也就意味著當需要熱修復時,下載的檔案要大一些,可能是整個apk;其次,這個框架使用的類載入器是PathClassLoader而不是DexClassLoader,本來PathClassLoader是有侷限的,因為它只能載入指定的私有路徑,而作者通過大量使用了反射的方式,直接替換原來的類載入器,然後通過自己的類載入器來完成整個dex的完全替換。總體來看,這個框架除了體積較大,優點是很多的。(不過這麼使用反射,APP應該很難在Google play中上線吧?)

本來我工作中對於反射基本沒用到,所以算不上熟悉,但是現在看來,這玩兒真的很好使啊,因為用這種方式,可以獲取很多Android系統不公開的私有API和屬性......

臥槽,我決定好好研究反射,我發四。

勘誤

暫無

後記

本來還有繼續分析其他的熱修復框架原始碼,但是這篇文章的篇幅已經不小了,中場休息,找機會我再把其他的框架原始碼的實現細節寫在新的文章中分享出來

最後是各個熱修復框架的效能表(不保證準確)

熱修復框架原始碼剖析(上)

相關文章