熱修復(fù)框架源碼剖析

前言

在一個多月前,我寫過一篇熱修復(fù)初探桨嫁,主要介紹了各種被廣泛討論和使用的熱修復(fù)的技術(shù)實現(xiàn)原理强法,在那篇文章中,我也說自己會繼續(xù)研究基于dex分包的熱修復(fù)技術(shù)的源碼膏萧。

基于dex分包的熱修復(fù)技術(shù)應(yīng)該是QQ空間團隊最先提出來的漓骚,可是他們只是通過技術(shù)文章分享了實現(xiàn)原理,其本身的源碼并沒有公開榛泛,所以QQ的熱修復(fù)實現(xiàn)細節(jié)以及編碼風(fēng)格是沒有機會觀摩了蝌蹂,但是還是有很多團隊基于QQ空間介紹的原理實現(xiàn)了熱修復(fù)并且公開了源碼,比如@dodola大神的RocooFix和AnoleFix(沒錯曹锨,他弄了倆)叉信,還有一個是在餓了么工作的Android前輩開發(fā)的Amigo

因為這位前輩特意在我的熱修復(fù)初探這篇文章下面留言向我宣傳他的框架艘希,所以首先我想來分析他的熱修復(fù)實現(xiàn)細節(jié)硼身。不過他自己也已經(jīng)寫了源碼解讀,雖然由于目前的代碼的更新導(dǎo)致他的源碼解讀和源碼有部分差異,但總體來說邏輯是一致的覆享。所以實際上我沒有必要在這里詳細的分析他的框架佳遂,只挑主要的來講。

Amigo熱修復(fù)框架剖析

Amigo github: https://github.com/eleme/Amigo

總得來說撒顿,從我看代碼的情況來看丑罪,這是一個比較完備的,可以應(yīng)用的熱修復(fù)框架凤壁,從檢測apk,到取出資源文件吩屹,dex文件,再到插入dex包到dexElements中拧抖,在重啟apk一系列過程都比較完善煤搜,考慮周到。所以唧席,在這里我只想講一件Amigo具體是如何將dex插入到dexElements中的擦盾,因為這個才是基于dex分包的熱修復(fù)技術(shù)的關(guān)鍵,不過他的修復(fù)方式和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方法里調(diào)用了runPatchApk(),開始準(zhǔn)備替換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();
            }
            //創(chuàng)建一個繼承自PathClassLoader的類的對象迹卢,把補丁APK的路徑傳入構(gòu)造一個加載器
            AmigoClassLoader amigoClassLoader = new AmigoClassLoader(demoAPk.getAbsolutePath(), getRootClassLoader());
            //這個方法是將該app所對應(yīng)的ActivityThread對象中LoadApk的加載器通過反射的方式替換掉。
            setAPKClassLoader(amigoClassLoader);
            //這個就是準(zhǔn)備替換dex的方法
            setDexElements(amigoClassLoader);
            //顧名思義徒仓,設(shè)置加載本地庫
            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)這個方法,先看看設(shè)置類加載器的setAPKClassLoader(amigoClassLoader)方法掉弛,因為這也是很難忽略的一個關(guān)鍵點症见,因此喂走,我們先看看他是怎么設(shè)置加載器的

 private void setAPKClassLoader(ClassLoader classLoader)
            throws IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException {
        //把getLoadedApk()返回的對象中“mClassLoader”屬性替換成我們剛才自己new的類加載器
        writeField(getLoadedApk(), "mClassLoader", classLoader);
    }
    

writeFiled這個方法的主要功能就是通過反射的機制,把我們的classloader設(shè)置到mClassLoader中去筒饰,關(guān)鍵是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) {
                //最終應(yīng)該返回了一個LoadedApk
                return wr.get();
            }
        }
        return null;
    }

好了瓷们,最終得到了LoadedApk對象,這個對象其實很重要秒咐,一個 apk加載之后所有信息都保存在此對象(比如:DexClassLoader谬晕、Resources、Application)携取,一個包對應(yīng)一個對象攒钳,以包名區(qū)別,而我們正好就用我們自己的類加載器對象替換掉這個LoadedApk對象中的classloader,就可以加載我們自己的apk了雷滋。由于我們自己的amigoClassLoader實際上繼承自PathClassLoader不撑,所以智能加載特定目錄下的apk,也就是說,我們的補丁apk需要放在特定目錄下才行晤斩。

好了焕檬,扯了這么遠,我們還是趕緊回到正題澳泵,替換dex實現(xiàn)熱修復(fù)实愚。繼續(xù)從setDexElements(amigoClassLoader)往下走

