android ClassLoader實現(xiàn)熱修復(fù)

1. 前言

在用公司的框架進行開發(fā)時泽台,最大的特點是模塊純java開發(fā)龙亲,打成dex包進行模塊更新,而不用更新app。


image

這個算是熱修復(fù)框架里的java multidex方式實現(xiàn)的记罚,接下來看看ClassLoader如何實現(xiàn)的熱修復(fù)。

2. 原理

首先來看看實現(xiàn)功能需要哪些原理壳嚎。

2.1 Dalvik,JIT與ART

Java是編譯-解釋語言,即程序員編譯之后不可以直接編譯為機器碼桐智,而是會編譯成字節(jié)碼(在Java程序中為.class文件末早,在Android程序中為.dex文件)。然后我們需要將字節(jié)碼再解釋成機器碼,使之能被CPU解讀说庭。這第二次解釋然磷,即從字節(jié)碼解釋成機器碼的過程,是程序安裝或運行后刊驴,在Java虛擬機中實現(xiàn)的姿搜。

2.1.1 Dalvik

android在最開始時,內(nèi)置了一個Dalvik虛擬機捆憎,其實也就是Google自己編寫的一個Java虛擬器舅柜,然后使用邊解釋邊執(zhí)行的方式來運行Java代碼,這種模式==運行效率極其低下==躲惰,因此很快Google就引入了JIT模式來改善這種情況致份。

2.1.2 JIT

JIT(Just In Time)是即時編譯的意思,當(dāng)用戶在使用App時础拨,會將經(jīng)常使用的功能編譯成機器碼氮块,這樣當(dāng)再次使用這個功能時就可以直接運行機器碼,而不用每次都一行行翻譯了太伊。

雖然JIT挺聰明的雇锡,且總體思路清晰理想豐滿逛钻,但現(xiàn)實是仍然卡的要死僚焦。==打開APP的時候會變慢==;==每次打開APP都要重復(fù)勞動,不能一勞永逸==;==如果用戶打開了JIT沒有編譯的代碼曙痘,就只能等DVM中的解釋器去邊執(zhí)行邊解釋了==芳悲。

2.1.3 ART

然而JIT的機制仍然不夠完美,在Android 5.0系統(tǒng)的時候Google進行了一次大變更边坤,廢棄了Dalvik虛擬機名扛,引入了全新開發(fā)的ART虛擬機,并使用 AOT(Ahead Of Time) 的方式來提升運行效率茧痒。AOT就是在應(yīng)用安裝的時候預(yù)先將代碼都編譯成機器碼肮韧,這樣在應(yīng)用運行的時候就不用再做解釋工作了,直接就可以運行旺订。

然而最終用戶實際的反饋卻并不怎么好弄企,==AOT機制使得安裝應(yīng)用變得太慢了,而且預(yù)先編譯機器碼還會占用額外的手機空間==区拳。

2.1.4 混合編譯

于是在Android 7.0系統(tǒng)中拘领,Google又進行了調(diào)整,這次重新引入了JIT模式樱调。應(yīng)用安裝的時候不會進行編譯操作约素,以保證安裝速度届良。App運行時,dex文件先通過解析器被直接執(zhí)行并記錄在profile文件,通過profile來判斷是否為熱點函數(shù)圣猎,熱點函數(shù)會被識別并被JIT編譯后存儲在 jit code cache 中士葫。手機進入 IDLE(空閑) 或者 Charging(充電) 狀態(tài)的時候,系統(tǒng)會掃描 App 目錄下的 profile 文件并執(zhí)行 AOT 過程進行編譯样漆。如果執(zhí)行到還沒來得及編譯的代碼为障,那么就使用JIT+解釋執(zhí)行的方式來頂住。

image

2.1.5 Android 8.0 改進解釋器

在Android8.0時期放祟,谷歌又盯上了解釋器鳍怨,其實縱觀上面的問題,根源就是這個解釋器解釋的太慢了9蛲住(什么JIT,AOT,老夫解釋只有一個字鞋喇,快)那我們何不讓這個解釋器解釋的快一點呢?于是谷歌改進了解釋器眉撵,解釋模式執(zhí)行效率大大提升侦香。

