Android_熱修復(fù)_Tinker原理分析

熱修復(fù)

目前國內(nèi)Android熱修復(fù)技術(shù)已經(jīng)發(fā)展的可以說百花齊放了,從實(shí)現(xiàn)方式來大致分類,可以分為:
① Native層實(shí)現(xiàn)
② Java層實(shí)現(xiàn)

之前有簡單分析過阿里開源的Andfix實(shí)現(xiàn)原理(基于Native層),詳見Android_熱修復(fù)_Andfix原理分析,這里就多說了,感興趣的可以看下
本文簡單分析一下Java層實(shí)現(xiàn)熱修復(fù)邏輯,簡單實(shí)現(xiàn)代碼熱修復(fù)Demo,以Tinker為例(當(dāng)然Tinker是支持代碼修復(fù),資源修復(fù),so修復(fù)的,感興趣的小伙伴自行移步官網(wǎng)~)

先梳理下思路:

Java類編譯過程

就是java類通過javac編譯成.class文件,再由dx.bat編譯成.dex文件的過程,不做贅述,簡單畫張圖~


Java類編譯

ClassLoader簡介

Android中的java.lang.ClassLoader這個(gè)類也不同于Java中的java.lang.ClassLoader狰闪。
Android中的ClassLoader類型也可分為系統(tǒng)ClassLoader和自定義ClassLoader挤渔。其中系統(tǒng)ClassLoader包括3種分別是:

  • BootClassLoader挎春,Android系統(tǒng)啟動時(shí)會使用BootClassLoader來預(yù)加載常用類参滴,與Java中的Bootstrap ClassLoader不同的是泡一,它并不是由C/C++代碼實(shí)現(xiàn)加缘,而是由Java實(shí)現(xiàn)的。BootClassLoader是ClassLoader的一個(gè)內(nèi)部類韧骗。
  • PathClassLoader翎苫,全名是dalvik/system.PathClassLoader权埠,可以加載已經(jīng)安裝的Apk,也就是/data/app/package 下的apk文件煎谍,也可以加載/vendor/lib, /system/lib下的nativeLibrary攘蔽。
  • DexClassLoader,全名是dalvik/system.DexClassLoader呐粘,可以加載一個(gè)未安裝的apk文件满俗。
    PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中作岖。
    下圖展示了Android中的ClassLoader中的繼承體系唆垃,其中SecureClassLoader和UrlClassLoader是在Java中的類加載器,在Android中是沒法辦使用的鳍咱。
image.png

.dex文件加載

通過源碼得知,.dex文件是通過BaseDexClassLoader類(ClassLoader的子類)進(jìn)行加載的,這個(gè)類里面有個(gè)成員變量DexPathList對象,而這個(gè)對象中有個(gè)有個(gè)數(shù)組存放的是DexElement對象,也就是從文件中加載的.dex文件,切入點(diǎn)就在這里,對于項(xiàng)目來說,一般項(xiàng)目會分包(方法數(shù)大于64k,及大于65535個(gè)時(shí)候,google提供的分包策略),如果采用Java代碼實(shí)現(xiàn)熱修復(fù),分包是肯定要做的,因?yàn)橐WC主包沒有bug,分包簡單說就是打包的apk一般有多個(gè).dex文件,如 classes.dex,classes2.dex 等等...
那么比如我們的classes2.dex中某個(gè)類的某個(gè)方法出現(xiàn)了異常,我們就可以創(chuàng)建一個(gè)修復(fù)包(修復(fù)后的classes2.dex文件),然后通過自定義的類加載器將修復(fù)后的classes2.dex文件copy到私有目錄,再插隊(duì)到系統(tǒng)ClassLoaderdexPathList對象dexElement的數(shù)組中,讓系統(tǒng)優(yōu)先加載修復(fù)后的classes2.dex文件,以做到熱修復(fù)的目的,這種實(shí)現(xiàn)方式必須要執(zhí)行修復(fù)邏輯后,重啟app才能實(shí)現(xiàn)效果~
了解了這些信息大致思路就有了,我們需要修復(fù)后的.dex文件加載解析,然后插隊(duì)舊的安裝包裝的.dex文件,做到插隊(duì)的操作,相當(dāng)于欺騙了Android系統(tǒng),大致如下:

實(shí)現(xiàn)原理

