淺析ClassLoader與其在A(yíng)ndroid熱修復(fù)中的使用

一. 概述

Android熱更新技術(shù)詣在解決線(xiàn)上版本的BUG修復(fù),以clasloader類(lèi)加載機(jī)制為核心糯彬,在不發(fā)布新版本的情況下讓線(xiàn)上應(yīng)用有能力進(jìn)行全量或者增量更新
本文淺析classloader類(lèi)加載機(jī)制與其在于熱修復(fù)中的應(yīng)用

ART和Dalvik

什么是Dalvik

Dalvik 是 Google 公司自己設(shè)計(jì)用于 Android 平臺(tái)的 Java 虛擬機(jī)盏袄,Android 工程師編寫(xiě)的 Java 或者 Kotlin 代碼最終都是在這臺(tái)虛擬機(jī)中被執(zhí)行的忿峻。在 Android 5.0 之前叫作 DVM薄啥,5.0 之后改為 ART(Android Runtime)。
在整個(gè) Android 操作系統(tǒng)體系中逛尚,ART 位于以下圖中紅框位置:


CgqCHl6qeUKAa86MAAY44MY5alU343.png

其實(shí)稱(chēng) DVM/ART 為 Android 版的 Java 虛擬機(jī)垄惧,這種說(shuō)法并不是很準(zhǔn)確。虛擬機(jī)必須符合 Java 虛擬機(jī)規(guī)范绰寞,也就是要通過(guò) JCM(Java Compliance Kit)的測(cè)試并獲得授權(quán)到逊,但是 DVM/ART 并沒(méi)有得到授權(quán)。

Class 的來(lái)龍去脈

Java 能夠?qū)崿F(xiàn)"一次編譯滤钱,到處運(yùn)行”觉壶,這其中 class 文件要占大部分功勞。為了讓 Java 語(yǔ)言具有良好的跨平臺(tái)能力件缸,Java 獨(dú)具匠心的提供了一種可以在所有平臺(tái)上都能使用的一種中間代碼——字節(jié)碼類(lèi)文件(.class文件)铜靶。有了字節(jié)碼,無(wú)論是哪種平臺(tái)(如:Mac他炊、Windows争剿、Linux 等),只要安裝了虛擬機(jī)都可以直接運(yùn)行字節(jié)碼痊末。并且蚕苇,有了字節(jié)碼,也解除了 Java 虛擬機(jī)和 Java 語(yǔ)言之間的耦合。

其實(shí),Java 虛擬機(jī)當(dāng)初被設(shè)計(jì)出來(lái)的目的就不單單是只運(yùn)行 Java 這一種語(yǔ)言缩幸。目前 Java 虛擬機(jī)已經(jīng)可以支持很多除 Java 語(yǔ)言以外的其他語(yǔ)言了褪迟,如 Groovy、JRuby、Jython、Scala 等。之所以可以支持其他語(yǔ)言锰茉,是因?yàn)檫@些語(yǔ)言經(jīng)過(guò)編譯之后也可以生成能夠被 JVM 解析并執(zhí)行的字節(jié)碼文件。而虛擬機(jī)并不關(guān)心字節(jié)碼是由哪種語(yǔ)言編譯而來(lái)的切心。如下圖所示:


Ciqah16DCV2AOOPvAAB-G25Eh54563.png

Dex 文件

傳統(tǒng) Class 文件是由一個(gè) Java 源碼文件生成的 .Class 文件飒筑,而 Android 是把所有 Class 文件進(jìn)行合并優(yōu)化,然后生成一個(gè)最終的 class.dex 文件绽昏。dex 文件去除了 class 文件中的冗余信息(比如重復(fù)字符常量)协屡,并且結(jié)構(gòu)更加緊湊,因此在 dex 解析階段全谤,可以減少 I/O 操作肤晓,提高了類(lèi)的查找速度。

dexopt與dexaot

  • dexopt
    在Dalvik虛擬機(jī)加載一個(gè)dex文件時(shí),會(huì)對(duì) dex 文件進(jìn)行驗(yàn)證和優(yōu)化补憾,得到odex(Optimized dex) 文件漫萄。這個(gè)文件和 dex 文件很像,只是使用了一些優(yōu)化操作碼盈匾。
  • dexaot
    ART 預(yù)先編譯機(jī)制腾务,在安裝時(shí)對(duì) dex 文件執(zhí)行dexopt優(yōu)化之后,再將odex進(jìn)行 AOT 提前編譯操作削饵,編譯為OAT(實(shí)際上是ELF文件)可執(zhí)行文件(機(jī)器碼)岩瘦。(相比做過(guò)odex優(yōu)化,未做過(guò)優(yōu)化的dex轉(zhuǎn)換成OAT要花費(fèi)更長(zhǎng)的時(shí)間)