2.1.6 Android 9.0 改進編譯模板

在Android9.0上提供了預(yù)先放置熱點代碼的方式,應(yīng)用在安裝的時候就能知道常用代碼會被提前編譯纽疟。

谷歌允許你在開發(fā)階段添加一個配置文件罐韩,這個配置文件內(nèi)可指定“熱點代碼”,當(dāng)應(yīng)用安裝完后污朽,ART在后臺悄悄編譯APP時散吵,會優(yōu)先編譯配置文件中指定的“熱點代碼”。

2.1.7 文件格式odex與oat

  • dex:可以簡單的理解為:Dex 文件是很多 .class 文件處理后的產(chǎn)物蟆肆,最終可以在 Android 運行時環(huán)境執(zhí)行矾睦。是字節(jié)碼
  • odexdalvik虛擬機中炎功,安裝時從apk文件中提取出classes.dex文件枚冗,并通過dexopt生成一個可運行的文件單獨存放在data/dalvik-cache目錄下,在DexClassLoader動態(tài)加載Dex文件時蛇损,也會進行Dex的優(yōu)化赁温。odex 文件也屬于dex文件
  • oatart虛擬機中淤齐,APK中的.dex文件(字節(jié)碼)會被dex2oat解釋為ELF格式的.oat文件(機器碼)同樣保存在手機的data/dalvik-cache目錄下股囊。

APK運行時,上述生成的oat文件會被加載到內(nèi)存中床玻,并且ART虛擬機可以通過里面的oatdata和oatexec段找到任意一個類的方法對應(yīng)的本地機器指令來執(zhí)行毁涉。 oat文件中的oatdata包含用來生成本地機器指令的dex文件,內(nèi)容oat文件中的oatexec包含有生成的本地機器指令锈死。

2.1.8 編譯模式j(luò)it與aot

在android5.0以下和7.0以上贫堰,jit和art的表現(xiàn)都不一樣穆壕。

2.1.8.1 android5.0以下的jit

在運行時對dex的指令進行intercept,解釋成機器碼其屏;虛擬機根據(jù)函數(shù)調(diào)用的次數(shù)喇勋,來決定熱點代碼;以函數(shù)為維度將熱點代碼的機器碼進行緩存偎行,而jit就是將熱點代碼優(yōu)化編譯緩存川背,存在下一次調(diào)用時直接調(diào)用該機器碼。

  • 優(yōu)點:安裝速度超快蛤袒;存儲空間小
  • 缺點:Multidex加載的時候會非常慢熄云,因為在dex加載時會進行dexopt;JIT中需要解釋器妙真,解釋器解釋的字節(jié)碼會帶來CPU和時間的消耗缴允;由于熱點代碼的Monitor一直在運行,也會帶來電量的損耗珍德;每次啟動應(yīng)用都需要重新編譯(沒有緩存)

2.1.8.2 5.0-7.0的aot

art虛擬機運行的文件格式也從odex轉(zhuǎn)換成了oat格式练般。oat文件包含oatdata和oatexec,前者包含dex文件內(nèi)容锈候,后者包含生成的本地機器指令薄料。

在APK安裝的時候,PackageManagerService會調(diào)用dex2oat通過aot靜態(tài)編譯的方式泵琳,來將所有的dex文件(包括Multidex)編譯oat文件摄职;DexClassLoader動態(tài)加載也會生成oat文件。

  • 優(yōu)點:運行時會超級快虑稼;在運行時省電琳钉,也節(jié)省各種資源
  • 缺點:應(yīng)用安裝和系統(tǒng)升級之后的應(yīng)用優(yōu)化比較耗時(重新編譯势木,把程序代碼轉(zhuǎn)換成機器語言)蛛倦,所有app都需要進行dex2oat的操作;由于oat文件中包含dex文件與編譯后的Native Code啦桌,導(dǎo)致占用空間也越來越大

2.1.8.3 7.0至今的aot與jit