思路大概是,我們需要一個(gè)修復(fù)bug的.dex文件,插隊(duì)到BaseDexClassLoader類下的DexPathList對象DexElement數(shù)組中,并且排序到最前面,讓系統(tǒng)加載到我們修復(fù)后的.dex文件不會再加載有bug的dex文件,完成插隊(duì)(插裝),這里會有個(gè)類加載機(jī)制的知識,本文不做詳細(xì)介紹,后面會單獨(dú)寫一篇總結(jié)~
大致實(shí)現(xiàn)步驟如下:

步驟

Demo實(shí)現(xiàn)

① 基礎(chǔ)配置-主包配置

配置分包,配置分包的目的主要是打包做出來的apk會有多個(gè).dex文件,實(shí)際項(xiàng)目應(yīng)用中要保證主包不要有bug,demo中加載.dex文件的時(shí)候也排除了主包文件classes.dex,大致如下:
創(chuàng)建BaseApplication,BaseActivity,MainActivity放置在主包,其中MainActivity主要是為了分包占位,只做了點(diǎn)擊跳轉(zhuǎn)分包中SecondActivity的邏輯
app目錄下的build.gradle開啟分包支持,在androiddefaultConfig下增加配置,其中multiDex-config.txt是配置保留在主包內(nèi)類文件

 //開啟分包
        multiDexEnabled true
        //分包的配置,將配置文件中的放置在主包
        multiDexKeepFile file("multiDex-config.txt")

添加分包依賴:

  //multidex分包依賴
    implementation 'com.android.support:multidex:1.0.3'

Application開啟分包:

public class BaseApplication extends MultiDexApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //安裝分包配置
        MultiDex.install(this);
    }
}
② 分包配置

分包就創(chuàng)建了一個(gè)SecondActivity類做模擬異常和修復(fù)異常的入口,和一個(gè)Calculate模擬異常,做了10/0的操作,修復(fù)后為10/1
注:獲取修復(fù)后的classes2.dex文件可以通過直接buildapk直接解壓獲取,或者用build-tools下的dx.bat執(zhí)行命令獲取,這篇文章中提及: Android_熱修復(fù)_Andfix原理分析

簡單貼下SecondActivity,點(diǎn)擊fix按鈕后的代碼,完整代碼見文末~:

  private void update() {
        //將下載的修復(fù)包,復(fù)制到私有目錄,解壓從.dex文件中取到對應(yīng)的.class文件
        //從sd卡取修復(fù)包
        File sourceFile = new File(Environment.getExternalStorageDirectory(), Constants.DEX_NAME);
        //目標(biāo)文件
        File targetFile = new File(getDir(Constants.DEX_DIR, Context.MODE_PRIVATE).getAbsolutePath() + File.separator + Constants.DEX_NAME);
        if (targetFile.exists()) {
            targetFile.delete();
            Log.e("update","刪除原有dex文件(已使用的)");
        }
        //將SD卡中的修復(fù)包c(diǎn)opy到私有目錄
        FileUtils.copyFile(sourceFile,targetFile);
        Log.e("update","copy完成");
        FixDexUtils.loadDexFile(this);
    }
③FixModule

新建一個(gè)Module處理熱修復(fù)的相關(guān)邏輯

image.png

只有五個(gè)文件,核心文件代碼在FixDexUtils,其它是工具類,還有個(gè)定義了幾個(gè)常量
看下FixDexUtils中的代碼

public class FixDexUtils {

    //修復(fù)文件可能有多個(gè)
    private static HashSet<File> loadedDex = new HashSet<>();

    //不建議這么寫,demo演示用
    static {
        loadedDex.clear();
    }

    public static void loadDexFile(Context context) {
        //獲取私有目錄
        File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
        //遍歷篩選私有目錄中的.dex文件
        File[] listFiles = fileDir.listFiles();

        for (int i = 0; i < listFiles.length; i++) {
            //文件名以.dex結(jié)尾,且不是主包.dex文件
            if (listFiles[i].getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equalsIgnoreCase(listFiles[i].getName())) {
                loadedDex.add(listFiles[i]);
            }
        }
        //創(chuàng)建自定義的類加載器
        createDexClassLoader(context ,fileDir);
    }

