Android 熱修復核心原理,ClassLoader類加載

[TOC]Android前沿技術探討:ClassLoader在熱修復中的應用

又在寫bug?這句話雖然是句玩笑話百侧,但是也正因為我們是人不是神,但也不能面面俱到杂伟,什么都考慮完美移层,出現(xiàn)bug是不可避免的仍翰。那么對于android我們出現(xiàn)了Bug怎么辦赫粥?

早期遇到Bug我們一般會緊急發(fā)布了一個版本。然而這個Bug可能就是簡簡單單的一行代碼予借,為了這一行代碼越平,進行全量或者增量更新迭代一個版本,未免有點大材小用了灵迫。而且新版本的普及需要時間秦叛,而且如果這次的新版本又有個小問題,怎么辦瀑粥?

那么為了解決這一個問題挣跋,熱修復出現(xiàn)了。

熱修復狞换,現(xiàn)在大家應該都不陌生避咆。從16年開始開始,熱修復技術在 Android 技術社區(qū)熱了一陣子修噪,這種不用發(fā)布新版本就可以修復線上 bug 的技術看起來非常黑科技查库。

本節(jié)課的目的并不在于熱修復本身,主要是通過熱修復這個案例熟悉其核心:類加載機制黄琼。(后續(xù)會有更詳細課程講解熱修復)

ART 和 Dalvik

**DVM**也是實現(xiàn)了**JVM**規(guī)范的一個虛擬器樊销,默認使用CMS垃圾回收器,但是與JVM運行 Class 字節(jié)碼不同,DVM 執(zhí)行 **Dex(Dalvik Executable Format)** ——專為 Dalvik 設計的一種壓縮格式围苫。Dex 文件是很多 .class 文件處理壓縮后的產(chǎn)物裤园,最終可以在 Android 運行時環(huán)境執(zhí)行。

而**ART(Android Runtime)** 是在 Android 4.4 中引入的一個開發(fā)者選項剂府,也是 Android 5.0 及更高版本的默認 Android 運行時比然。ART 和 Dalvik 都是運行 Dex 字節(jié)碼的兼容運行時,因此針對 Dalvik 開發(fā)的應用也能在 ART 環(huán)境中運作周循。

https://source.android.google.cn/devices/tech/dalvik/gc-debug

dexopt與dexaot

  • dexopt

    Dalvik中虛擬機在加載一個dex文件時强法,對 dex 文件 進行 驗證 和 優(yōu)化的操作,其對 dex 文件的優(yōu)化結果變成了 odex(Optimized dex) 文件湾笛,這個文件和 dex 文件很像饮怯,只是使用了一些優(yōu)化操作碼。

  • dex2oat

    ART 預先編譯機制嚎研,在安裝時對 dex 文件執(zhí)行dexopt優(yōu)化之后再將odex進行 AOT 提前編譯操作蓖墅,編譯為OAT(實際上是ELF文件)可執(zhí)行文件(機器碼)。(相比做過ODEX優(yōu)化临扮,未做過優(yōu)化的DEX轉(zhuǎn)換成OAT要花費更長的時間)

dex.png

ClassLoader介紹

任何一個 Java 程序都是由一個或多個 class 文件組成论矾,在程序運行時,需要將 class 文件加載到 JVM 中才可以使用杆勇,負責加載這些 class 文件的就是 Java 的類加載機制贪壳。ClassLoader 的作用簡單來說就是加載 class 文件,提供給程序運行時使用蚜退。每個 Class 對象的內(nèi)部都有一個 classLoader 字段來標識自己是由哪個 ClassLoader 加載的闰靴。
class Class<T> {
  ...
  private transient ClassLoader classLoader;
  ...
}
ClassLoader是一個抽象類,而它的具體實現(xiàn)類主要有:
  • BootClassLoader

    用于加載Android Framework層class文件钻注。

  • PathClassLoader

    用于Android應用程序類加載器蚂且。可以加載指定的dex幅恋,以及jar杏死、zip、apk中的classes.dex

  • DexClassLoader

    用于加載指定的dex捆交,以及jar淑翼、zip、apk中的classes.dex