ART 和 Dalvik 對(duì)比

1葵孤、在Dalvik下担钮,應(yīng)用運(yùn)行需要解釋執(zhí)行,常用熱點(diǎn)代碼通過(guò)即時(shí)編譯器(JIT)將字節(jié)碼轉(zhuǎn)換為機(jī)器碼尤仍,運(yùn)行效率低。而在A(yíng)RT 環(huán)境中狭姨,應(yīng)用在安裝時(shí)宰啦,字節(jié)碼預(yù)編譯(AOT)成機(jī)器碼,安裝慢了饼拍,但運(yùn)行效率會(huì)提高赡模。

2、ART占用空間比Dalvik大(字節(jié)碼變?yōu)闄C(jī)器碼)师抄, “空間換時(shí)間"漓柑。

3、預(yù)編譯也可以明顯改善電池續(xù)航叨吮,因?yàn)閼?yīng)用程序每次運(yùn)行時(shí)不用重復(fù)編譯了辆布,從而減少了 CPU 的使用頻率,降低了能耗茶鉴。

二.ClassLoader

一個(gè)完整的 Java 程序是由多個(gè) .class 文件組成的锋玲,在程序運(yùn)行過(guò)程中,需要將這些 .class 文件加載到 JVM 中才可以使用涵叮。而負(fù)責(zé)加載這些 .class 文件的就是類(lèi)加載器(ClassLoader)惭蹂。

Java 中 ClassLoader

JVM 中自帶 3 個(gè)類(lèi)加載器:
1.啟動(dòng)類(lèi)加載器 BootstrapClassLoader
2.擴(kuò)展類(lèi)加載器 ExtClassLoader (JDK 1.9 之后,改名為 PlatformClassLoader)
3.系統(tǒng)加載器 APPClassLoader
以上 3 者在 JVM 中有各自分工割粮,但是又互相有依賴(lài)盾碗。
APPClassLoader 系統(tǒng)類(lèi)加載器
部分源碼如下:

 static class AppClassLoader extends URLClassLoader {
 public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }
}

AppClassLoader 主要加載系統(tǒng)屬性“java.class.path”配置下類(lèi)文件,也就是環(huán)境變量 CLASS_PATH 配置的路徑舀瓢。因此 AppClassLoader 是面向用戶(hù)的類(lèi)加載器廷雅,我們自己編寫(xiě)的代碼以及使用的第三方 jar 包通常都是由它來(lái)加載的。

ExtClassLoader 擴(kuò)展類(lèi)加載器
部分源碼如下

static class ExtClassLoader extends URLClassLoader {
       static {
            ClassLoader.registerAsParallelCapable();
        }
 
      private static ExtClassLoader createExtClassLoader() throws IOException {
            try {
                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            final File[] dirs = getExtDirs();
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }
     private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            ...
          }
 }

可以看出,ExtClassLoader 加載系統(tǒng)屬性“java.ext.dirs”配置下類(lèi)文件榜轿。
BootstrapClassLoader 啟動(dòng)類(lèi)加載器
BootstrapClassLoader 同上面的兩種 ClassLoader 不太一樣幽歼。
首先,它并不是使用 Java 代碼實(shí)現(xiàn)的谬盐,而是由 C/C++ 語(yǔ)言編寫(xiě)的甸私,它本身屬于虛擬機(jī)的一部分。因此我們無(wú)法在 Java 代碼中直接獲取它的引用飞傀。如果嘗試在 Java 層獲取 BootstrapClassLoader 的引用皇型,系統(tǒng)會(huì)返回 null。

Android中ClassLoader

本質(zhì)上砸烦,Android 和傳統(tǒng)的 JVM 是一樣的弃鸦,也需要通過(guò) ClassLoader 將目標(biāo)類(lèi)加載到內(nèi)存,類(lèi)加載器之間也符合雙親委派模型幢痘。但是在 Android 中唬格, ClassLoader 的加載細(xì)節(jié)有略微的差別。
在 Android 虛擬機(jī)里是無(wú)法直接運(yùn)行 .class 文件的颜说,Android 會(huì)將所有的 .class 文件轉(zhuǎn)換成一個(gè) .dex 文件购岗,并且 Android 將加載 .dex 文件的實(shí)現(xiàn)封裝在 BaseDexClassLoader 中,而我們一般只使用它的兩個(gè)子類(lèi):PathClassLoader 和 DexClassLoader门粪。