7.0之后的采用了Hybrid Mode的ART虛擬機:aot溯壶,jit,解釋器三種混合編譯甫男,來從運行時的性能且改、存儲、安裝板驳、加載時間進行平衡又跛。

安裝時以Intercept的方式來運行App;在系統(tǒng)空閑的時候會在后臺對App進行AOT靜態(tài)編譯若治,并且會根據(jù)解釋器運行時所收集的運行時函數(shù)調(diào)用的信息生成的Profile文件來進行參考慨蓝,Profile文件中的熱點函數(shù)會被jit編譯器編譯并緩存下次使用感混。

[圖片上傳失敗...(image-be1557-1576665837767)]
由上圖可知,解釋器解釋函數(shù)會記錄在Profile文件中礼烈,如果Profile文件判斷方法是熱點代碼弧满,會jit編譯器進行編譯并緩存,下次調(diào)用則直接使用緩存機械碼此熬。

在BackgroundDexOptService中庭呜,會根據(jù)所生成的Profile以及Dex文件在后臺進行AOT,根據(jù)運行時的Profile文件會選擇性的將常用的函數(shù)編譯成NativeCode

9.0谷歌推出了熱點代碼配置功能犀忱,當(dāng)應(yīng)用安裝完后募谎,ART在后臺悄悄編譯APP時,會優(yōu)先編譯配置文件中指定的“熱點代碼”阴汇。

2.1.8.4 存放目錄

大家都知道 apk其實就是zip包 apk安裝過程其實就是解壓過程

用戶應(yīng)用安裝涉及以下幾個目錄

data/app 安裝目錄 安裝時會把apk文件copy到這里

data/dalvik-cache 如上述描述中的存放.dex(.odex 無論davilk的dex 還是art的oat格式)

data/data/pkg/ 存放應(yīng)用程序的數(shù)據(jù)

2.1.9 方舟編譯器

翻譯器和編譯器是不同的近哟;編譯器是把源程序的每一條語句都編譯成機器語言并保存成二進制文件,這樣運行時計算機可以直接以機器語言來運行此程序鲫寄,因而速度很快吉执;而解釋器則是只在執(zhí)行程序時,才一條一條地解釋成機器語言來讓計算機執(zhí)行地来,因此運行速度不如編譯后的程序運行得快戳玫。

例如上面的混合編譯,jit是即時編譯未斑,AOT靜態(tài)編譯咕宿,jit配合解釋器使用。無論是編譯器還是解釋器蜡秽,只是在虛擬機上打補丁府阀,手機上的虛擬機+編譯器+解釋器本身不僅占用硬件資源,還無法最大發(fā)揮軟件運行性能芽突。正因如此试浙,所以絕大部分手機廠商只能無奈的通過簡單粗暴提升Android手機的內(nèi)存和存儲空間,來彌補虛擬機的弊端寞蚌。

方舟編譯器與其說是一個編譯器田巴,不如說是一個編譯運行系統(tǒng);這個系統(tǒng)的運行需要開發(fā)環(huán)境和終端(也就是智能手機)的配合挟秤,其目的是繞過Android操作系統(tǒng)中App的運行所必須依賴的虛擬機壹哺,將Java/C/C++等混合代碼一次編譯成機器碼直接在手機上運行,徹底告別Java的JNI額外開銷艘刚,也徹底告別了虛擬機的GC內(nèi)存回收帶來的應(yīng)用進程掉線——從而最終實現(xiàn)Android操作系統(tǒng)的流暢度管宵。

它將編譯過程放在了開發(fā)者那里,直接運行機械碼也比Java/C/C++等混合代碼快,還自制了更好的垃圾回收箩朴。

2.2 ClassLoader簡介

在java中笛臣,它是類加載器,顧名思義隧饼,根據(jù)一個指定的類的全限定名,找到對應(yīng)的Class字節(jié)碼文件,然后加載它轉(zhuǎn)化成一個java.lang.Class類的一個實例沈堡,將class類載入到JVM中。