很多博客里說PathClassLoader只能加載已安裝的apk的dex零渐,其實這說的應該是在dalvik虛擬機上窒舟。

但現(xiàn)在一般不用關心dalvik了。

Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");
Log.e(TAG, "MainActivity.class 由:" + getClassLoader() +" 加載");


//輸出:
Activity.class 由:java.lang.BootClassLoader@d3052a9 加載

MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加載

它們之間的關系如下:

ClassLoader.png

PathClassLoaderDexClassLoader的共同父類是BaseDexClassLoader诵盼。

public class DexClassLoader extends BaseDexClassLoader {
    
    public DexClassLoader(String dexPath, String optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
         super(dexPath, null, librarySearchPath, parent);
    }
}

可以看到兩者唯一的區(qū)別在于:創(chuàng)建DexClassLoader需要傳遞一個optimizedDirectory參數(shù)惠豺,并且會將其創(chuàng)建為File對象傳給super银还,而PathClassLoader則直接給到null。因此兩者都可以加載指定的dex洁墙,以及jar蛹疯、zip、apk中的classes.dex

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());

File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());
其實,`optimizedDirectory`參數(shù)就是dexopt的產(chǎn)出目錄(odex)热监。那`PathClassLoader`創(chuàng)建時捺弦,這個目錄為null,就意味著不進行dexopt孝扛?并不是列吼,`optimizedDirectory`為null時的默認路徑為:***/data/dalvik-cache***。

在API 26源碼中苦始,將DexClassLoader的optimizedDirectory標記為了 deprecated 棄用寞钥,實現(xiàn)也變?yōu)榱耍?/p>

public DexClassLoader(String dexPath, String optimizedDirectory,
                  String librarySearchPath, ClassLoader parent) {
  super(dexPath, null, librarySearchPath, parent);
}

......和PathClassLoader一摸一樣了!

雙親委托機制

可以看到創(chuàng)建`ClassLoader`需要接收一個`ClassLoader parent`參數(shù)陌选。這個`parent`的目的就在于實現(xiàn)類加載的雙親委托理郑。即:

某個類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器咨油,依次遞歸您炉,如果父類加載器可以完成類加載任務,就成功返回役电;只有父類加載器無法完成此加載任務時赚爵,才自己去加載。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    
    // 檢查class是否有被加載  
    Class c = findLoadedClass(name);
    if (c == null) {
        long t0 = System.nanoTime();
        try {
            if (parent != null) {
                //如果parent不為null宴霸,則調(diào)用parent的loadClass進行加載  
                c = parent.loadClass(name, false);
            } else {
                //parent為null囱晴,則調(diào)用BootClassLoader進行加載  
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        
        }

        if (c == null) {
            // 如果都找不到就自己查找
            long t1 = System.nanoTime();
            c = findClass(name);
        }
    }
    return c;
}

因此我們自己創(chuàng)建的ClassLoader: new PathClassLoader("/sdcard/xx.dex", getClassLoader());并不僅僅只能加載 xx.dex中的class膏蚓。

值得注意的是:c = findBootstrapClassOrNull(name);

按照方法名理解瓢谢,應該是當parent為null時候,也能夠加載BootClassLoader加載的類驮瞧。

new PathClassLoader("/sdcard/xx.dex", null)氓扛,能否加載Activity.class?

但是實際上论笔,Android當中的實現(xiàn)為:(Java不同)

private Class findBootstrapClassOrNull(String name)
{
  return null;
} 

findClass