PathClassLoader
PathClassLoader 用來(lái)加載系統(tǒng) apk 和被安裝到手機(jī)中的 apk 內(nèi)的 dex 文件喊积。它的 2 個(gè)構(gòu)造函數(shù)如下:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
    }

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

參數(shù)說(shuō)明

  • dexPath:dex 文件路徑,或者包含 dex 文件的 jar 包路徑玄妈;
  • librarySearchPath:C/C++ native 庫(kù)的路徑乾吻。

DexClassLoader
對(duì)比 PathClassLoader 只能加載已經(jīng)安裝應(yīng)用的 dex 或 apk 文件,DexClassLoader 則沒(méi)有此限制拟蜻,可以從 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件绎签,這也是插件化和熱修復(fù)的基礎(chǔ),在不需要安裝應(yīng)用的情況下瞭郑,完成需要使用的 dex 的加載辜御。

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

參數(shù)說(shuō)明:

  • dexPath:包含 class.dex 的 apk、jar 文件路徑 屈张,多個(gè)路徑用文件分隔符(默認(rèn)是“:”)分隔擒权。
  • optimizedDirectory:用來(lái)緩存優(yōu)化的 dex 文件的路徑,即從 apk 或 jar 文件中提取出來(lái)的 dex 文件阁谆。該路徑不可以為空碳抄,且應(yīng)該是應(yīng)用私有的,有讀寫(xiě)權(quán)限的路徑场绿。

它們之間的繼承關(guān)系


20200424151225867.png

雙親委派模式

所謂雙親委派模式就是剖效,當(dāng)類(lèi)加載器收到加載類(lèi)或資源的請(qǐng)求時(shí),通常都是先委托給父類(lèi)加載器加載,也就是說(shuō)璧尸,只有當(dāng)父類(lèi)加載器找不到指定類(lèi)或資源時(shí)咒林,自身才會(huì)執(zhí)行實(shí)際的類(lèi)加載過(guò)程。

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進(jìn)行加載  
                c = parent.loadClass(name, false);
            } else {
                //parent為null垫竞,則調(diào)用BootClassLoader進(jìn)行加載  
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        
        }
 
        if (c == null) {
            // 如果都找不到就自己查找
            long t1 = System.nanoTime();
            c = findClass(name);
        }
    }
    return c;
}
2022-05-26 21.29.33.png

委托:如上所述,委托機(jī)制是指將加載一個(gè)類(lèi)的請(qǐng)求交給父類(lèi)加載器蛀序,如果這個(gè)父類(lèi)加載器不能夠找到或者加載這個(gè)類(lèi)欢瞪,那么再加載它。
可見(jiàn)性:可見(jiàn)性的原理是子類(lèi)的加載器可以看見(jiàn)所有的父類(lèi)加載器加載的類(lèi)徐裸,而父類(lèi)加載器看不到子類(lèi)加載器加載的類(lèi)遣鼓。
單一性:?jiǎn)我恍栽硎侵竷H加載一個(gè)類(lèi)一次,這是由委托機(jī)制確保子類(lèi)加載器不會(huì)再次加載父類(lèi)加載器加載過(guò)的類(lèi)重贺。

findClass

可以看到在所有父ClassLoader無(wú)法加載Class時(shí)骑祟,則會(huì)調(diào)用自己的findClass方法。findClass在ClassLoader中的定義為:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

其實(shí)任何ClassLoader子類(lèi)檬姥,都可以重寫(xiě)loadClass與findClass曾我。一般如果你不想使用雙親委托,則重寫(xiě)loadClass修改其實(shí)現(xiàn)健民。而重寫(xiě)findClass則表示在雙親委托下,父ClassLoader都找不到Class的情況下贫贝,定義自己如何去查找一個(gè)Class秉犹。而我們的PathClassLoader會(huì)自己負(fù)責(zé)加載MainActivity這樣的程序中自己編寫(xiě)的類(lèi),利用雙親委托父ClassLoader加載Framework中的Activity稚晚。說(shuō)明PathClassLoader并沒(méi)有重寫(xiě)loadClass崇堵,因此我們可以來(lái)看看PathClassLoader中的 findClass 是如何實(shí)現(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;
}