而android中燕雁,也同樣需要有ClassLoader機制將class類加載到Android 的 Dalvik(5.0之前版本)/ART(5.0增加的)中诞丽,是將class打包成一個或者多個 dex文件,再由BaseDexClassLoader來進行處理拐格。

image

在Android中僧免,ClassLoader是一個抽象類,實際開發(fā)過程中捏浊,我們一般是使用其具體的子類DexClassLoader懂衩、PathClassLoader這些類加載器來加載類的,BootClassLoader加載系統(tǒng)類金踪,它們的不同之處是:

  • BootClassLoader:主要用于加載系統(tǒng)的類浊洞,包括java和android系統(tǒng)的類庫,和JVM中不同胡岔,BootClassLoader是ClassLoader內(nèi)部類法希,是由Java實現(xiàn)的,它也是所有系統(tǒng)ClassLoader的父ClassLoader
  • DexClassLoader:可以用于加載任意路徑的zip靶瘸、dex苫亦、jar或者apk文件,也是進行安卓動態(tài)加載的基礎(chǔ)
  • PathClassLoader:用于加載Android系統(tǒng)類和開發(fā)編寫應(yīng)用的類怨咪,只能加載已經(jīng)安裝應(yīng)用的 dex 或 apk 文件

簡單來說,DexClassLoader是動態(tài)加載sd卡中的dex屋剑,PathClassLoader加載安裝時解壓在私有路徑data/dalvik-cache下的dex。

在Android系統(tǒng)啟動的時候會創(chuàng)建一個Boot類型的ClassLoader實例诗眨,用于加載一些系統(tǒng)Framework層級需要的類唉匾,我們的Android應(yīng)用里也需要用到一些系統(tǒng)的類,所以APP啟動的時候也會把這個Boot類型的ClassLoader傳進來辽话。

一個運行的Android應(yīng)用至少有2個ClassLoader肄鸽。一個是BootClassLoader(系統(tǒng)啟動的時候創(chuàng)建的)卫病,另一個是PathClassLoader油啤。

BootClassLoader
class BootClassLoader extends ClassLoader {
    private static BootClassLoader instance;
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }
    public BootClassLoader() {
        super(null);
    }
    
}

BootClassLoader也是ClassLoader的內(nèi)部類,為單例蟀苛,parent為空益咬。,

DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}
PathClassLoader
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);
    }
}

PathClassLoader在app啟動時帜平,在ActivityThread幽告,performLaunchActivity方法中梅鹦,LoadApk.getClassLoader->LoadApk.createOrUpdateClassLoaderLocked->ApplicationLoaders.getDefault().getClassLoader->ClassLoaderFactory.createClassLoader->new PathClassLoader(dexPath, librarySearchPath, parent);

這兩者只是簡單的對BaseDexClassLoader做了一下封裝,具體的實現(xiàn)還是在父類里冗锁。不過這里也可以看出齐唆,PathClassLoader的optimizedDirectory只能是null,進去BaseDexClassLoader看看這個參數(shù)是干什么的

BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(parent);
    this.originalPath = dexPath;
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

主要是創(chuàng)建了一個DexPathList對象

DexPathList
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ……
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files,
        File optimizedDirectory) {
    ArrayList<Element> elements = new ArrayList<Element>();
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            zip = new ZipFile(file);
        }
        ……
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

/**
 * Converts a dex/jar file path and an output directory to an
 * output file path for an associated optimized dex file.
 */
private static String optimizedPathFor(File path,
        File optimizedDirectory) {
    String fileName = path.getName();
    if (!fileName.endsWith(DEX_SUFFIX)) {
        int lastDot = fileName.lastIndexOf(".");
        if (lastDot < 0) {
            fileName += DEX_SUFFIX;
        } else {
            StringBuilder sb = new StringBuilder(lastDot + 4);
            sb.append(fileName, 0, lastDot);
            sb.append(DEX_SUFFIX);
            fileName = sb.toString();
        }
    }
    File result = new File(optimizedDirectory, fileName);
    return result.getPath();
}