    /**
     * @param context
     * @param fileDir
     * 創(chuàng)建自己的類加載器,加載私有目錄的.dex文件,上面已經(jīng)將修復(fù)包中的dex文件copy到私有目錄
     */
    private static void createDexClassLoader(Context context, File fileDir) {
        //解壓目錄
       String optimizedDir = fileDir.getAbsolutePath()+File.separator+"opt_dex";
        File fileOpt = new File(optimizedDir);
        if (!fileOpt.exists()) {
            fileOpt.mkdirs();
        }

        for (File dex : loadedDex) {
            //創(chuàng)建自己的類加載器,臨時(shí)的
            DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDir, null, context.getClassLoader());
            //有一個(gè)修復(fù)文件,就插裝一次
            hotFix(classLoader,context);
        }
    }

    private static void hotFix(DexClassLoader classLoader, Context context) {
        try {
            //獲取系統(tǒng)的PathClassLoader類加載器
            PathClassLoader pathClassLoader = (PathClassLoader)context.getClassLoader();
            //獲取自己的dexElements數(shù)組

            Object myElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));
            //獲取系統(tǒng)的dexElements數(shù)組
            Object systemElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathClassLoader));
            //合并數(shù)組,并排序,生成一個(gè)新的數(shù)組
            Object dexElements=ArrayUtils.combineArray(myElements,systemElements);
            //通過反射獲取系統(tǒng)的pathList屬性
            Object systemPathList = ReflectUtils.getPathList(pathClassLoader);
            //通過反射,將合并后新的dexElements賦值給系統(tǒng)的pathList
            ReflectUtils.setFieldValue(systemPathList,"dexElements",dexElements);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

主要做的工作就是:就是上面那張流程圖,就是先通過遍歷,解壓等操作,獲取到需要執(zhí)行熱修復(fù)的.dex文件集合,遍歷該集合,對應(yīng)每次創(chuàng)建一個(gè)臨時(shí)的DexClassLoader,然后執(zhí)行修復(fù)步驟,細(xì)分就是六步:

image.png

最終實(shí)現(xiàn)效果如圖(Demo使用手機(jī)是華為8.0的手機(jī)):
注:這里為了效果圖更直觀,已經(jīng)重啟過一次app
注: 這種方式實(shí)現(xiàn)的熱修復(fù)必須要重啟App才可以實(shí)現(xiàn)修復(fù),這一點(diǎn)也是類加載機(jī)制決定的,如下圖中修復(fù)之后再次打開加載執(zhí)行修復(fù)后的classes.dex文件是在BaseApplication中調(diào)用了修復(fù)方法,詳見完整代碼

效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末降盹,一起剝皮案震驚了整個(gè)濱河市与柑,隨后出現(xiàn)的幾起案子谤辜,更是在濱河造成了極大的恐慌,老刑警劉巖价捧,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丑念,死亡現(xiàn)場離奇詭異,居然都是意外死亡结蟋,警方通過查閱死者的電腦和手機(jī)脯倚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人推正,你說我怎么就攤上這事恍涂。” “怎么了植榕?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵再沧,是天一觀的道長。 經(jīng)常有香客問我尊残,道長炒瘸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任寝衫,我火速辦了婚禮顷扩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘慰毅。我一直安慰自己隘截,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布事富。 她就那樣靜靜地躺著技俐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪统台。 梳的紋絲不亂的頭發(fā)上雕擂,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機(jī)與錄音贱勃,去河邊找鬼井赌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贵扰,可吹牛的內(nèi)容都是我干的仇穗。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼戚绕,長吁一口氣:“原來是場噩夢啊……” “哼纹坐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起舞丛,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤耘子,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后球切,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谷誓,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年吨凑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捍歪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片户辱。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糙臼,靈堂內(nèi)的尸體忽然破棺而出庐镐,到底是詐尸還是另有隱情,我是刑警寧澤变逃,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布焚鹊,位于F島的核電站,受9級特大地震影響韧献,放射性物質(zhì)發(fā)生泄漏末患。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一锤窑、第九天 我趴在偏房一處隱蔽的房頂上張望璧针。 院中可真熱鬧,春花似錦渊啰、人聲如沸探橱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隧膏。三九已至,卻和暖如春嚷那,著一層夾襖步出監(jiān)牢的瞬間胞枕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工魏宽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腐泻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓队询,卻偏偏與公主長得像派桩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子蚌斩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355