實(shí)現(xiàn)非常簡(jiǎn)單客燕,從pathList中查找class鸳劳。繼續(xù)查看DexPathList:

public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
    //.........
    // splitDexPath 實(shí)現(xiàn)為返回 List<File>.add(dexPath)
    // makeDexElements 會(huì)去 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;
}

Class 對(duì)象在執(zhí)行引擎中的初始化過(guò)程

一個(gè) class 文件被加載到內(nèi)存中需要經(jīng)過(guò) 3 大步:裝載、鏈接也搓、初始化赏廓。其中鏈接又可以細(xì)分為:驗(yàn)證、準(zhǔn)備傍妒、解析 3 小步幔摸。因此用一張圖來(lái)描述 class 文件加載到內(nèi)存的步驟如下所示。


Cgq2xl6O2nSAXgX1AAAk3WIjy2w291.png

裝載

裝載是指 Java 虛擬機(jī)查找 .class 文件并生成字節(jié)流颤练,然后根據(jù)字節(jié)流創(chuàng)建 java.lang.Class 對(duì)象的過(guò)程既忆。

這一過(guò)程主要完成以下 3 件事:

1)ClassLoader 通過(guò)一個(gè)類(lèi)的全限定名(包名 + 類(lèi)名)來(lái)查找 .class 文件,并生成二進(jìn)制字節(jié)流:其中 class 字節(jié)碼文件的來(lái)源不一定是 .class 文件,也可以是 jar 包患雇、zip 包跃脊,甚至是來(lái)源于網(wǎng)絡(luò)的字節(jié)流。

2)把 .class 文件的各個(gè)部分分別解析(parse)為 JVM 內(nèi)部特定的數(shù)據(jù)結(jié)構(gòu)苛吱,并存儲(chǔ)在方法區(qū)酪术。

3)在內(nèi)存中創(chuàng)建一個(gè) java.lang.Class 類(lèi)型的對(duì)象:

接下來(lái)程序在運(yùn)行過(guò)程中所有對(duì)該類(lèi)的訪(fǎng)問(wèn)都通過(guò)這個(gè)類(lèi)對(duì)象,也就是這個(gè) Class 類(lèi)型的類(lèi)對(duì)象是提供給外界訪(fǎng)問(wèn)該類(lèi)的接口又谋。

加載時(shí)機(jī)

一個(gè)項(xiàng)目經(jīng)過(guò)編譯之后拼缝,往往會(huì)生成大量的 .class 文件。當(dāng)程序運(yùn)行時(shí)彰亥,JVM 并不會(huì)一次性的將這些 .class 文件全部加載到內(nèi)存中咧七。Java 虛擬機(jī)規(guī)范中并沒(méi)有嚴(yán)格規(guī)定,不同的虛擬機(jī)實(shí)現(xiàn)會(huì)有不同實(shí)現(xiàn)任斋。不過(guò)以下兩種情況一般會(huì)對(duì) class 進(jìn)行裝載操作继阻。

  • 隱式裝載:在程序運(yùn)行過(guò)程中,當(dāng)碰到通過(guò) new 等方式生成對(duì)象時(shí)废酷,系統(tǒng)會(huì)隱式調(diào)用 ClassLoader 去裝載對(duì)應(yīng)的 class 到內(nèi)存中瘟檩;
  • 顯示裝載:在編寫(xiě)源代碼時(shí),主動(dòng)調(diào)用 Class.forName() 等方法也會(huì)進(jìn)行 class 裝載操作澈蟆,這種方式通常稱(chēng)為顯示裝載墨辛。

鏈接

鏈接過(guò)程分為 3 步:驗(yàn)證、準(zhǔn)備趴俘、解析睹簇。

驗(yàn)證:

驗(yàn)證是鏈接的第一步,目的是為了確保 .class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求寥闪,并且不會(huì)危及虛擬機(jī)本身的安全太惠。主要包含以下幾個(gè)方面的檢驗(yàn)。
1.文件格式檢驗(yàn):檢驗(yàn)字節(jié)流是否符合 class 文件格式的規(guī)范疲憋,并且能被當(dāng)前版本的虛擬機(jī)處理凿渊。
2.元數(shù)據(jù)檢驗(yàn):對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的內(nèi)容符合 Java 語(yǔ)言規(guī)范的要求缚柳。
3.字節(jié)碼檢驗(yàn):通過(guò)數(shù)據(jù)流和控制流分析喂击,確定程序語(yǔ)義是合法翰绊、符合邏輯的。
4.符號(hào)引用檢驗(yàn):符號(hào)引用檢驗(yàn)可以看作是對(duì)類(lèi)自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn)。