前面介紹虛擬機的時候說過冻河,odex和oat都是存在/data/dalvik-cache 目錄箍邮,這也是optimizedDirectory為null時,默認的內(nèi)部存儲路徑叨叙,也證明PathClassLoader是用來加載已經(jīng)安裝應(yīng)用的 dex
锭弊。

從創(chuàng)建代碼就可以窺測出一些實現(xiàn)熱修復(fù)的關(guān)鍵,BaseDexClassLoader中會創(chuàng)建一個對象DexPathList擂错,DexPathList中會使用dexElements 來存儲已加載的dex信息,DexFile 味滞。

2.4 雙親代理模型

既然知道dex信息存儲對象,那來看看如何加載dex到钮呀。

ClassLoader.loadClass
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    // 1 通過調(diào)用c層findLoadedClass檢查該類是否被加載過剑鞍,若加載過則返回class對象(緩存機制),是通過BootClassLoader加載的緩存
    Class c = findLoadedClass(name);
    if (c == null) {
        long t0 = System.nanoTime();
        try {
            if (parent != null) {
                //2 各種類型的類加載器在構(gòu)造時都會傳入一個parent類加載器
                //2 若parent類不為空,則調(diào)用parent類的loadClass方法
                c = parent.loadClass(name, false);
            } else {
                //3 查閱了PathClassLoader爽醋、DexClassLoader并沒有重寫該方法攒暇,默認是返回null
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        }
        if (c == null) {
            //4  如果父ClassLoader不能加載該類才由自己去加載,這個方法從本ClassLoader的搜索路徑中查找該類
            long t1 = System.nanoTime();
            c = findClass(name);
        }
    }
    return c;
}
protected final Class<?> findLoadedClass(String name) {
    ClassLoader loader;
    //BootClassLoader加載的緩存
    if (this == BootClassLoader.getInstance())
        loader = null;
    else
        loader = this;
    return VMClassLoader.findLoadedClass(loader, name);
}
  1. 系統(tǒng)類會優(yōu)先通過BootClassLoader進行加載,反正串改系統(tǒng)類子房。

  2. parent.loadClass(name, false)這步就是雙親委派機制的妙處了形用。優(yōu)先從父類讀取dex,父類找不到再由自己用findClass去加載证杭。

BaseDexClassLoader#findClass
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    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;
}

可以看出田度,它是通過pathList來加載類的。

DexPathList#findClass
public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
             //調(diào)用到c層defineClassNative方法進行查找
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

前面創(chuàng)建對象的時候解愤,BaseDexClassLoader構(gòu)造函數(shù)中創(chuàng)建了pathList對象镇饺,DexPathList對象中makeElements方法通過dex路徑創(chuàng)建了dexElements對象。而加載類時也是通過遍歷dexElements送讲,通過其DexFileloadClassBinaryName來加載類奸笤。

所以這里是==一個熱修復(fù)的點==,你可以將需要熱修復(fù)的dex文件插入到dexElements數(shù)組前面哼鬓,這樣遍歷的時候查到你最新插入的則返回监右,從而實現(xiàn)動態(tài)替換有問題類

DexFile#loadClassBinaryName
public Class loadClassBinaryName(String name, ClassLoader loader) {
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);

最終還是調(diào)用了Native方法defineClass加載類。有趣的是异希,標準JVM中健盒,ClassLoader是用defineClass加載類的,而Android中defineClass被棄用了,改用了loadClass方法扣癣,而且加載類的過程也挪到了DexFile中惰帽,在DexFile中加載類的具體方法也叫defineClass,不知道是Google故意寫成這樣的還是巧合父虑。

2.5 熱修復(fù)方案

2.5.1 向PathClassLoader的dexElements進行插入新的dex(目前最常見的方式)

從上面的ClassLoader#loadClass方法你就會知道该酗,初始化的時候會進入BaseDexClassLoader#findClass方法中通過遍歷dexElements進行查找dex文件,因為dexElements是一個數(shù)組士嚎,所以我們可以通過反射的形式垂涯,將需要熱修復(fù)的dex文件插入到數(shù)組首部,這樣遍歷數(shù)組的時候就會優(yōu)先讀取你插入的dex航邢,從而實現(xiàn)熱修復(fù)耕赘。