private void setDexElements(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException {
        //getPathList這是通過反射的方式去讀取BaseDexClassLoader中的pathList對象,這個對象中有一個dexElements數(shù)組兔辅,包裹了運行的APK中的所有的dex腊敲。
        Object dexPathList = getPathList(classLoader);
        //文件目錄下,補丁apk的dex文件對象數(shù)組
        File[] listFiles = dexDir.listFiles();

        List<File> validDexes = new ArrayList<>();
        for (File listFile : listFiles) {
            if (listFile.getName().endsWith(".dex")) {
                //添加到列表中
                validDexes.add(listFile);
            }
        }
        //創(chuàng)建一個一樣大的文件數(shù)組
        File[] dexes = validDexes.toArray(new File[validDexes.size()]);
        //通過反射讀取dexPathList對象中的原本的dexElements數(shù)組對象
        Object originDexElements = readField(dexPathList, "dexElements");
        //返回dexElements數(shù)組中元素的類型
        Class<?> localClass = originDexElements.getClass().getComponentType();
        int length = dexes.length;
        //然后根據(jù)這個類型創(chuàng)建一個同樣大的新數(shù)組
        Object dexElements = Array.newInstance(localClass, length);
        for (int k = 0; k < length; k++) {
            為數(shù)組賦值
            Array.set(dexElements, k, getElementWithDex(dexes[k], optimizedDir));
        }
        //最后维苔,通過反射的方式把這個新數(shù)組放到dexPathList這個對象中去碰辅。
        writeField(dexPathList, "dexElements", dexElements);
    }

好了,現(xiàn)在對于dex的替換基本上完成了介时,最后是一些重啟或者重新運行Application的工作乎赴。假如對于BaseDexClassLoader,dexPathList潮尝,dexElements這些還不是很清楚榕吼,可以看一看我之前的那篇文章熱修復(fù)初探,里面有相關(guān)的介紹。

小結(jié)

如果你真的認真看了我的上一篇文章熱修復(fù)初探的話勉失,你會發(fā)現(xiàn)這個框架其實跟我介紹了那種基于dex分包的熱修復(fù)原理還有一些出入羹蚣,因為這是整體把所有的dex包的替換掉,也就意味著當(dāng)需要熱修復(fù)時乱凿,下載的文件要大一些顽素,可能是整個apk咽弦;其次,這個框架使用的類加載器是PathClassLoader而不是DexClassLoader胁出,本來PathClassLoader是有局限的型型,因為它只能加載指定的私有路徑,而作者通過大量使用了反射的方式全蝶,直接替換原來的類加載器闹蒜,然后通過自己的類加載器來完成整個dex的完全替換∫忠總體來看绷落,這個框架除了體積較大,優(yōu)點是很多的始苇。(不過這么使用反射砌烁,APP應(yīng)該很難在Google play中上線吧?)

本來我工作中對于反射基本沒用到催式,所以算不上熟悉函喉,但是現(xiàn)在看來,這玩兒真的很好使啊荣月,因為用這種方式函似,可以獲取很多Android系統(tǒng)不公開的私有API和屬性......

臥槽,我決定好好研究反射,我發(fā)四。

勘誤

暫無

后記

本來還有繼續(xù)分析其他的熱修復(fù)框架源碼礁芦,但是這篇文章的篇幅已經(jīng)不小了,中場休息蔑担,找機會我再把其他的框架源碼的實現(xiàn)細節(jié)寫在新的文章中分享出來

最后是各個熱修復(fù)框架的性能表(不保證準(zhǔn)確)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咽白,隨后出現(xiàn)的幾起案子啤握,更是在濱河造成了極大的恐慌,老刑警劉巖晶框,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件排抬,死亡現(xiàn)場離奇詭異,居然都是意外死亡授段,警方通過查閱死者的電腦和手機蹲蒲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侵贵,“玉大人届搁,你說我怎么就攤上這事。” “怎么了卡睦?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵宴胧,是天一觀的道長。 經(jīng)常有香客問我表锻,道長恕齐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任瞬逊,我火速辦了婚禮显歧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘码耐。我一直安慰自己追迟,他們只是感情好溶其,可當(dāng)我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布骚腥。 她就那樣靜靜地躺著,像睡著了一般瓶逃。 火紅的嫁衣襯著肌膚如雪束铭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天厢绝,我揣著相機與錄音契沫,去河邊找鬼。 笑死昔汉,一個胖子當(dāng)著我的面吹牛懈万,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播靶病,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼会通,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了娄周?” 一聲冷哼從身側(cè)響起涕侈,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎煤辨,沒想到半個月后裳涛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡众辨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年端三,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹃彻。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡技肩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虚婿,我是刑警寧澤旋奢,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站然痊,受9級特大地震影響至朗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剧浸,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一锹引、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧唆香,春花似錦嫌变、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冯吓,卻和暖如春倘待,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背组贺。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工凸舵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人失尖。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓啊奄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掀潮。 傳聞我的和親對象是個殘疾皇子菇夸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容