準(zhǔn)備

準(zhǔn)備是鏈接的第 2 步麦撵,這一階段的主要目的是為類(lèi)中的靜態(tài)變量分配內(nèi)存免胃。

解析

解析是鏈接的最后一步羔沙,這一階段的任務(wù)是把常量池中的符號(hào)引用轉(zhuǎn)換為直接引用扼雏,也就是具體的內(nèi)存地址诗充。在這一階段蝴蜓,JVM 會(huì)將常量池中的類(lèi)励翼、接口名抓狭、字段名否过、方法名等轉(zhuǎn)換為具體的內(nèi)存地址苗桂。

初始化

class 加載的最后一步煤伟,這一階段是執(zhí)行類(lèi)構(gòu)造器<clinit>方法的過(guò)程围辙,并真正初始化類(lèi)變量姚建。

初始化的時(shí)機(jī)

對(duì)于裝載階段掸冤,JVM 并沒(méi)有規(guī)范何時(shí)具體執(zhí)行稿湿。但是對(duì)于初始化伊群,JVM 規(guī)范中嚴(yán)格規(guī)定了 class 初始化的時(shí)機(jī)舰始,主要有以下幾種情況會(huì)觸發(fā) class 的初始化:
1.虛擬機(jī)啟動(dòng)時(shí)丸卷,初始化包含 main 方法的主類(lèi)谜嫉;
2.遇到 new 指令創(chuàng)建對(duì)象實(shí)例時(shí),如果目標(biāo)對(duì)象類(lèi)沒(méi)有被初始化則進(jìn)行初始化操作住闯;
3.當(dāng)遇到訪(fǎng)問(wèn)靜態(tài)方法或者靜態(tài)字段的指令時(shí)比原,如果目標(biāo)對(duì)象類(lèi)沒(méi)有被初始化則進(jìn)行初始化操作;
4.子類(lèi)的初始化過(guò)程如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化蚌铜,則需要先觸發(fā)其父類(lèi)的初始化识腿;
5.使用反射 API 進(jìn)行反射調(diào)用時(shí)渡讼,如果類(lèi)沒(méi)有進(jìn)行過(guò)初始化則需要先觸發(fā)其初始化;
6.第一次調(diào)用 java.lang.invoke.MethodHandle 實(shí)例時(shí)蹬昌,需要初始化 MethodHandle 指向方法所在的類(lèi)。

三.熱修復(fù)

PathClassLoader中存在一個(gè)Element數(shù)組明刷,Element類(lèi)中存在一個(gè)dexFile成員表示dex文件,即:APK中有X個(gè)dex挤聘,則Element數(shù)組就有X個(gè)元素。

20200424155832582.png

在PathClassLoader中的Element數(shù)組為:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位于patch.dex與classes2.dex中都存在一份,當(dāng)進(jìn)行類(lèi)查找時(shí)幼驶,循環(huán)獲得dexElements中的DexFile购桑,查找到了Key.class則立即返回,不會(huì)再管后續(xù)的element中的DexFile是否能加載到Key.class了缭贡。

因此實(shí)際上,一種熱修復(fù)實(shí)現(xiàn)可以將出現(xiàn)Bug的class單獨(dú)的制作一份hotfix.dex文件(補(bǔ)丁包),然后在程序啟動(dòng)時(shí)纲岭,從服務(wù)器下載fix.dex保存到某個(gè)路徑,再通過(guò)hotfix.dex的文件路徑沽翔,用其創(chuàng)建Element對(duì)象雳殊,然后將這個(gè)Element對(duì)象插入到我們程序的類(lèi)加載器PathClassLoader的pathList中的dexElements數(shù)組頭部座咆。這樣在加載出現(xiàn)Bug的class時(shí)會(huì)優(yōu)先加載hotfix.dex中的修復(fù)類(lèi),從而解決Bug哺呜。

實(shí)踐

package com.yupaopao.hotfix;
public class MyTitle {
    public String getTitle() {
        return "I am original Title";
    }
}

以上MyTitle類(lèi)中g(shù)etTitle方法存在bug国撵,需要通過(guò)預(yù)先加載fix.dex補(bǔ)丁包加載正確的MyTitle類(lèi)

以下為正確MyTitle類(lèi)