image

我們可以通過創(chuàng)建一個DexClassLoader對象,來獲取要加載dex的dexElements對象膳殷,然后插入PathClassLoader的dexElements中操骡。 這也是公司框架的熱修復(fù)實現(xiàn)方式。

實現(xiàn)細節(jié)赚窃,創(chuàng)建熱修復(fù)中的對象
//1.創(chuàng)建DexClassLoader對象,這里optimizedDirectory為libs
DexClassLoader loader = new DexClassLoader(dexPath, context.getDir("libs", 0).getAbsolutePath(), libraryPath, context.getClassLoader());
//2.獲取PathClassLoader對象
PathClassLoader pathLoader = context.getClassLoader();
//3.通過反射獲取響應(yīng)的PathList對象
Object pathLoaderPathList = getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
Object dexLoaderPathList = getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
//4.通過反射獲取相應(yīng)的DexElements對象
Object pathDexElements = getField(pathLoaderPathList, pathLoaderPathList.getClass(), "dexElements");
Object dexDexElements = getField(dexLoaderPathList, dexLoaderPathList.getClass(), "dexElements");
//5.將要加載的dex插入PathClassLoader的dexElements中册招,比較dexFile的mFileName,如果有相同的勒极,先置空以去除重復(fù)的dexElements是掰,再將兩個dexElements合并
Object dexElements = combine(pathDexElements, dexDexElements);
//6.將合并后的dexElements放入pathLoaderPathList中
setFieldValue(pathLoaderPathList, pathLoaderPathList.getClass(), "dexElements", dexElements);
//7.獲取要創(chuàng)建的類Class,創(chuàng)建DexClassLoader對象辱匿,因為雙親委派機制键痛,會由PathClassLoader加載類(直接使用PathClassLoader加載也行)
loader = new DexClassLoader(dexPath, context.getDir("libs", 0).getAbsolutePath(), libraryPath, context.getClassLoader());
Class clazz = loader.loadClass(classPath);
//8.通過反射創(chuàng)建對象
Constructor<?> constructor = clazz.getConstructor(Context.class);
Object ret = constructor.newInstance(c);

//通過反射獲取變量
private Object getField(Object obj, Class<?> classObject, String fieldName) {
    Field localField = null;

    try {
        if (obj != null && classObject != null && fieldName != null) {
            localField = classObject.getDeclaredField(fieldName);
            if (localField != null) {
                localField.setAccessible(true);
                Object var7 = localField.get(obj);
                return var7;
            }
        }
    } catch (Exception var10) {
    } finally {
    }

    return null;
}