可以看到在所有父ClassLoader無法加載Class時采郎,則會調(diào)用自己的`findClass`方法。`findClass`在ClassLoader中的定義為:
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
其實任何ClassLoader子類狂魔,都可以重寫`loadClass`與`findClass`蒜埋。一般如果你不想使用雙親委托,則重寫`loadClass`修改其實現(xiàn)最楷。而重寫`findClass`則表示在雙親委托下整份,父ClassLoader都找不到Class的情況下待错,定義自己如何去查找一個Class。而我們的`PathClassLoader`會自己負責加載`MainActivity`這樣的程序中自己編寫的類烈评,利用雙親委托父ClassLoader加載Framework中的`Activity`火俄。說明`PathClassLoader`并沒有重寫`loadClass`,因此我們可以來看看PathClassLoader中的 `findClass` 是如何實現(xiàn)的讲冠。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String    
                        librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath,       
                                    optimizedDirectory);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //查找指定的class
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" +                                                       name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
            throw cnfe;
    }
    return c;
}
實現(xiàn)非常簡單瓜客,從`pathList`中查找class。繼續(xù)查看`DexPathList`
public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
    //.........
    // splitDexPath 實現(xiàn)為返回 List<File>.add(dexPath)
    // makeDexElements 會去 List<File>.add(dexPath) 中使用DexFile加載dex文件返回 Element數(shù)組
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
    //.........
    
}

public Class findClass(String name, List<Throwable> suppressed) {
     //從element中獲得代表Dex的 DexFile
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            //查找class
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

熱修復

`PathClassLoader`中存在一個Element數(shù)組竿开,Element類中存在一個dexFile成員表示dex文件谱仪,即:APK中有X個dex,則Element數(shù)組就有X個元素否彩。
類查找.png
在`PathClassLoader`中的Element數(shù)組為:[patch.dex , classes.dex , classes2.dex]芽卿。如果存在**Key.class**位于patch.dex與classes2.dex中都存在一份,當進行類查找時胳搞,循環(huán)獲得`dexElements`中的DexFile卸例,查找到了**Key.class**則立即返回,不會再管后續(xù)的element中的DexFile是否能加載到**Key.class**了肌毅。  

因此實際上筷转,一種熱修復實現(xiàn)可以將出現(xiàn)Bug的class單獨的制作一份fix.dex文件(補丁包),然后在程序啟動時悬而,從服務器下載fix.dex保存到某個路徑呜舒,再通過fix.dex的文件路徑,用其創(chuàng)建`Element`對象笨奠,然后將這個`Element`對象插入到我們程序的類加載器`PathClassLoader`的`pathList`中的`dexElements`數(shù)組頭部袭蝗。這樣在加載出現(xiàn)Bug的class時會優(yōu)先加載fix.dex中的修復類,從而解決Bug般婆。

熱修復的方式不止這一種到腥,并且如果要完整實現(xiàn)此種熱修復可能還需要注意一些其他的問題(如:反射兼容)。

作業(yè)

實現(xiàn)ClassLoader熱修復的兼容問題

可以觀看視頻哦:https://www.bilibili.com/video/BV1Yh41197EW?spm_id_from=333.999.0.0

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔚袍,一起剝皮案震驚了整個濱河市乡范,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌啤咽,老刑警劉巖晋辆,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宇整,居然都是意外死亡瓶佳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門鳞青,熙熙樓的掌柜王于貴愁眉苦臉地迎上來霸饲,“玉大人索赏,你說我怎么就攤上這事√耍” “怎么了潜腻?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長器仗。 經(jīng)常有香客問我融涣,道長,這世上最難降的妖魔是什么精钮? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任威鹿,我火速辦了婚禮,結果婚禮上轨香,老公的妹妹穿的比我還像新娘忽你。我一直安慰自己,他們只是感情好臂容,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布科雳。 她就那樣靜靜地躺著,像睡著了一般脓杉。 火紅的嫁衣襯著肌膚如雪糟秘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天球散,我揣著相機與錄音尿赚,去河邊找鬼。 笑死蕉堰,一個胖子當著我的面吹牛凌净,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播屋讶,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼冰寻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了丑婿?” 一聲冷哼從身側(cè)響起性雄,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎羹奉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體约计,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡诀拭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了煤蚌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耕挨。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡细卧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出筒占,到底是詐尸還是另有隱情贪庙,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布翰苫,位于F島的核電站止邮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奏窑。R本人自食惡果不足惜导披,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埃唯。 院中可真熱鬧撩匕,春花似錦、人聲如沸墨叛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漠趁。三九已至滓技,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間棚潦,已是汗流浹背令漂。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丸边,地道東北人叠必。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像妹窖,于是被迫代替她去往敵國和親纬朝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

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