package com.yupaopao.hotfix;
public class MyTitle {
    public String getTitle() {
        return "I am hotfix Title";
    }
}

將MyTitle.java打包成MyTitle.jar澳厢,然后通過(guò) dx 工具將生成的 MyTitle.jar 包中的 class 文件優(yōu)化為 dex 文件喳整。

dx --dex --output=MyTitle.jar

上述 MyTitle.jar 就是我們最終需要用作 hotfix 的 jar 包。
首先將 HotFix patch 保存到本地目錄下魏保。一般在真實(shí)項(xiàng)目中季二,我們可以通過(guò)向后端發(fā)送請(qǐng)求的方式刻蚯,將最新的 HotFix patch 下載到本地中,使用 DexClassLoader 本地目錄中的MyTitle類(lèi)

package com.yupaopao.hotfix;

import android.app.Application;
import android.content.Context;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;

import dalvik.system.BaseDexClassLoader;
import dalvik.system.PathClassLoader;

public class HotfixApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        File apk = new File(getCacheDir() + "/hotfix.dex");
       if (apk.exists()) {
            try {
                ClassLoader classLoader = getClassLoader();
                //默認(rèn)類(lèi)加載器
                Class loaderClass = BaseDexClassLoader.class;
                Field pathListField = loaderClass.getDeclaredField("pathList");
                pathListField.setAccessible(true);
                Object pathListObject = pathListField.get(classLoader);
                Class pathListClass = pathListObject.getClass();
                Field dexElementsField = pathListClass.getDeclaredField("dexElements");
                dexElementsField.setAccessible(true);
                //默認(rèn)類(lèi)加載器中dexElements數(shù)組
                Object dexElementsObject = dexElementsField.get(pathListObject);

                //新建類(lèi)加載器加載hotfix.dex中的類(lèi)
                PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), null);
                Object newPathListObject = pathListField.get(newClassLoader);
                Object newDexElementsObject = dexElementsField.get(newPathListObject);

                int oldLength = Array.getLength(dexElementsObject);
                int newLength = Array.getLength(newDexElementsObject);
                Object concatDexElementsObject = Array.newInstance(dexElementsObject.getClass().getComponentType(), oldLength + newLength);
                for (int i = 0; i < newLength; i++) {
                    Array.set(concatDexElementsObject, i, Array.get(newDexElementsObject, i));
                }
                for (int i = 0; i < oldLength; i++) {
                    Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObject, i));
                }
                //將hotfix.dex中的類(lèi)追加進(jìn)默認(rèn)類(lèi)加載器中
                dexElementsField.set(pathListObject, concatDexElementsObject);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

應(yīng)用重啟后在A(yíng)pplication初始化時(shí)類(lèi)加載器便會(huì)先加載修復(fù)過(guò)后的MyTitle類(lèi)以政,從而修復(fù)已有bug.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市寸五,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌十性,老刑警劉巖霞势,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)尖坤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)寞忿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)叫编,“玉大人辖佣,你說(shuō)我怎么就攤上這事〈暧猓” “怎么了卷谈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)霞篡。 經(jīng)常有香客問(wèn)我世蔗,道長(zhǎng),這世上最難降的妖魔是什么朗兵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任污淋,我火速辦了婚禮,結(jié)果婚禮上余掖,老公的妹妹穿的比我還像新娘寸爆。我一直安慰自己,他們只是感情好浊吏,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布而昨。 她就那樣靜靜地躺著,像睡著了一般找田。 火紅的嫁衣襯著肌膚如雪歌憨。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,084評(píng)論 1 291
  • 那天墩衙,我揣著相機(jī)與錄音务嫡,去河邊找鬼。 笑死漆改,一個(gè)胖子當(dāng)著我的面吹牛心铃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挫剑,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼去扣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了樊破?” 一聲冷哼從身側(cè)響起愉棱,我...
    開(kāi)封第一講書(shū)人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哲戚,沒(méi)想到半個(gè)月后奔滑,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡顺少,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年朋其,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了王浴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梅猿,死狀恐怖氓辣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情袱蚓,我是刑警寧澤筛婉,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站癞松,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏入蛆。R本人自食惡果不足惜响蓉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哨毁。 院中可真熱鬧枫甲,春花似錦、人聲如沸扼褪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)话浇。三九已至脏毯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間幔崖,已是汗流浹背食店。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赏寇,地道東北人吉嫩。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嗅定,于是被迫代替她去往敵國(guó)和親自娩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

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