//合并兩個dexElements,并去除重復(fù)的dexElement
private Object combine(Object local, Object tar) {
    Class<?> localClass = null;
    int len = 0;
    int len_i = 0;
    int len_local = false;
    int len_tar = false;
    Object locObj = null;
    Object tarObj = null;
    Object loc = null;
    Object result = null;
    Object obj_loc = null;
    Object obj_loc_name = null;
    Object obj_tar = null;
    Object obj_tar_name = null;

    try {
        if (local != null && tar != null) {
            int len_local = Array.getLength(local);
            int len_tar = Array.getLength(tar);
            int i;
            //1.去除重復(fù)的dexElement
            if (len_local > 0 && len_tar > 0) {
                for(i = 0; i < len_local; ++i) {
                    locObj = Array.get(local, i);
                    if (locObj != null) {
                        obj_loc = this.a(locObj, locObj.getClass(), "dexFile");
                        if (obj_loc != null) {
                            obj_loc_name = this.a(obj_loc, obj_loc.getClass(), "mFileName");
                            if (obj_loc_name != null) {
                                for(int j = 0; j < len_tar; ++j) {
                                    tarObj = Array.get(tar, j);
                                    if (tarObj != null) {
                                        obj_tar = this.a(tarObj, tarObj.getClass(), "dexFile");
                                        if (obj_tar != null) {
                                            obj_tar_name = this.a(obj_tar, obj_tar.getClass(), "mFileName");
                                            if (obj_tar_name != null && obj_tar_name.equals(obj_loc_name)) {
                                                Array.set(local, i, (Object)null);
                                                break;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            for(i = 0; i < len_local; ++i) {
                locObj = Array.get(local, i);
                if (locObj != null) {
                    ++len;
                }
            }

            localClass = local.getClass().getComponentType();
            if (localClass != null) {
                if (len > 0) {
                    loc = Array.newInstance(localClass, len);
                    if (loc != null) {
                        for(i = 0; i < len_local; ++i) {
                            if (Array.get(local, i) != null) {
                                Array.set(loc, len_i, Array.get(local, i));
                                ++len_i;
                            }
                        }
                    }
                }

                result = Array.newInstance(localClass, len + len_tar);
                if (result != null) {
                    for(i = 0; i < len + len_tar; ++i) {
                        if (i < len) {
                            Array.set(result, i, Array.get(loc, i));
                        } else {
                            Array.set(result, i, Array.get(tar, i - len));
                        }
                    }

                    Object var19 = result;
                    return var19;
                }
            }
        }
    } catch (Exception var22) {
    }

    return null;
}

//通過反射將值存入field中
private void setFieldValue(Object obj, Class<?> classObject, String field, Object value) {
    Field localField = null;

    try {
        if (obj != null && classObject != null && field != null && value != null) {
            localField = classObject.getDeclaredField(field);
            if (localField != null) {
                localField.setAccessible(true);
                localField.set(obj, value);
            }
        }
    } catch (Exception var10) {
    } 
}

2.5.2 通過自定義ClassLoader實現(xiàn)class攔截替換

我們知道PathClassLoader是加載已安裝的apk的dex匾七,那我們可以
在 PathClassLoader 和 BootClassLoader 之間插入一個 自定義的MyClassLoader絮短,而我們通過ClassLoader#loadClass方法中的第2步知道,若parent不為空昨忆,會調(diào)用parent.loadClass方法丁频,固我們可以在MyClassLoader中重寫loadClass方法,在這個里面做一個判斷去攔截替換掉我們需要修復(fù)的class邑贴。

也就是說席里,利用雙親委派模式,在PathClassLoader加載類時拢驾,先調(diào)用MyClassLoader的loadClass奖磁,在自定義的loadClass方法中,加載熱修復(fù)的類独旷。


image
具體實現(xiàn)
  1. 創(chuàng)建MyClassLoader署穗,parent要設(shè)置為pathClassLoader的Parent
ClassLoader pathClassLoader = MyApplication.getContext().getClassLoader();
MyClassLoader myClassLoader = new MyClassLoader(pathClassLoader.getParent());
  1. 將pathClassLoader的Parent設(shè)為MyClassLoader寥裂,將MyClassLoader插入PathClassLoader 和 BootClassLoader 之間
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(classLoader, newParent);
  1. 利用DexClassLoader加載熱修復(fù)的類嵌洼。
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, context.getDir("dex",context.MODE_PRIVATE).getAbsolutePath(),null, context.getClassLoader());
Class<?> herosClass = dexClassLoader.getClass().getSuperclass();
Method m1 = herosClass.getDeclaredMethod("findClass", String.class);
m1.setAccessible(true);
Class newClass = (Class) m1.invoke(dexClassLoader, className);
  1. 存入自定義的MyClassLoader中,在其loadClass加載相應(yīng)的熱加載類
myClassLoader.registerClass(className, newClass);

2.6 CLASS_ISPREVERIFIED標記

android5.0之前案疲,在 Dalvik虛擬機下,執(zhí)行 dexopt 時麻养,會對類進行掃描褐啡,如果類里面所有直接依賴的類都在同一個 dex 文件中,那么這個類就會被打上 CLASS_ISPREVERIFIED 標記鳖昌,表示這個類已經(jīng)預(yù)先驗證過了备畦。如果一個類有 CLASS_ISPREVERIFIED標記,那么在熱修復(fù)時许昨,它加載了其他 dex 文件中的類懂盐,會報經(jīng)典的Class ref in pre-verified class resolved to unexpected implementation異常

為了解決這個問題,QQ空間給出的解決方案就是糕档,準備一個 AntilazyLoad 類莉恼,這個類會單獨打包成一個 hack.dex,然后在所有的類的構(gòu)造方法中增加這樣的代碼:

if (ClassVerifier.PREVENT_VERIFY) {
   System.out.println(AntilazyLoad.class);
}

復(fù)制代碼這樣在 odex 過程中速那,每個類都會出現(xiàn) AntilazyLoad 在另一個dex文件中的問題俐银,所以odex的驗證過程也就不會繼續(xù)下去,這樣做犧牲了dvm對dex的優(yōu)化效果了端仰。

總結(jié)

本文先介紹了

  • Dalvik與art虛擬機捶惜,
  • jit即時編譯與aot靜態(tài)編譯,
  • dex與odex與oat格式文件荔烧。
  • PathClassLoader吱七,DexClassLoader,BaseDexClassLoader的關(guān)系鹤竭;
  • 雙親委派機制加載類陪捷;
  • BaseDexClassLoader構(gòu)造函數(shù)中創(chuàng)建了pathList對象,DexPathList對象中makeElements方法通過dex路徑創(chuàng)建了dexElements對象诺擅。而加載類時也是通過遍歷dexElements市袖,通過其DexFile的loadClassBinaryName來加載類。
  • 利用加載機制實現(xiàn)熱修復(fù)的兩種方式:1.插入PathClassLoader的dexElements烁涌,利用其記載類的順序苍碟;2.利用雙親委派機制,自定義ClassLoader插入PathClassLoader 和 BootClassLoader 之間撮执,通過自定義的loadClass來加載類微峰。

參考

Android虛擬機的JIT編譯器

Dalvik和Art,JIT ,AOT, oat, dex, odex

說一說Android的Dalvik,ART與JIT抒钱,AOT

解讀:華為方舟編譯器的革命性到底體現(xiàn)在哪里蜓肆?

9102年了颜凯,還不知道Android為什么卡?

Android動態(tài)加載基礎(chǔ) ClassLoader工作機制

剖析ClassLoader深入熱修復(fù)原理

2018 深入解析Android熱修復(fù)技術(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仗扬,一起剝皮案震驚了整個濱河市症概,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌早芭,老刑警劉巖彼城,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異退个,居然都是意外死亡募壕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門语盈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舱馅,“玉大人,你說我怎么就攤上這事刀荒〈停” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵照棋,是天一觀的道長资溃。 經(jīng)常有香客問我,道長烈炭,這世上最難降的妖魔是什么溶锭? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮符隙,結(jié)果婚禮上趴捅,老公的妹妹穿的比我還像新娘。我一直安慰自己霹疫,他們只是感情好拱绑,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丽蝎,像睡著了一般猎拨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上屠阻,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天红省,我揣著相機與錄音,去河邊找鬼国觉。 笑死吧恃,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的麻诀。 我是一名探鬼主播痕寓,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼傲醉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了呻率?” 一聲冷哼從身側(cè)響起硬毕,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎筷凤,沒想到半個月后昭殉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苞七,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡藐守,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蹂风。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卢厂。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惠啄,靈堂內(nèi)的尸體忽然破棺而出慎恒,到底是詐尸還是另有隱情,我是刑警寧澤撵渡,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布融柬,位于F島的核電站,受9級特大地震影響趋距,放射性物質(zhì)發(fā)生泄漏粒氧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一节腐、第九天 我趴在偏房一處隱蔽的房頂上張望外盯。 院中可真熱鬧,春花似錦翼雀、人聲如沸饱苟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至窄俏,卻和暖如春狈邑,著一層夾襖步出監(jiān)牢的瞬間城须,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工官地, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留酿傍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓驱入,卻偏偏與公主長得像赤炒,于是被迫代替她去往敵國和親氯析。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

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