熱修復框架源碼剖析(上)

前言

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

基於dex分包的熱修復技術應該是QQ空間團隊最早提出來的,但是他們只是經過技術文章分享了實現原理,其自己的源碼並無公開,因此QQ的熱修復實現細節以及編碼風格是沒有機會觀摩了,可是仍是有不少團隊基於QQ空間介紹的原理實現了熱修復而且公開了源碼,好比@dodola大神的RocooFix和AnoleFix(沒錯,他弄了倆),還有一個是在餓了麼工做的Android前輩開發的Amigojava

由於這位前輩特地在個人熱修復初探這篇文章下面留言向我宣傳他的框架,因此首先我想來分析他的熱修復實現細節。不過他本身也已經寫了源碼解讀,雖然因爲目前的代碼的更新致使他的源碼解讀和源碼有部分差別,但整體來講邏輯是一致的。因此實際上我沒有必要在這裏詳細的分析他的框架,只挑主要的來說。android

Amigo熱修復框架剖析

Amigo github: github.com/eleme/Amigogit

總得來講,從我看代碼的狀況來看,這是一個比較完備的,能夠應用的熱修復框架,從檢測apk,到取出資源文件,dex文件,再到插入dex包到dexElements中,在重啓apk一系列過程都比較完善,考慮周到。因此,在這裏我只想講一件Amigo具體是如何將dex插入到dexElements中的,由於這個纔是基於dex分包的熱修復技術的關鍵,不過他的修復方式和QQ空間團隊提出的de仍是有一點不一樣。github

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()方法app

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()究竟是什麼鬼?less

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須要放在特定目錄下才行。ide

好了,扯了這麼遠,咱們仍是趕忙回到正題,替換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和屬性......

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

勘誤

暫無

後記

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

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

相關文章
相關標籤/搜索