Android熱更新技術的研究與實現【安卓巴士博文大賽】 [復制鏈接]

2017-8-19 09:28
zeki_10 閱讀:13483 評論:16 贊:13
Tag:  熱更新Dex加載Dex加載

Android熱更新技術的研究與實現

------必備引言------

最近時間確實有點緊張啦!不過我還是連日趕工完成了這篇文章,沒辦法,陽陽的神秘大獎對我來說誘惑很大呀!

之前的比賽中第一篇博文介紹了kotlin語言:使用Kotlin來開發Android【安卓巴士博文大賽】

第二篇是正在重構為kotlin的項目kibo,主要側重于使用框架的講解:使用Kotlin開發Android項目-Kibo【安卓巴士博文大賽】

本文因為篇幅較長,可能會有錯別字出現,望見諒。

第一部分重點是對當下熱門的熱更新方案進行研究,第二部分則是自己動手實現一個自己的熱更新框架。

Android熱更新技術的研究與實現之研究篇

熱更新這個詞出現的時間已經很久了,感覺現在要找工作才來看是晚了不少,但是好東西什么時候學習都不晚的。
今天看到一句話,和大家分享下,人一生有三樣東西是別人搶不走的:

  1. 吃進胃里的食物

  2. 藏在心中的夢想

  3. 讀進大腦里的書

所以趁著我們的時光正好,多學點東西肯定是賺翻的!!(當然多吃點也沒錯,不配點圖感覺好突兀)

言歸正傳,首先我們要了解與熱更新相關的一些概念吧!

---------概念講解--------

熱更新 相關概念

  • 組件化----就是將一個app分成多個模塊,每個模塊都是一個組件(Module),開發的過程中我們可以讓這些組件相互依賴或者單獨調試部分組件等,但是最終發布的時候是將這些組件合并統一成一個apk,這就是組件化開發。我之前的開發方式基本上都是這一種。具體可以參考Android組件化方案

  • 插件化--將整個app拆分成很多模塊,這些模塊包括一個宿主多個插件,每個模塊都是一個apk(組件化的每個模塊是個lib),最終打包的時候將宿主apk和插件apk分開或者聯合打包。開發中,往往會堆積很多的需求進項目,超過 65535 后,插件化就是一個解決方案。

具體組件化和插件化分析大家可以看這個系列,講解和例子以及源碼都很清楚:APP項目如何與插件化無縫結合

放張圖幫大家理解:

組件化和插件化

  • 熱更新 – 更新的類或者插件粒度較小的時候,我們會稱之為熱修復,一般用于修復bug!!比如更新一個bug方法或者緊急修改lib包,甚至一個類等。2016 Google 的 Android Studio 推出了Instant Run 功能 同時提出了3個名詞;

    1. 熱部署” – 方法內的簡單修改,無需重啟app和Activity。

    2. 暖部署” – app無需重啟,但是activity需要重啟,比如資源的修改。

    3. 冷部署” – app需要重啟,比如繼承關系的改變或方法的簽名變化等。

  • 增量更新,與熱更新區別最大的一個,其實這個大家應該很好理解,安卓上的有些很大的應用,特別是游戲,大則好幾個G的多如牛毛,但是每次更新的時候卻不是要去下載最新版,而只是下載一個幾十兆的增量包就可以完成更新了,而這所使用的技術就是增量更新了。實現的過程大概是這個樣子的:我們手機上安裝著某個大應用,下載增量包之后,手機上的apk和增量包合并形成新的包,然后會再次安裝,這個安裝過程可能是可見的,或者應用本身有足夠的權限直接在后臺安裝完成。

今天碰到Android Studio的更新,這應該就是增量更新啦!補丁包只有51M,如果下載新版本有1G多。

Studio增量更新

而熱更新究竟是什么呢?

有一些這樣的情況, 當一個App發布之后,突然發現了一個嚴重bug需要進行緊急修復,這時候公司各方就會忙得焦頭爛額:重新打包App、測試、向各個應用市場和渠道換包、提示用戶升級、用戶下載、覆蓋安裝。有時候僅僅是為了修改了一行代碼,也要付出巨大的成本進行換包和重新發布。老是發布版本用戶會瘋掉的!!!(好吧 猿猿們也會瘋掉。。)

update again bug-fighting!

這時候就提出一個問題:有沒有辦法以補丁的方式動態修復緊急Bug,不再需要重新發布App,不再需要用戶重新下載,覆蓋安裝?

這種需要替換運行時新的類和資源文件的加載,就可以認為是熱操作了。而在熱更新出現之前,通過反射注解、反射調用和反射注入等方式已經可以實現類的動態加載了。而熱更新框架的出現就是為了解決這樣一個問題的。

從某種意義上來說,熱更新就是要做一件事,替換。當替換的東西屬于大塊內容的時候,就是模塊化了,當你去替換方法的時候,叫熱更新,當你替換類的時候,加熱插件,而且重某種意義上講,所有的熱更新方案,都是一種熱插件,因為熱更新方案就是在app之外去干這個事。就這么簡單的理解。無論是替換一個類,還是一個方法,都是在干替換這件事請。。這里的替換,也算是幾種hook操作,無論在什么代碼等級上,都是一種侵入性的操作。

所以總結一句話簡單理解熱更新就是改變app運行行為的技術!(或者說就是對已發布app進行bug修復的技術) 此時的猿猿們頓時眼前一亮,用戶也笑了。。

good job! so cool!

好的,現在我們已經知道熱更新為何物了,那么我們就先看看熱更新都有哪些成熟的方案在使用了。

熱更新方案介紹

熱更新方案發展至今,有很多團隊開發過不同的解決方案,包括Dexposed、AndFix,(HotFix)Sophix,Qzone超級補丁的類Nuwa方式,微信的Tinker, 大眾點評的nuwa、百度金融的rocooFix, 餓了么的amigo以及美團的robust、騰訊的Bugly熱更新。
蘋果公司現在已經禁止了熱更新,不過估計也組織不了開發者們的熱情吧!

我先講幾種方案具體如何使用,說下原理,最后再講如何實現一個自己的熱更新方案!

--Dexposed & AndFix & (HotFix)SopHix --阿里熱更新方案

Dexposed (阿里熱更新方案一)

"Dexposed" 是大廠阿里以前的一個開源熱更新項目,基于 Xposed "Xposed"的AOP框架,方法級粒度,可以進行AOP編程、插樁、熱補丁、SDK hook等功能。

Xposeed 大家如果不熟悉的話可以看下: Xposed源碼剖析——概述,我以前用 Xposed 做過一些小東西(其實就是獲取 root 權限后hook 修改一些手機數據,比如支付寶步數,qq 微信步數等,當然了,余額啥的是改不了滴),在這里就不獻丑了,畢竟重點也不是這個。我們可以看出 Xposed 有一個缺陷就是需要 root ,而 Dexposed 就是一個不需要 root 權限的 hook 框架。以前阿里的主流 app ,例如手機淘寶,支付寶,天貓都使用了 Dexposed 支持在線熱更新,現在已經不用了,用最新的 Sophix 了,后面講。

Dexposed 中的 AOP 原理來自于 Xposed。在 Dalvik 虛擬機下,主要是通過改變一個方法對象方法在 Dalvik 虛擬機中的定義來實現,具體做法就是將該方法的類型改變為 native 并且將這個方法的實現鏈接到一個通用的 Native Dispatch 方法上。這個 Dispatch 方法通過 JNI 回調到 Java 端的一個統一處理方法,最后在統一處理方法中調用 before , after 函數來實現AOP。在 Art 虛擬機上目前也是是通過改變一個 ArtMethod 的入口函數來實現。

Dexposed

可惜 android 4.4之后的版本都用 Art 取代了 Dalvik ,所以要 hook Android4.4 以后的版本就必須去適配 Art 虛擬機的機制。目前官方表示,為了適配 Art 的 dexposed_l 只是 beta 版,所以最好不要在正式的線上產品中使用它。

現在阿里已經拋棄 Dexposed 了,原因很明顯,4.4 以后不支持了,我們就不細細分析這個方案了,感興趣的朋友可以通過"這里"了解。簡單講下它的實現方式:

  1. 引入一個名為 patchloader 的 jar 包,這個函數庫實現了一個熱更新框架,宿主 apk (可能含有 bug 的上線版本)在發布時會將這個 jar 包一起打包進 apk 中;

  2. 補丁 apk (已修復線上版本 bug 的版本)只是在編譯時需要這個 jar 包,但打包成 apk 時不包含這個 jar 包,以免補丁 apk 集成到宿主 apk 中時發生沖突;

  3. 補丁 apk 將會以 provided 的形式依賴 dexposedbridge.jar 和 patchloader.jar;

  4. 通過在線下載的方式從服務器下載補丁 apk ,補丁 apk 集成到宿主 apk 中,使用補丁 apk 中的函數替換原來的函數,從而實現在線修復 bug 的功能。

AndFix (阿里熱更新方案二)

AndFix 是一個 Android App 的在線熱補丁框架。使用此框架,我們能夠在不重復發版的情況下,在線修改 App 中的 Bug 。AndFix 就是 “Android Hot-Fix”的縮寫。支持 Android 2.3到6.0版本,并且支持 arm 與 X86 系統架構的設備。完美支持 Dalvik 與 ART 的 Runtime。AndFix 的補丁文件是以 .apatch 結尾的文件。它從你的服務器分發到你的客戶端來修復你 App 的 bug 。

AndFix 更新實現過程(畫的丑勿怪⊙﹏⊙):

AndFix 更新實現過程

  1. 首先添加依賴

    `compile 'com.alipay.euler:andfix:[email protected]'`
    
  2. 然后在 Application.onCreate() 中添加以下代碼

    `patchManager = new PatchManager(context);`
    
    `patchManager.init(appversion);//current version`
    
    `patchManager.loadPatch();`
    
  3. 可以用這句話獲取 appversion,每次 appversion 變更都會導致所有補丁被刪除,如果 appversion 沒有改變,則會加載已經保存的所有補丁。

    `String appversion= getPackageManager().getPackageInfo(getPackageName(), 0).versionName;`
    
  4. 然后在需要的地方調用 PatchManager 的 addPatch 方法加載新補丁,比如可以在下載補丁文件之后調用。

  5. 之后就是打補丁的過程了,首先生成一個 apk 文件,然后更改代碼,在修復 bug 后生成另一個 apk。通過官方提供的工具 apkpatch 生成一個 .apatch 格式的補丁文件,需要提供原 apk,修復后的 apk,以及一個簽名文件。

  6. 通過網絡傳輸或者 adb push 的方式將 apatch 文件傳到手機上,然后運行到 addPatch 的時候就會加載補丁。

AndFix 更新的原理:

  1. 首先通過虛擬機的 JarFile 加載補丁文件,然后讀取 PATCH.MF 文件得到補丁類的名稱

  2. 使用 DexFile 讀取 patch 文件中的 dex 文件,得到后根據注解來獲取補丁方法,然后根據注解中得到雷鳴和方法名,使用 classLoader 獲取到 Class,然后根據反射得到 bug 方法。

  3. jni 層使用 C++ 的指針替換 bug 方法對象的屬性來修復 bug。

具體的實現主要都是我們在 Application 中初始化的PatchManager中(具體分析在后面的注釋可以看到)。

public PatchManager(Context context) {
    mContext = context;
    mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager
    mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存放patch補丁文件的文件夾
    mPatchs = new ConcurrentSkipListSet<Patch>();//初始化存在Patch類的集合,此類適合大并發
    mLoaders = new ConcurrentHashMap<String, ClassLoader>();//初始化存放類對應的類加載器集合
}

其中mAndFixManager = new AndFixManager(mContext);的實現:

public AndFixManager(Context context) {
    mContext = context;
    mSupport = Compat.isSupport();//判斷Android機型是否適支持AndFix
    if (mSupport) {
        mSecurityChecker = new SecurityChecker(mContext);//初始化簽名判斷類
        mOptDir = new File(mContext.getFilesDir(), DIR);//初始化patch文件存放的文件夾
        if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
            mSupport = false;
            Log.e(TAG, "opt dir create error.");
        } else if (!mOptDir.isDirectory()) {// not directory
            mOptDir.delete();//如果不是文件目錄就刪除
            mSupport = false;
        }
    }
}

。。。。。。。。。。。。

然后是對版本的初始化mPatchManager.init(appversion)init(String appVersion)代碼如下:

 public void init(String appVersion) {
    if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
        Log.e(TAG, "patch dir create error.");
        return;
    } else if (!mPatchDir.isDirectory()) {// not directory
        mPatchDir.delete();
        return;
    }
    SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
            Context.MODE_PRIVATE);//存儲關于patch文件的信息
    //根據你傳入的版本號和之前的對比,做不同的處理
    String ver = sp.getString(SP_VERSION, null);
    if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
        cleanPatch();//刪除本地patch文件
        sp.edit().putString(SP_VERSION, appVersion).commit();//并把傳入的版本號保存
    } else {
        initPatchs();//初始化patch列表,把本地的patch文件加載到內存
    }
}
/*************省略初始化、刪除、加載具體方法實現*****************/

init 初始化主要是對 patch 補丁文件信息進行保存或者刪除以及加載。

那么 patch 補丁文件是如何加載的呢?其實 patch 補丁文件本質上是一個 jar 包,使用 JarFile 來讀取即可:

public Patch(File file) throws IOException {
    mFile = file;
    init();
}

@SuppressWarnings("deprecation")
private void init() throws IOException {
    JarFile jarFile = null;
    InputStream inputStream = null;
    try {
        jarFile = new JarFile(mFile);//使用JarFile讀取Patch文件
        JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);//獲取META-INF/PATCH.MF文件
        inputStream = jarFile.getInputStream(entry);
        Manifest manifest = new Manifest(inputStream);
        Attributes main = manifest.getMainAttributes();
        mName = main.getValue(PATCH_NAME);//獲取PATCH.MF屬性Patch-Name
        mTime = new Date(main.getValue(CREATED_TIME));//獲取PATCH.MF屬性Created-Time

        mClassesMap = new HashMap<String, List<String>>();
        Attributes.Name attrName;
        String name;
        List<String> strings;
        for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
            attrName = (Attributes.Name) it.next();
            name = attrName.toString();
            //判斷name的后綴是否是-Classes,并把name對應的值加入到集合中,對應的值就是class類名的列表
            if (name.endsWith(CLASSES)) {
                strings = Arrays.asList(main.getValue(attrName).split(","));
                if (name.equalsIgnoreCase(PATCH_CLASSES)) {
                    mClassesMap.put(mName, strings);
                } else {
                    mClassesMap.put(
                            name.trim().substring(0, name.length() - 8),// remove
                                                                        // "-Classes"
                            strings);
                }
            }
        }
    } finally {
        if (jarFile != null) {
            jarFile.close();
        }
        if (inputStream != null) {
            inputStream.close();
        }
    }

}

然后就是最重要的patchManager.loadPatch()

public void loadPatch() {
    mLoaders.put("*", mContext.getClassLoader());// wildcard
    Set<String> patchNames;
    List<String> classes;
    for (Patch patch : mPatchs) {
        patchNames = patch.getPatchNames();
        for (String patchName : patchNames) {
            classes = patch.getClasses(patchName);//獲取patch對應的class類的集合List
            mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
                    classes);//修復bug方法
        }
    }
}

循環獲取補丁對應的 class 類來修復 bug 方法,mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),classes)

 public synchronized void fix(File file, ClassLoader classLoader,
        List<String> classes) {
    if (!mSupport) {
        return;
    }
    //判斷patch文件的簽名
    if (!mSecurityChecker.verifyApk(file)) {// security check fail
        return;
    }

         /******省略部分代碼********/

        //加載patch文件中的dex
        final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                optfile.getAbsolutePath(), Context.MODE_PRIVATE);

        if (saveFingerprint) {
            mSecurityChecker.saveOptSig(optfile);
        }

        ClassLoader patchClassLoader = new ClassLoader(classLoader) {
            @Override
            protected Class<?> findClass(String className)
                    throws ClassNotFoundException {//重寫ClasLoader的findClass方法
                Class<?> clazz = dexFile.loadClass(className, this);
                if (clazz == null
                        && className.startsWith("com.alipay.euler.andfix")) {
                    return Class.forName(className);// annotation’s class
                                                    // not found
                }
                if (clazz == null) {
                    throw new ClassNotFoundException(className);
                }
                return clazz;
            }
        };
        Enumeration<String> entrys = dexFile.entries();
        Class<?> clazz = null;
        while (entrys.hasMoreElements()) {
            String entry = entrys.nextElement();
            if (classes != null && !classes.contains(entry)) {
                continue;// skip, not need fix
            }
            clazz = dexFile.loadClass(entry, patchClassLoader);//獲取有bug的類文件
            if (clazz != null) {
                fixClass(clazz, classLoader);// next code
            }
        }
    } catch (IOException e) {
        Log.e(TAG, "pacth", e);
   }
}
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
    Method[] methods = clazz.getDeclaredMethods();
    MethodReplace methodReplace;
    String clz;
    String meth;
    for (Method method : methods) {
        //獲取此方法的注解,因為有bug的方法在生成的patch的類中的方法都是有注解的
        methodReplace = method.getAnnotation(MethodReplace.class);
        if (methodReplace == null)
            continue;
        clz = methodReplace.clazz();//獲取注解中clazz的值
        meth = methodReplace.method();//獲取注解中method的值
        if (!isEmpty(clz) && !isEmpty(meth)) {
            replaceMethod(classLoader, clz, meth, method);//next code
        }
    }
}
private void replaceMethod(ClassLoader classLoader, String clz,
        String meth, Method method) {
    try {
        String key = clz + "@" + classLoader.toString();
        Class<?> clazz = mFixedClass.get(key);//判斷此類是否被fix
        if (clazz == null) {// class not load
            Class<?> clzz = classLoader.loadClass(clz);
            // initialize target class
            clazz = AndFix.initTargetClass(clzz);//初始化class
        }
        if (clazz != null) {// initialize class OK
            mFixedClass.put(key, clazz);
            Method src = clazz.getDeclaredMethod(meth,
                    method.getParameterTypes());//根據反射獲取到有bug的類的方法(有bug的apk)
            AndFix.addReplaceMethod(src, method);//src是有bug的方法,method是補丁方法
        }
    } catch (Exception e) {
        Log.e(TAG, "replaceMethod", e);
  }
}
public static void addReplaceMethod(Method src, Method dest) {
    try {
        replaceMethod(src, dest);//調用了native方法
        initFields(dest.getDeclaringClass());
    } catch (Throwable e) {
        Log.e(TAG, "addReplaceMethod", e);
    }
}
private static native void replaceMethod(Method dest, Method src);

從上面的 bug 修復源碼可以看出,就是在找補丁包中有 @MethodReplace 注解的方法,然后反射獲取原 apk 中方法的位置,最后進行替換。

而最后調用的 replaceMethod(Method dest,Method src) 則是 native 方法,源碼中有兩個 replaceMethod:

extern void dalvik_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Dalvik
extern void art_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Art

從源碼的注釋也能看出來,因為安卓 4.4 版本之后使用的不再是 Dalvik 虛擬機,而是 Art 虛擬機,所以需要對不同的手機系統做不同的處理。

首先看 Dalvik 替換方法的實現:

 extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
    JNIEnv* env, jobject src, jobject dest) {
    jobject clazz = env->CallObjectMethod(dest, jClassMethod);
    ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
        dvmThreadSelf_fnPtr(), clazz);
    clz->status = CLASS_INITIALIZED;

    Method* meth = (Method*) env->FromReflectedMethod(src);
    Method* target = (Method*) env->FromReflectedMethod(dest);
    LOGD("dalvikMethod: %s", meth->name);

    meth->jniArgInfo = 0x80000000;
    meth->accessFlags |= ACC_NATIVE;//把Method的屬性設置成Native方法

    int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);
    if (!dvmIsStaticMethod(meth))
    argsSize++;
    meth->registersSize = meth->insSize = argsSize;
    meth->insns = (void*) target;

    meth->nativeFunc = dalvik_dispatcher;//把方法的實現替換成native方法
}

Art 替換方法的實現:

 
 //不同的art系統版本不同處理也不同
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
        JNIEnv* env, jobject src, jobject dest) {
    if (apilevel > 22) {
        replace_6_0(env, src, dest);
    } else if (apilevel > 21) {
        replace_5_1(env, src, dest);
    } else {
        replace_5_0(env, src, dest);
    }
}
//以5.0為例:
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
    art::mirror::ArtMethod* smeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
   
    art::mirror::ArtMethod* dmeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
   
    dmeth->declaring_class_->class_loader_ =
            smeth->declaring_class_->class_loader_; //for plugin classloader
    dmeth->declaring_class_->clinit_thread_id_ =
            smeth->declaring_class_->clinit_thread_id_;
    dmeth->declaring_class_->status_ = (void *)((int)smeth->declaring_class_->status_-1);
    //把一些參數的指針給補丁方法
    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->access_flags_ = dmeth->access_flags_;
    smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
    smeth->dex_cache_initialized_static_storage_ =
            dmeth->dex_cache_initialized_static_storage_;
    smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
    smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
    smeth->vmap_table_ = dmeth->vmap_table_;
    smeth->core_spill_mask_ = dmeth->core_spill_mask_;
    smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
    smeth->mapping_table_ = dmeth->mapping_table_;
    smeth->code_item_offset_ = dmeth->code_item_offset_;
    smeth->entry_point_from_compiled_code_ =
            dmeth->entry_point_from_compiled_code_;
   
    smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
    smeth->native_method_ = dmeth->native_method_;//把補丁方法替換掉
    smeth->method_index_ = dmeth->method_index_;
    smeth->method_dex_index_ = dmeth->method_dex_index_;
   
    LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,
            dmeth->entry_point_from_compiled_code_);

}

其實這個替換過程可以看做三步完成

  1. 打開鏈接庫得到操作句柄,獲取 native 層的內部函數,得到 ClassObject 對象

  2. 修改訪問權限的屬性為 public

  3. 得到新舊方法的指針,新方法指向目標方法,實現方法的替換。

如果我們想知道補丁包中到底替換了哪些方法,可以直接方便易 patch 文件,然后看到的所有含有 @ReplaceMethod 注解的方法基本上就都是需要替換的方法了。

最近我在學習 C++,頓時感覺到還是這種可以控制底層的語言是多么強大,不過安卓可以通過 JNI 調用 C++,也就沒什么可吐槽的了!

好的,現在 AndFix 我們分析了一遍它的實現過程和原理,其優點是不需要重啟即可應用補丁,遺憾的是它還是有不少缺陷的,這直接導致阿里再次拋棄了它,缺陷如下:

  1. 并不能支持所有的方法修復
     AndFix修復范圍

  2. 不支持 YunOS

  3. 無法添加新類和新的字段

  4. 需要使用加固前的 apk 制作補丁,但是補丁文件很容易被反編譯,也就是修改過的類源碼容易泄露。

  5. 使用加固平臺可能會使熱補丁功能失效(看到有人在 360 加固提了這個問題,自己還未驗證)。

Sophix---阿里終極熱修復方案

不過阿里作為大廠咋可能沒有個自己的熱更新框架呢,所以阿里爸爸最近還是做了一個新的熱更新框架 SopHix

方案對比

巴巴再次證明我是最強的,誰都沒我厲害!!!因為我啥都支持,而且沒缺點。。簡直就是無懈可擊!

那么我們就來項目集成下看看具體的使用效果吧!具體就拿支持的方法級替換來演示吧!

先去創建個應用:

創建Sophix應用

獲取 AppId:24582808-1,和 AppSecret:da283640306b464ff68ce3b13e036a6e 以及 RSA 密鑰**。三個參數配置在 application 節點下面:

    <meta-data
        android:name="com.taobao.android.hotfix.IDSECRET"
        android:value="24582808-1" />
    <meta-data
        android:name="com.taobao.android.hotfix.APPSECRET"
        android:value="da283640306b464ff68ce3b13e036a6e" />
    <meta-data
        android:name="com.taobao.android.hotfix.RSASECRET"
        android:value="MIIEvAIBA**********" />

添加 maven 倉庫地址:

repositories {
    maven {
       url "http://maven.aliyun.com/nexus/content/repositories/releases"
    }
}

添加 gradle 坐標版本依賴:

compile 'com.aliyun.ams:alicloud-android-hotfix:3.1.0'

項目結構也很簡單:

項目結構

MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ((TextView)findViewById(R.id.tv)).setText(String.valueOf(BuildConfig.VERSION_NAME));
        findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent;
                intent = new Intent(MainActivity.this,SecondActivity.class);
                startActivity(intent);
            }
        });

      }
}

其實就是有一個文本框顯示當前版本,還有一個按鈕用來跳轉到 SecondActivity

SecondActivity的內容:

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        String  s  = null;
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(SecondActivity.this, "彈出框內容彈出錯啦!", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

也很簡單,只有一個按鈕,按鈕點擊之后彈出一個 Toast 顯示“彈出框內容彈出錯啦!”

就這樣,我們的一個上線 app 完成了(粗糙是粗糙了點),下面來看下效果吧(請諒解我第一次錄屏的渣渣技術,以后會做的越來越好)

bug效果

然后我們的用戶開始用了,發現一個bug!“彈出框彈出的內容是錯誤的!”,用戶可不管別的,馬上給我改好啊!

此時的開發er估計心頭千萬頭草泥馬在奔騰了,求神拜佛上線不要出問題,剛上線就出問題了,“where is my 測試er!!!”不說了,趕緊修吧,最暴力的方法就是 SecondActivity 的 Toast 中彈出“彈出框內容彈正常啦!”一句代碼搞定!bingo!

如果沒有熱更新,可能就要搞個臨時版本或者甚至發布一個新版本,但是現在我們有了 Sophix ,就不需要這么麻煩了。

首先我們去下載補丁打包工具(不得不說,工具確實比較粗糙(丑)。。。)

阿里補丁工具

舊包:<必填> 選擇基線包路徑(有問題的 APK)。

新包:<必填> 選擇新包路徑(修復過該問題 APK)。

日志:打開日志輸出窗口。

高級:展開高級選項

設置:配置其他信息。

GO!:開始生成補丁。

所以首先我們把舊包和新包添加上之后,配置好之后看看會發生什么吧!

強制冷啟動是補丁打完后重啟才生效。

配置

正在生成補丁 補丁生成成功

時間看情況吧,因為項目本身內容比較少,所以生成補丁的速度比較快,等一下就好了。項目比較大的話估計需要等的時間長一點

我們來看看到底生成了什么?打開補丁生成目錄

生成的補丁文件

這個就是我們生成的補丁文件了,下一步補丁如何使用?
我們打開阿里的管理控制臺,將補丁上傳到控制臺,就可以發布了.

補丁上傳

補丁發布

這里有個坑,我用自己的中興手機發現在使用補丁調試工具的時候一直獲取包名錯誤,然后就借了別人的華為手機測試然后就可以了。最后我是在模擬器上完成錄制的。

我們首先下載調試工具來看看效果吧,首先連接應用(坑就在這里,有的手機可能會提示包名錯誤,但是其實是沒有錯的,雖然官網給出了解決方案,可依舊沒有解決,不得已只能用模擬器了)

調試工具

然后有兩種方式可以加載補丁包,一種是掃二維碼,還有一種是加載本地補丁jar包,模擬器上實在不好操作啊!!!最后我屈服了,借了同學的手機掃二維碼加載補丁包了。。。然后就會有 log 提示

調試工具加載補丁包

從圖中的 log 提示我們可以看出首先下載了補丁包,然后打補丁完成,要求我們重啟 APP,那我們就重啟唄,看到的當然就應該是補丁打好的 1.1 版本和 Toast 彈出正常啦!!

更新版本 更新Toast

當然了,目前我們還是在調試工具上加載的補丁包,我們接下來將補丁包發布后就可以不用調試工具,直接打開 app 就可以實現打補丁了,這樣就完成了 bug 的修復!

其實這么看起來確實是非常簡單就實現了熱修復,主要我們的生成補丁工作都是交給阿里提供的工具實現了,其實我們也能看得出來,Sophix 和前面介紹的 AndFix 很像,不同的地方是補丁文件已經給出工具可以一鍵生成了,而且支持的東西更多了。其他比如 so 庫和 library 以及資源文件的更新大家可以查看官方文檔了解。

其實 Sophix 主要是在阿里百川 HotFix 的版本上的一個更新,而 HotFix 又是什么呢?

HotFix和ndFix的關系

所以阿里爸爸一直在進步著呢,知道技術存在問題就要去解決問題,這不,從Dexposed-->AndFix-->HotFix-->Sophix,技術是越來越成熟了。

下面介紹另外一個大廠的幾種熱更新方案

Qzone超級補丁 & 微信Tinker 騰訊熱更新方案

巴巴家的熱更新技術一直在發展,作為互聯網巨頭的騰訊怎甘落后,所以也是窮追不舍的干起來!

Qzone超級補丁(騰訊熱更新方案一) & DEX加載原理

因為超級補丁技術是基于 DEX 分包方案,使用了多 DEX 加載的原理,所以我先給大家簡單講下 DEX加載 的一些東西:

Android 程序要運行需要先編譯打包成 dex,之后才可以被 Android 虛擬機解析運行。因此我們如果想要即時修補 bug 就要讓修復的代碼被 Android 虛擬機識別,如何才能讓虛擬機認識我們修改過的代碼呢,也就是我們需要把修改過的代碼打包成單獨的 dex。
然后接下來要做的就是如何讓虛擬機加載我們修改過后的 dex jar包中的類呢?

我們需要了解的是類加載器是如何加載類的。

在 Android 中有 2 種類加載器: PathClassLoader 和 DexClassLoader,源碼如下:

public class DexClassLoader extends BaseDexClassLoader {  

    public DexClassLoader(String dexPath, String optimizedDirectory,  
            String libraryPath, ClassLoader parent) {  
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);  
    }  
}  

public class PathClassLoader extends BaseDexClassLoader {  

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

這兩者的區別是:

  1. DexClassLoader:可以加載 jar/apk/dex,可以從 SD 卡中加載未安裝的 apk;

  2. PathClassLoader:要傳入系統中 apk 的存放 Path,所以只能加載已經安裝的 apk 文件。

兩個類都只是簡單的對 BaseDexClassLoader 做了一下封裝,具體的實現還是在父類里。不過這里也可以看出,PathClassLoaderoptimizedDirectory 只能是 null,進去 BaseDexClassLoader 看看這個參數是干什么的

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

這里創建了一個 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();
}

我們不需要弄的特別明白,只要知道這里 optimizedDirectory 是用來緩存我們需要加載的 dex 文件的,并創建一個 DexFile 對象,如果它為 null,那么會直接使用 dex 文件原有的路徑來創建DexFile 對象。

optimizedDirectory 必須是一個內部存儲路徑,無論哪種動態加載,加載的可執行文件一定要存放在內部存儲。DexClassLoader 可以指定自己的 optimizedDirectory,所以它可以加載外部的 dex,因為這個 dex 會被復制到內部路徑的 optimizedDirectory;而 PathClassLoader 沒有 optimizedDirectory,所以它只能加載內部的 dex,這些大都是存在系統中已經安裝過的 apk 里面的。

上面還只是創建了類加載器的實例,其中創建了一個 DexFile 實例,用來保存 dex 文件,我們猜想這個實例就是用來加載類的。

Android 中,ClassLoader 用 loadClass 方法來加載我們需要的類

 public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }

        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }
    return clazz;
}

loadClass 方法調用了 findClass 方法,而 BaseDexClassLoader 重載了這個方法,到 BaseDexClassLoader 看看

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);
    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }
    return clazz;
}

結果還是調用了 DexPathList的findClass

public Class findClass(String name) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    return null;
}

這里遍歷了之前所有的 DexFile 實例,其實也就是遍歷了所有加載過的 dex 文件,再調用 loadClassBinaryName 方法一個個嘗試能不能加載想要的類。

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

上面的類加載中 DexPathList 的 findClass,一個 classloader 可以包含多個 dex,其中這個集合中的對象就是所有的 dex 文件,然后調用從頭開始遍歷所有的 dex 如果在 dex 中找到所需要的類,那么就直接返回,也就是說如果存在多個 dex 在前一個 dex 中找到了需要找到的類,也就不會繼續查找其他 dex 中有沒有這個類了。

dex.loadClassBinaryName(name, definingContext)在這個 dex 中查找相應名字的類,之后 defineClass 把字節碼交給虛擬機就完成了類的加載。

也許你看到這里會比較暈,沒關系,上面的你可以當做沒看到,直接看下面這句話吧:如果要加載一個類,就會調用 ClassLoader 的 findClass 方法,在dex中查找這個類,找到后加載到內存

so,我們的關鍵人物就是在 findClass 的時候讓類加載找到我們修復過后的類,而不是未修復的類。
例如,比如說要修復的類名為 BugClass,我們要做的就是將這個類修改為正確的后,打包成 dex 的 jar,然后想辦法讓類加載去查找我們打包的jar中的 BugClass 類 而不是先前的 BugClass 類,這樣,加載類的時候使用的就是我們修復過后的代碼,而忽略掉原本的有問題的代碼。問題又轉變到了如何讓我們自己打包的 dex 文件放到原本的 dex 文件之前,也就是把我們打包的 dex 放到 dexElements 集合的靠前的位置

這樣算是把超級補丁的原理講了一遍,應該有一個大概的認識了,而超級補丁所做的就是讓類加載器只找到我們修復完成的類!

通俗的說 也就是我們要改變的是 dexElements 中的內容,在其中添加一個 dex 而且放在靠前的位置,而 dexElements 是 PathClassLoader類中的一個成員變量。

因為 Qzone 超級補丁方案并沒有開源,在這里只是給大家講了類加載機制來說下實現原理,具體的實現過程應該是這樣子的(圖可能是最直觀的):

替換過程

通過反射的方式獲取應用的 PathdexClassloader —> PathList —> DexElements,再獲取補丁 dex 的 DexClassloader —> PathList —> DexElements,然后通過 combinArray 的方法將2個 DexElements 合并,補丁的 DexElements 放在前面,然后使用合并后的 DexElements 作為 PathdexClassloader 中的 DexElements,這樣在加載的時候就可以優先加載到補丁 dex,從中可以加載到我們的補丁類,能基本保證穩定性與兼容性

優勢:

  1. 沒有合成整包(和微信 Tinker 比起來,下一個講),產物比較小,比較靈活

  2. 可以實現類替換,兼容性高。(某些手機不起作用)

不足:

  1. 不支持即時生效,必須通過重啟才能生效。

  2. 為了實現修復這個過程,必須在應用中加入兩個 dex ! dalvikhack.dex 中只有一個類,對性能影響不大,但是對于 patch.dex 來說,修復的類到了一定數量,就需要花不少的時間加載。

  3. 在 ART 模式下,如果類修改了結構,就會出現內存錯亂的問題。為了解決這個問題,就必須把所有相關的調用類、父類子類等等全部加載到 patch.dex 中,導致補丁包異常的大,進一步增加應用啟動加載的時候,耗時更加嚴重。

微信 Tinker (騰訊熱更新方案二)

對于微信來說,實現熱更新使用一個“高可用”的補丁框架,至少滿足以下幾個條件:

  1. 穩定性與兼容性;微信需要在數億臺設備上運行,即使補丁框架帶來1%的異常,也將影響到數萬用戶。保證補丁框架的穩定性與兼容性是我們的第一要務;

  2. 性能;微信對性能要求也非常苛刻,首先補丁框架不能影響應用的性能,這里基于大部分情況下用戶不會使用到補丁。其次補丁包應該盡量少,這關系到用戶流量與補丁的成功率問題;

  3. 易用性;在解決完以上兩個核心問題的前提下,我們希望補丁框架簡單易用,并且可以全面支持,甚至可以做到功能發布級別。

看完上面的 Qzone 超級補丁方案(主要是給大家講了dex的加載原理,不過這個對后面的分析tinker也很重要)。那么到底有沒有那么一種方案,能做到開發透明,但是卻沒有 QZone 方案的缺陷呢?肯定是有的,比如我們完全可以使用新的 Dex,那樣既不出現 Art 地址錯亂的問題,在 Dalvik 也無須插樁。當然考慮到補丁包的體積,我們不能直接將新的 Dex 放在里面。但我們可以將新舊兩個 Dex 的差異放到補丁包中,最簡單我們可以采用 BsDiff 算法。

tinker-master

Tinker 是微信官方的 Android 熱補丁解決方案,它支持動態下發代碼、So 庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。

Tinker 更像是 APP 的增量更新,在服務器端通過差異性算法,計算出新舊 dex 之間的差異包,推送到客戶端,進行合成。傳統的差異性算法有 BsDiff,而 Tinker 的牛逼之處就在于它自己基于 Dex 的文件格式,研發出了 DexDiff 算法,這個我們后面再說。

如果我們的應用想要集成 Tinker 熱更新的話,可以直接在騰訊的 Bugly 創建自己的應用,然后接入。這里我就創建了一個應用,但是集成我是直接使用官方的例子。因為官方給出的集成步驟很詳細,還有對應的一整套教程,大家用起來應該都很方便。我們先來做個嘗試:

首先創建一個應用,獲取 AppID 和 AppKey,然后在 GitHub 上下載 BuglyHotfixEasyDemo(這里我就不新建項目了,感覺也沒有必要),目錄結構如下:

Bugly創建應用

官方BuglyDemo

BugClass 就是存在錯誤的類:

public class BugClass {

    public String bug() {
        // 這段代碼會報空指針異常
        // String str = null;
        // Log.e("BugClass", "get string length:" + str.length());
        return "This is a bug class";
    }
}

LoadBugClass 就是獲取 BugClass中返回的字符串

public class LoadBugClass {

    /**
     *獲取bug字符串.
     *
     *@return 返回bug字符串
     */
    public static String getBugString() {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

而 MainActivity 中有很多按鈕,其中有一個按鈕式,點擊彈出 Toast,顯示的內容就是上面返回的字符串;

 /**********省略N行代碼*************/ 
 /**
 *根據應用patch包前后來測試是否應用patch包成功.
 *
 *應用patch包前,提示"This is a bug class"
 *應用patch包之后,提示"The bug has fixed"
 */
public void testToast() {
    Toast.makeText(this, LoadBugClass.getBugString(), Toast.LENGTH_SHORT).show();
}

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.btnShowToast:  // 測試熱更新功能  點擊顯示結果按鈕
            testToast();
            break;
/***********再次省略N行代碼************/

從項目結構上看也是很簡單那的一個例子,多渠道打包我們就不嘗試了,就來個簡單的基本打包實現吧!

顯示效果(點擊顯示效果按鈕后,現在還是有 bug 的包,所以顯示的是bug class):

Tinker 熱更新集成實現

1、編譯基準包
  1. 配置基準包的 tinkerId

在配置好如 AppId 等之后還需要在 tinker-support.gradle 文件中需要寫入自己的配置:

tinkerId 最好是一個唯一標識,例如 git 版本號、versionName 等等。 如果你要測試熱更新,你需要對基線版本進行聯網上報。

這里強調一下,基線版本配置一個唯一的 tinkerId,而這個基線版本能夠應用補丁的前提是集成過熱更新 SDK,并啟動上報過聯網,這樣我們后臺會將這個 tinkerId 對應到一個目標版本,例如 tinkerId = "bugly_1.0.0" 對應了一個目標版本是 1.0.0,基于這個版本打的補丁包就能匹配到目標版本。

  1. 編譯生成基準包(原包,含 bug)

執行 assembleRelease 編譯生成基準包:會在 build/baseApk 目錄下生成如下文件,具體路徑和文件名可以自己配置

啟動apk,上報聯網數據
我們每次冷啟動都會請求補丁策略,會上報當前版本號和 tinkerId,這樣我們后臺就能將這個唯一的 tinkerId 對應到一個版本,測試的時候可以打開 logcat 查看我們的日志,如下圖所示:

我們能看到 tinkerId.

2、對基線版本的bug修復

其實就是講 BugClass 中的返回字符串改為 “The bug has fixed”;

3、根據基線版本生成補丁包

修改待修復 apk 路徑、mapping 文件路徑、resId 文件路徑

 /**
  *此處填寫每次構建生成的基準包目錄 
  */
 def baseApkDir = "app-0813-20-54-50" //改成剛才生成的目錄  其實是按日期時間生成的目錄
 tinkerId = "1.0.1-patch"

執行構建補丁包的 task,其實生成的就是 bug 修復的完整 apk

如果你要生成不同編譯環境的補丁包,只需要執行 TinkerSupport 插件生成的 task,比如 buildTinkerPatchRelease 就能生成 release 編譯環境的補丁包。 注: TinkerSupport 插件版本低于 1.0.4 的,需要使用 tinkerPatchRelease 來生成補丁包 。

生成的補丁包在 build/outputs/patch 目錄下:

主要會生成 3 個文件: unSignedApk, signedApk 以及 signedWith7ZipApk 。

unSignedApk 只要將 tinker_result 中的文件壓縮到一個壓縮包即可。

signedApk 將 unSignedApk 使用 jarsigner 進行簽名。

signedWith7ZipApk 主要是對 signedApk 進行解壓再做 sevenZip 壓縮。

4、上傳補丁包到平臺

見證奇跡的時刻到了!!上傳補丁包到平臺并下發編輯規則,點擊發布新補丁,上傳前面生成的 patch 包,平臺會自動為你匹配到目標版本,可以選擇下發范圍(開發設備、全量設備、自定義),填寫完備注之后,點擊立即下發讓補丁生效,這樣你就可以在客戶端當中收到我們的策略,SDK會自動幫你把補丁包下到本地。

Bugly上傳補丁

再次啟動會發現停止運行,那是因為客戶端收到策略需要下載補丁更新,最后的修復后效果:

Bugly修復后

好的,這下 Bugly 熱更新我們就簡單的看了下效果,其所應用的就是微信的 Tinker 方案,其實不難看出,Bugly 和阿里的 Sophix 都是針對補丁包的一種下發策略。

熱更新技術的兩大流派,一種就是阿里的 Native 流派,即 AndFix 和 Sophix,還有一種就是騰訊自己的 Qzone 超級補丁屬于 java 流派,最后微信還是選擇了繼續走自己的 java 流派(自己的路就是要一走到黑!),但是微信并不是固守陳規,而是追求極致!這不得不提到前面說的 DexDiff 算法了:

DexDiff算法:

上面我們說了 dex 的加載過程,我們都知道 dex 文件是運行在 Dalvik 中的字節碼文件,類似于運行于 JVM 中的 class 文件,在反編譯的時候,apk 中會包含一個或者多個*.dex文件,該文件中存儲了我們編寫的代碼,一般情況下我們還會通過工具轉化為 jar,然后通過一些工具反編譯查看(dex2jar)。

jar文件大家應該都清楚,類似于 class 文件的壓縮包,一般情況下,我們直接解壓就可以看到一個個 class 文件。而dex文件我們無法通過解壓獲取內部的 class 文件,那肯定是因為它的格式決定的,具體的格式我們不在這里分析,我們看一下 DexDiff 的基本步驟(細節分析源碼時會講到):

  1. 首先,明確有這么幾個東西,bugdex,bugfixeddex,patchdex;

  2. 其次,計算出bugfixeddex中每一部分(指的是dex結構中的某一特定部分)占用的大小;

  3. 然后,比較bugdex和bugfixeddex的每一部分,對每一部分進行對比,并記錄不同(刪除了哪些,新增了哪些,記錄和存儲以什么形式我們暫時不管)。

  4. 最后,將保存的不同的記錄寫入補丁中

由上面可知 Tinker 中 Dex 的熱更新主要分為三個部分: 一、補丁包的生成; 二、補丁包下發后生成全量 Dex; 三、生成全量Dex后的加載過程。

具體的Tinker是如何實現熱更新的呢?源碼出真知,我們下載tinker的源碼來看看不就知道了嘛,畢竟是開源的嘛!"tinker源碼傳送"

我下載的是目前最新的1.8.1版本。源碼我們挑重點看,主要就找上面所說的三部分來看:

tinker源碼 tinker-patch-lib

一、補丁包的生成;

我們上文在生成補丁的時候,調用了 tinker-support 中的 buildTinkerPatchRelease

當我們運行這個之后,
執行時間最長的當屬 tinkerPatchRelease 的這個過程,

com.tencent.tinker.build.patch.Runner 這個類就是我們在執行 buildTinkerPatchRelease 會執行的類,具體是執行類中的tinkerPatch()方法:

 protected void tinkerPatch() {
    Logger.d("-----------------------Tinker patch begin-----------------------");

    Logger.d(config.toString());
    try {
        //gen patch
        ApkDecoder decoder = new ApkDecoder(config);
        decoder.onAllPatchesStart();
        decoder.patch(config.mOldApkFile, config.mNewApkFile);
        decoder.onAllPatchesEnd();

        //gen meta file and version file
        PatchInfo info = new PatchInfo(config);
        info.gen();

        //build patch
        PatchBuilder builder = new PatchBuilder(config);
        builder.buildPatch();

    } catch (Throwable e) {
        e.printStackTrace();
        goToError();
    }

    Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin());
    Logger.d("Tinker patch done, you can go to file to find the output %s", config.mOutFolder);
    Logger.d("-----------------------Tinker patch end-------------------------");
}

這個其實就是生成補丁的過程,其中調用 com.tencent.tinker.build.decoder.ApkDecoder 中 patch(File oldFile, File newFile) 方法:

 public boolean patch(File oldFile, File newFile) throws Exception {
    writeToLogFile(oldFile, newFile);
    //check manifest change first
    manifestDecoder.patch(oldFile, newFile);

    unzipApkFiles(oldFile, newFile);

    Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

    //get all duplicate resource file
    for (File duplicateRes : resDuplicateFiles) {
    //            resPatchDecoder.patch(duplicateRes, null);
        Logger.e("Warning: res file %s is also match at dex or library pattern, " + "we treat it as unchanged in the new resource_out.zip", getRelativePathStringToOldFile(duplicateRes));
    }

    soPatchDecoder.onAllPatchesEnd();
    dexPatchDecoder.onAllPatchesEnd();
    manifestDecoder.onAllPatchesEnd();
    resPatchDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    return true;
}

從源碼中我們可以看出首先是對 manifest 文件進行檢測,看其是否有更改,如果發現 manifest 的組件有新增,則拋出異常,因為目前 Tinker 暫不支持四大組件的新增。

檢測通過后解壓 apk 文件,遍歷新舊 apk,交給 ApkFilesVisitor 進行處理。

ApkFilesVisitor 的 visitFile 方法中,對于 dex 類型的文件,調用 dexDecoder 進行 patch 操作;我們主要是針對 dexDecoder 進行分析,所以省略 so 類型和 res 類型操作代碼:

  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

        Path relativePath = newApkPath.relativize(file);

        Path oldPath = oldApkPath.resolve(relativePath);

        File oldFile = null;
        //is a new file?!
        if (oldPath.toFile().exists()) {
            oldFile = oldPath.toFile();
        }
        String patternKey = relativePath.toString().replace("\\", "/");

        if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {
            //also treat duplicate file as unchanged
            if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
                resDuplicateFiles.add(oldFile);
            }

            try {
                dexDecoder.patch(oldFile, file.toFile());
            } catch (Exception e) {
        //      e.printStackTrace();
                throw new RuntimeException(e);
            }
            return FileVisitResult.CONTINUE;
        }
        if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {
            //also treat duplicate file as unchanged
            /*****省略so解析,對于so類型的文件,使用soDecoder進行patch操作**************/
        }
        if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {
           /*****省略so解析,對于Res類型文件,使用resDecoder進行操作patch操作**************/
        }
        return FileVisitResult.CONTINUE;
    }

可以看出是調用 DexDiffDecoder.patch(final File oldFile, final File newFile) 方法,源碼如下:

@SuppressWarnings("NewApi")
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
    final String dexName = getRelativeDexName(oldFile, newFile);
    />>>>>>>>>>>>>>>>>>>>>>省略N行代碼<<<<<<<<<<<<<<<<<<<<<</
    try {
        excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
    } 
    />>>>>>>>>>>>>>>>>>>>>>省略N行代碼<<<<<<<<<<<<<<<<<<<<<</

    // If corresponding new dex was completely deleted, just return false.
    // don't process 0 length dex
    if (newFile == null || !newFile.exists() || newFile.length() == 0) {
        return false;
    }

    File dexDiffOut = getOutputPath(newFile).toFile();

    final String newMd5 = getRawOrWrappedDexMD5(newFile);

    //new add file
    if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
        hasDexChanged = true;
        copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
        return true;
    }

    />>>>>>>>>>>>>>>>>>>>>>省略N行代碼<<<<<<<<<<<<<<<<<<<<<</

    RelatedInfo relatedInfo = new RelatedInfo();
    relatedInfo.oldMd5 = oldMd5;
    relatedInfo.newMd5 = newMd5;

    // collect current old dex file and corresponding new dex file for further processing.
    oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));

    dexNameToRelatedInfoMap.put(dexName, relatedInfo);

    return true;
}

由源碼可以看出是先檢測輸入的 dex 文件中是否有不允許修改的類被修改了,如 loader 相關的類是不允許被修改的,這種情況下會拋出異常;

如果 dex 是新增的,直接將該 dex 拷貝到結果文件;

如果 dex 是修改的,收集增加和刪除的 class。oldAndNewDexFilePairList 將新舊 dex 對應關系保存起來,用于后面的分析。

單單只是將新的 dex 文件加入到 addedDexFiles。調用的是 UniqueDexDiffDecoder.patch:

@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
    boolean added = super.patch(oldFile, newFile);
    if (added) {
        String name = newFile.getName();
        if (addedDexFiles.contains(name)) {
            throw new TinkerPatchException("illegal dex name, dex name should be unique, dex:" + name);
        } else {
            addedDexFiles.add(name);
        }
    }
    return added;
}

在 patch 完成后,會調用 generatePatchInfoFile 生成補丁文件。DexFiffDecoder.generatePatchInfoFile 中首先遍歷 oldAndNewDexFilePairList,取出新舊文件對。

判斷新舊文件的 MD5 是否相等,不相等,說明有變化,會根據新舊文件創建 DexPatchGenerator,DexPatchGenerator 構造函數中包含了 15 個 Dex 區域的比較算法:

private DexSectionDiffAlgorithm<StringData> stringDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Integer> typeIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ProtoId> protoIdSectionDiffAlg;
private DexSectionDiffAlgorithm<FieldId> fieldIdSectionDiffAlg;
private DexSectionDiffAlgorithm<MethodId> methodIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassDef> classDefSectionDiffAlg;
private DexSectionDiffAlgorithm<TypeList> typeListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSetRefList> annotationSetRefListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSet> annotationSetSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassData> classDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Code> codeSectionDiffAlg;
private DexSectionDiffAlgorithm<DebugInfoItem> debugInfoSectionDiffAlg;
private DexSectionDiffAlgorithm<Annotation> annotationSectionDiffAlg;
private DexSectionDiffAlgorithm<EncodedValue> encodedArraySectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationsDirectory> annotationsDirectorySectionDiffAlg;

DexDiffDecoder.executeAndSaveTo(OutputStream out) 這個函數里面會根據上面的 15 個算法對 dex 的各個區域進行比較,每個算法代表每個區域,算法的目的就像我們之前描述 DexDiff 第3步的那樣,要知道“刪除了哪些,新增了哪些”,最后生成 dex 文件的差異。

這是整個 dex diff 算法的核心。以 StringDataSectionDiffAlgorithm 為例,算法流程如下:

每個算法都會執行 execute 和 simulatePatchOperation 方法:

    /************省略N行代碼*************/
    this.stringDataSectionDiffAlg.execute();
    this.patchedStringDataItemsOffset = patchedheaderSize + patchedIdSectionSize;
    if (this.oldDex.getTableOfContents().stringDatas.isElementFourByteAligned) {
        this.patchedStringDataItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedStringDataItemsOffset);
    }
    this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);
    /************省略N行代碼*************/

首先看 execute(代碼比較長,因為是算法核心,不好省略,所以分兩部分講下,大家可以去源碼中看com.tencent.tinker.build.dexpatcher.algorithms.diff.DexSectionDiffAlgorithm)

 public void execute() {
    this.patchOperationList.clear();

    this.adjustedOldIndexedItemsWithOrigOrder = collectSectionItems(this.oldDex, true);
    this.oldItemCount = this.adjustedOldIndexedItemsWithOrigOrder.length;

    AbstractMap.SimpleEntry<Integer, T>[] adjustedOldIndexedItems = new AbstractMap.SimpleEntry[this.oldItemCount];
    System.arraycopy(this.adjustedOldIndexedItemsWithOrigOrder, 0, adjustedOldIndexedItems, 0, this.oldItemCount);
    Arrays.sort(adjustedOldIndexedItems, this.comparatorForItemDiff);

    AbstractMap.SimpleEntry<Integer, T>[] adjustedNewIndexedItems = collectSectionItems(this.newDex, false);
    this.newItemCount = adjustedNewIndexedItems.length;
    Arrays.sort(adjustedNewIndexedItems, this.comparatorForItemDiff);

    int oldCursor = 0;
    int newCursor = 0;
    while (oldCursor < this.oldItemCount || newCursor < this.newItemCount) {
        if (oldCursor >= this.oldItemCount) {
            // rest item are all newItem.
            while (newCursor < this.newItemCount) {
                AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor++];
                this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
            }
        } else
        if (newCursor >= newItemCount) {
            // rest item are all oldItem.
            while (oldCursor < oldItemCount) {
                AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor++];
                int deletedIndex = oldIndexedItem.getKey();
                int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
                this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
                markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
            }
        } else {
            AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor];
            AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor];
            int cmpRes = oldIndexedItem.getValue().compareTo(newIndexedItem.getValue());
            if (cmpRes < 0) {
                int deletedIndex = oldIndexedItem.getKey();
                int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
                this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
                markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
                ++oldCursor;
            } else
            if (cmpRes > 0) {
                this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
                ++newCursor;
            } else {
                int oldIndex = oldIndexedItem.getKey();
                int newIndex = newIndexedItem.getKey();
                int oldOffset = getItemOffsetOrIndex(oldIndexedItem.getKey(), oldIndexedItem.getValue());
                int newOffset = getItemOffsetOrIndex(newIndexedItem.getKey(), newIndexedItem.getValue());

                if (oldIndex != newIndex) {
                    this.oldIndexToNewIndexMap.put(oldIndex, newIndex);
                }

                if (oldOffset != newOffset) {
                    this.oldOffsetToNewOffsetMap.put(oldOffset, newOffset);
                }

                ++oldCursor;
                ++newCursor;
            }
        }
      /**********前半部分**********************/
    }

分析:

  1. 首先讀取 oldDex 和 newDex 對應區域的數據并排序,分別 adjustedOldIndexedItems 和 adjustedNewIndexedItems。

  2. 接下來就開始遍歷了,分別根據當前的 cursor,獲取 oldItem 和 newItem,對其 value 對對比:
    如果 <0 ,則認為該 old Item 被刪除了,記錄為 PatchOperation.OP_DEL,并記錄該 oldItem index 到 PatchOperation 對象,加入到 patchOperationList 中。

如果 >0 ,則認為該 newItem 是新增的,記錄為 PatchOperation.OP_ADD,并記錄該 newItem index 和 value 到 PatchOperation 對象,加入到 patchOperationList 中。
如果 =0 ,不會生成 PatchOperation。

  1. 經過上面的遍歷操作,我們得到了一個 patchOperationList對象。
    繼續下半部分代碼:

    /*************后半部分**********************/
    // So far all diff works are done. Then we perform some optimize works.
    // detail: {OP_DEL idx} followed by {OP_ADD the_same_idx newItem}
    // will be replaced by {OP_REPLACE idx newItem}
    Collections.sort(this.patchOperationList, comparatorForPatchOperationOpt);

    Iterator<PatchOperation<T>> patchOperationIt = this.patchOperationList.iterator();
    PatchOperation<T> prevPatchOperation = null;
    while (patchOperationIt.hasNext()) {
        PatchOperation<T> patchOperation = patchOperationIt.next();
        if (prevPatchOperation != null
            && prevPatchOperation.op == PatchOperation.OP_DEL
            && patchOperation.op == PatchOperation.OP_ADD
        ) {
            if (prevPatchOperation.index == patchOperation.index) {
                prevPatchOperation.op = PatchOperation.OP_REPLACE;
                prevPatchOperation.newItem = patchOperation.newItem;
                patchOperationIt.remove();
                prevPatchOperation = null;
            } else {
                prevPatchOperation = patchOperation;
            }
        } else {
            prevPatchOperation = patchOperation;
        }
    }

    // Finally we record some information for the final calculations.
    patchOperationIt = this.patchOperationList.iterator();
    while (patchOperationIt.hasNext()) {
        PatchOperation<T> patchOperation = patchOperationIt.next();
        switch (patchOperation.op) {
            case PatchOperation.OP_DEL: {
                indexToDelOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
            case PatchOperation.OP_ADD: {
                indexToAddOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
            case PatchOperation.OP_REPLACE: {
                indexToReplaceOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
        }
    }
 }

分析:

  1. 首先對 patchOperationList 按照 index 排序,如果 index 一致則先 DEL(刪除)、后ADD(添加)。

  2. 接下來一個對所有的 operation 的迭代,主要將 index 一致的,且連續的 DEL、ADD 轉化為 REPLACE(替換)操作。

  3. 最后將 patchOperationList 轉化為 3 個 Map,分別為: indexToDelOperationMap, indexToAddOperationMap, indexToReplaceOperationMap。

  4. 完成 execute 之后,我們主要的產物就是 3 個 Map,分別記錄了: oldDex 中哪些 index 需要刪除; newDex 中新增了哪些 item;哪些 item 需要替換為新 item。

  5. 這基本上就是DexDif算法的核心思想了( StringDataSectionDiffAlgorithm 舉例,其他的一樣分析);

剛才說了每個算法除了 execute() 還有個 simulatePatchOperation():

 public void simulatePatchOperation(int baseOffset) {
    boolean isNeedToMakeAlign = getTocSection(this.oldDex).isElementFourByteAligned;
    int oldIndex = 0;
    int patchedIndex = 0;
    int patchedOffset = baseOffset;
    while (oldIndex < this.oldItemCount || patchedIndex < this.newItemCount) {
        if (this.indexToAddOperationMap.containsKey(patchedIndex)) {
            PatchOperation<T> patchOperation = this.indexToAddOperationMap.get(patchedIndex);
            if (isNeedToMakeAlign) {
                patchedOffset = SizeOf.roundToTimesOfFour(patchedOffset);
            }
            T newItem = patchOperation.newItem;
            int itemSize = getItemSize(newItem);
            updateIndexOrOffset(this.newToPatchedIndexMap,0,getItemOffsetOrIndex(patchOperation.index, newItem),0,patchedOffset);
            ++patchedIndex;
            patchedOffset += itemSize;
        } else
        if (this.indexToReplaceOperationMap.containsKey(patchedIndex)) {
            PatchOperation<T> patchOperation = this.indexToReplaceOperationMap.get(patchedIndex);
            /*******省略N代碼***********/
            ++patchedIndex;
            patchedOffset += itemSize;
        } else
        if (this.indexToDelOperationMap.containsKey(oldIndex)) {
            ++oldIndex;
        } else
        if (this.indexToReplaceOperationMap.containsKey(oldIndex)) {
            ++oldIndex;
        } else
        if (oldIndex < this.oldItemCount) {
            /*******省略N代碼***********/
            ++oldIndex;
            ++patchedIndex;
            patchedOffset += itemSize;
        }
    }
    this.patchedSectionSize = SizeOf.roundToTimesOfFour(patchedOffset - baseOffset);
}

首先是要遍歷 oldIndex 與 newIndex,分別在 indexToAddOperationMap, indexToReplaceOperationMap, indexToDelOperationMap中查找。
這里關注一點最終的一個產物是 this.patchedSectionSize,由 patchedOffset-baseOffset 得到。

這里有幾種情況會造成 patchedOffset += itemSize:

  1. indexToAddOperationMap 中包含 patchIndex

  2. indexToReplaceOperationMap 包含 patchIndex

  3. 不在 indexToDelOperationMap 與 indexToReplaceOperationMap 中的 oldDex.

這個 patchedSectionSize 其實對應 newDex 的這個區域的 size。所以,包含需要 ADD 的 Item,會被替代的 Item,以及 OLD ITEMS 中沒有被刪除和替代的 Item。

這三者相加即為 newDex 的 itemList。
到這里,StringDataSectionDiffAlgorithm 算法就執行完畢了。

經過這樣的一個算法,我們得到了 PatchOperationList 和對應區域 sectionSize。那么執行完成所有的算法,應該會得到針對每個算法的 PatchOperationList,和每個區域的 sectionSize;每個區域的 sectionSize 實際上換算得到每個區域的 offset。

每個區域的算法,execute 和 simulatePatchOperation 代碼都是復用的父類 com.tencent.tinker.build.dexpatcher.algorithms.diff.DexSectionDiffAlgorithm 的方法,所以其他的都差不多,可以自己查看。
接下來看執行完成所有的算法后的 writeResultToStream 方法:

 private void writeResultToStream(OutputStream os) throws IOException {
    DexDataBuffer buffer = new DexDataBuffer();
    buffer.write(DexPatchFile.MAGIC);
    buffer.writeShort(DexPatchFile.CURRENT_VERSION);
    buffer.writeInt(this.patchedDexSize);
    // we will return here to write firstChunkOffset later.
    int posOfFirstChunkOffsetField = buffer.position();
    buffer.writeInt(0);
    buffer.writeInt(this.patchedStringIdsOffset);
    buffer.writeInt(this.patchedTypeIdsOffset);
    buffer.writeInt(this.patchedProtoIdsOffset);
    /*****省略其他算法***********/
    buffer.write(this.oldDex.computeSignature(false));
    int firstChunkOffset = buffer.position();
    buffer.position(posOfFirstChunkOffsetField);
    buffer.writeInt(firstChunkOffset);
    buffer.position(firstChunkOffset);

    writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList());
    writePatchOperations(buffer, this.typeIdSectionDiffAlg.getPatchOperationList());
    writePatchOperations(buffer, this.typeListSectionDiffAlg.getPatchOperationList());
    /*****省略其他算法***********/

    byte[] bufferData = buffer.array();
    os.write(bufferData);
    os.flush();
}

首先寫了 MAGIC,CURRENT_VERSION 主要用于檢查該文件為合法的 tinker patch 文件。
然后寫入 patchedDexSize,第四位寫入的是數據區的 offset,可以看到先使用 0 站位,等所有的 map list 相關的 offset 書寫結束,寫入當前的位置。

接下來寫入所有的跟 maplist 各個區域相關的 offset(這里各個區域的排序不重要,讀寫一致即可)
然后執行每個算法寫入對應區域的信息,最后生成 patch 文件

其實就是對每個區域比較后將比較的結果寫入 patch 文件中,文件格式寫在 DexDataBuffer 中
生成的文件以 dex 結尾,但需要注意的是,它不是真正的 dex 文件,具體格式分析在 DexDataBuffer 中。

其中 writePatchOperations 方法就是寫入的方法,我們還是只看 stringDataSectionDiffAlg 的:


private <T extends Comparable<T>> void writePatchOperations(
        DexDataBuffer buffer, List<PatchOperation<T>> patchOperationList
) {
    List<Integer> delOpIndexList = new ArrayList<>(patchOperationList.size());
    List<Integer> addOpIndexList = new ArrayList<>(patchOperationList.size());
    List<Integer> replaceOpIndexList = new ArrayList<>(patchOperationList.size());
    List<T> newItemList = new ArrayList<>(patchOperationList.size());

    for (PatchOperation<T> patchOperation : patchOperationList) {
        switch (patchOperation.op) {
            case PatchOperation.OP_DEL: {
                delOpIndexList.add(patchOperation.index);
                break;
            }
            case PatchOperation.OP_ADD: {
                addOpIndexList.add(patchOperation.index);
                newItemList.add(patchOperation.newItem);
                break;
            }
            case PatchOperation.OP_REPLACE: {
                replaceOpIndexList.add(patchOperation.index);
                newItemList.add(patchOperation.newItem);
                break;
            }
        }
    }

    buffer.writeUleb128(delOpIndexList.size());
    int lastIndex = 0;
    for (Integer index : delOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    buffer.writeUleb128(addOpIndexList.size());
    lastIndex = 0;
    for (Integer index : addOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    buffer.writeUleb128(replaceOpIndexList.size());
    lastIndex = 0;
    for (Integer index : replaceOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    for (T newItem : newItemList) {
        if (newItem instanceof StringData) {
            buffer.writeStringData((StringData) newItem);
        } else
        /***********其他*******************/
    }
}

從代碼中我們可以看出我們的寫入步驟:首先把 patchOperationList 轉化為 3 個 OpIndexList,分別對應 DEL, ADD, REPLACE,以及將所有的 item 存入 newItemList。
然后依次寫入:

  1. del 操作的個數,每個 del 的 index

  2. add 操作的個數,每個 add 的 index

  3. replace 操作的個數,每個需要 replace 的 index

  4. 依次寫入 newItemList.

最后來看看我們生成的 patch 是什么樣子的:

  1. 首先包含幾個字段,證明自己是 tinker patch

  2. 包含生成 newDex 各個區域的 offset,即可以將 newDex 劃分了多個區域,定位到起點

  3. 包含 newDex 各個區域的 Item 的刪除的索引( oldDex ),新增的索引和值,替換的索引和值

那么這么看,我們猜測 Patch 的邏輯時這樣的:

  1. 首先根據各個區域的 offset,確定各個區域的起點

  2. 讀取 oldDex 各個區域的 items,然后根據 patch 中去除掉 oldDex 中需要刪除的和需要替換的 item,再加上新增的 item 和替換的 item 即可組成 newOld 該區域的 items。

所以,newDex 的某個區域的包含:

 oldItems - del - replace + addItems + replaceItems

這樣就完成了補丁包的生成過程,那么服務器在下發補丁之后如何合成全量的新 Dex 的呢?下面我們來分析第二部分:

二、補丁包下發后生成全量Dex;

如何合成全量的新Dex來運行

當 app 收到服務器下發的補丁后,會觸發 DefaultPatchListener.onPatchReceived 事件,調用 TinkerPatchService.runPatchService 啟動 patch 進程進行補丁 patch 工作。

UpgradePatch.tryPatch() 中會首先檢查補丁的合法性,簽名,以及是否安裝過補丁,檢查通過后會嘗試 dex, so 以及 res 文件的 patch。

我們主要分析 DexDiffPatchInternal.tryRecoverDexFiles,討論 dex 的 patch 過程。

tryRecoverDexFiles 調用 DexDiffPatchInternal.patchDexFile:

private static void patchDexFile(
    ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
    ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
/**********省略N行代碼  最終都會調用這個方法************/
   new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
}

最終通過 DexPatchApplier.executeAndSaveTo 進行執行及生產全量 dex。

public void executeAndSaveTo(File file) throws IOException {
    OutputStream os = null;
    try {
        os = new BufferedOutputStream(new FileOutputStream(file));
        executeAndSaveTo(os);
    } finally {
        if (os != null) {
            try {
                os.close();
            } catch (Exception e) {
                // ignored.
            }
        }
    }
}

其實就是調用了 DexPatchApplier.executeAndSaveTo(os):
方法代碼比較長,源碼中也是分了三部分注釋:

executeAndSaveTo(os) 三部分之第一部分
 public void executeAndSaveTo(OutputStream out) throws IOException {
    // Before executing, we should check if this patch can be applied to
    // old dex we passed in.
    byte[] oldDexSign = this.oldDex.computeSignature(false);
    if (oldDexSign == null) {
        throw new IOException("failed to compute old dex's signature.");
    }
    if (this.patchFile == null) {
        throw new IllegalArgumentException("patch file is null.");
    }
    byte[] oldDexSignInPatchFile = this.patchFile.getOldDexSignature();
    if (CompareUtils.uArrCompare(oldDexSign, oldDexSignInPatchFile) != 0) {
        throw new IOException(
                String.format(
                        "old dex signature mismatch! expected: %s, actual: %s",
                        Arrays.toString(oldDexSign),
                        Arrays.toString(oldDexSignInPatchFile)
                )
        );
    }

    // Firstly, set sections' offset after patched, sort according to their offset so that
    // the dex lib of aosp can calculate section size.
    TableOfContents patchedToc = this.patchedDex.getTableOfContents();

    patchedToc.header.off = 0;
    patchedToc.header.size = 1;
    patchedToc.mapList.size = 1;

    patchedToc.stringIds.off
            = this.patchFile.getPatchedStringIdSectionOffset();
    patchedToc.typeIds.off
            = this.patchFile.getPatchedTypeIdSectionOffset();
    patchedToc.typeLists.off
    /*****省略其他算法過程************/

    Arrays.sort(patchedToc.sections);

    patchedToc.computeSizesFromOffsets();

// Firstly, set sections' offset after patched, sort according to their offset so that
// the dex lib of aosp can calculate section size.
這里實際上,就是讀取 patchFile 中記錄的值給 patchedDex 的 TableOfContent 中各種 Section (大致對應 map list 中各個 map_list_item )賦值,即設定各個區域的偏移量。

然后就是排序,設置 byteCount 等字段信息。patchedDex 是最終合成的 dex。

executeAndSaveTo(os) 三部分之第二部分
    // Secondly, run patch algorithms according to sections' dependencies.
    this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    /***省略其他算法代碼*****/

    this.stringDataSectionPatchAlg.execute();
    this.typeIdSectionPatchAlg.execute();

    /***省略其他算法代碼*****/

第二部分其實是將 15 種算法初始化了一遍,然后都去執行 execute()。我們依然是拿 stringDataSectionPatchAlg 來分析,其實還是調用的抽象父類 DexSectionPatchAlgorithm 中的 execute 方法:

public void execute() {
    final int deletedItemCount = patchFile.getBuffer().readUleb128();
    final int[] deletedIndices = readDeltaIndiciesOrOffsets(deletedItemCount);

    final int addedItemCount = patchFile.getBuffer().readUleb128();
    final int[] addedIndices = readDeltaIndiciesOrOffsets(addedItemCount);

    final int replacedItemCount = patchFile.getBuffer().readUleb128();
    final int[] replacedIndices = readDeltaIndiciesOrOffsets(replacedItemCount);

    final TableOfContents.Section tocSec = getTocSection(this.oldDex);
    Dex.Section oldSection = null;

    int oldItemCount = 0;
    if (tocSec.exists()) {
        oldSection = this.oldDex.openSection(tocSec);
        oldItemCount = tocSec.size;
    }

    // Now rest data are added and replaced items arranged in the order of
    // added indices and replaced indices.
    doFullPatch(
            oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices
    );
}

我們在寫入的時候現在都被讀取出來了,這里的算法和生成補丁的 DexDiff 是一個逆向的過程,每個區域的合并算法采用二路歸并,在 old dex 的基礎上對元素進行刪除,增加,替換操作。:

  1. del 操作的個數,每個 del 的 index,存儲在一個 int[] deletedIndices 中;

  2. add 操作的個數,每個 add 的 index,存儲在一個 int[] addedIndices 中;

  3. replace 操作的個數,每個需要replace的index,存儲在一個 int[] replacedIndices 中;

接下來獲取了 oldDex 中 oldItems 和 oldItemCount。然后帶著這些參數執行方法 doFullPatch(oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices):

private void doFullPatch(
        Dex.Section oldSection,
        int oldItemCount,
        int[] deletedIndices,
        int[] addedIndices,
        int[] replacedIndices
) {
    int deletedItemCount = deletedIndices.length;
    int addedItemCount = addedIndices.length;
    int replacedItemCount = replacedIndices.length;
    int newItemCount = oldItemCount + addedItemCount - deletedItemCount;

    int deletedItemCounter = 0;
    int addActionCursor = 0;
    int replaceActionCursor = 0;

    int oldIndex = 0;
    int patchedIndex = 0;
    while (oldIndex < oldItemCount || patchedIndex < newItemCount) {
        if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {
            /****************第1部分******************/
            T addedItem = nextItem(patchFile.getBuffer());
            int patchedOffset = writePatchedItem(addedItem);
            ++addActionCursor;
            ++patchedIndex;
        } else
        if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {
            /****************第2部分 省略N行代碼,和上一部分類似,后面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        } else
        if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {
            /****************第3部分(1) 省略N行代碼,和上一部分類似,后面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        } else
        if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {
            /****************第3部分(2) 省略N行代碼,和上一部分類似,后面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        } else
        if (oldIndex < oldItemCount) {
            /****************第4部分 省略N行代碼,和上一部分類似,后面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        }
    }

    if (addActionCursor != addedItemCount || deletedItemCounter != deletedItemCount
            || replaceActionCursor != replacedItemCount
    ) {
        throw new IllegalStateException(
              /*************..String。。。。。。。。/
                )
        );
    }
}

到此,生成 Dex 過程完成。
從源碼中可以看出我們是向位于 patchedDex 的 stringData 區寫數據,按照上面我們說的,應該要寫入新增的、替換的的數據,而我們寫入的過程:

首先計算出 newItemCount = oldItemCount + addCount - delCount,然后開始遍歷,遍歷條件為 0~oldItemCount 或 0~newItemCount。
而在 patchIndex 從 0~newItemCount 之間都會寫入對應的 Item。

Item 寫入通過代碼我們可以看到(第1、2、3(1)、3(2)、4部分),具體代碼如下:

  1. 首先判斷該 patchIndex 是否包含在 addIndices 中,如果包含則寫入:

    if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {

           T addedItem = nextItem(patchFile.getBuffer());
           int patchedOffset = writePatchedItem(addedItem);
           ++addActionCursor;
           ++patchedIndex;

    }

  2. 再者判斷是否在 repalceIndices 中,如果包含則寫入:

    if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {

    T replacedItem = nextItem(patchFile.getBuffer());
    int patchedOffset = writePatchedItem(replacedItem);
    ++replaceActionCursor;
    ++patchedIndex;

    }

  3. 然后判斷如果發現 oldIndex 被 delete 或者 replace,直接跳過:

    if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {

    T skippedOldItem = nextItem(oldSection); // skip old item.
    markDeletedIndexOrOffset(
            oldToPatchedIndexMap,
            oldIndex,
            getItemOffsetOrIndex(oldIndex, skippedOldItem)
    );
    ++oldIndex;
    ++deletedItemCounter;

    } else
    if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {

    T skippedOldItem = nextItem(oldSection); // skip old item.
    markDeletedIndexOrOffset(
            oldToPatchedIndexMap,
            oldIndex,
            getItemOffsetOrIndex(oldIndex, skippedOldItem)
    );
    ++oldIndex;

    }

  4. 最后一個 index 指的就是,oldIndex 為非 delete 和 replace 的,也就是和 newDex 中 items 相同的部分。

    if (oldIndex < oldItemCount) {

    T oldItem = adjustItem(this.oldToPatchedIndexMap, nextItem(oldSection));
    
    int patchedOffset = writePatchedItem(oldItem);
    
    updateIndexOrOffset(
            this.oldToPatchedIndexMap,
            oldIndex,
            getItemOffsetOrIndex(oldIndex, oldItem),
            patchedIndex,
            patchedOffset
    );
    
    ++oldIndex;
    ++patchedIndex;
    }
    

上述 1 2 4三個部分即可組成完整的 newDex 的該區域。完成了 stringData 區域的 patch 算法。
其他的 14 種算法的 execute 代碼是相同的(父抽象類),執行的操作類似,都會完成各個部分的 patch 算法。
當所有的區域都完成恢復后,那么剩下的就是 header 和 mapList 了,所以回到所有算法執行完成的地方,即 executeAndSaveTo(OutputStream out) 的第三部分:

executeAndSaveTo(os) 三部分之第三部分

public void executeAndSaveTo(OutputStream out) throws IOException {

        /************省略this.stringDataSectionPatchAlg.execute()前的代碼*********/
    this.stringDataSectionPatchAlg.execute();
    /******省略其他算法執行execute()******************/

    // Thirdly, write header, mapList. Calculate and write patched dex's sign and checksum.
    Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);
    patchedToc.writeHeader(headerOut);

    Dex.Section mapListOut = this.patchedDex.openSection(patchedToc.mapList.off);
    patchedToc.writeMap(mapListOut);

    this.patchedDex.writeHashes();

    // Finally, write patched dex to file.
    this.patchedDex.writeTo(out);
}

可以看到首先是定位到 header 區域,寫 header 相關數據;定位到 mapList 區域,編寫 mapList 相關數據。兩者都完成的時候,需要編寫 header 中比較特殊的兩個字段:簽名和 checkSum,因為這兩個字段是依賴 mapList 的,所以必須在編寫 mapList后。
這樣就完成了完整的dex的生成,最后將內存中的所有數據寫到文件中。

三、生成全量Dex后的加載過程

上述是完整 Dex 的生成過程,也是算法的核心所在,所以花了很長時間,下面就是我們生成完整 Dex 后的加載過程咯,這一部分主要是在這個包下:

tinker-loader

TinkerApplication 通過反射的方式將實際的 app 業務隔離,這樣可以在熱更新的時候修改實際的 app 內容。

在 TinkerApplication 中的 onBaseContextAttached 中會通過反射調用 TinkerLoader 的 tryLoad 加載已經合成的 dex。

 private static final String TINKER_LOADER_METHOD   = "tryLoad";
 private void loadTinker() {
    //disable tinker, not need to install
    if (tinkerFlags == TINKER_DISABLE) {
        return;
    }
    tinkerResultIntent = new Intent();
    try {
        //reflect tinker loader, because loaderClass may be define by user!
        Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());

        Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
        Constructor<?> constructor = tinkerLoadClass.getConstructor();
        tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
    } catch (Throwable e) {
        //has exception, put exception error code
        ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
        tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
    }
}

下面是反射調用的 TinkerLoader 中的 tryLoad 方法:

@Override
public Intent tryLoad(TinkerApplication app) {
    Intent resultIntent = new Intent();

    long begin = SystemClock.elapsedRealtime();
    tryLoadPatchFilesInternal(app, resultIntent);
    long cost = SystemClock.elapsedRealtime() - begin;
    ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
    return resultIntent;
}

其中 tryLoadPatchFilesInternal 是加載 Patch 文件的核心函數(代碼比較多,大家看注釋應該就可以明白每段是做什么的了):

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
    final int tinkerFlag = app.getTinkerFlags();

    if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
        //tinkerFlag是否開啟,否則不加載
        Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
        return;
    }
    //tinker
    File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
    if (patchDirectoryFile == null) {
        //tinker目錄是否生成,沒有則表示沒有生成全量的dex,不需要重新加載
        Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
        //treat as not exist
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
        return;
    }
    //tinker/patch.info
    File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);

    //check patch info file whether exist
    if (!patchInfoFile.exists()) {
        //tinker/patch.info是否存在,否則不加載
        Log.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
        return;
    }
    //old = 641e634c5b8f1649c75caf73794acbdf
    //new = 2c150d8560334966952678930ba67fa8
    File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);

    patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
    if (patchInfo == null) {
        //讀取patch.info,讀取失敗則不加載
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
        return;
    }

    String oldVersion = patchInfo.oldVersion;
    String newVersion = patchInfo.newVersion;
    String oatDex = patchInfo.oatDir;

    if (oldVersion == null || newVersion == null || oatDex == null) {
        //判斷版本號是否為空,為空則不加載
        //it is nice to clean patch
        Log.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
        return;
    }

    resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
    resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);

    //tinker/patch.info/patch-641e634c
    String patchVersionDirectory = patchDirectoryPath + "/" + patchName;

    File patchVersionDirectoryFile = new File(patchVersionDirectory);

    if (!patchVersionDirectoryFile.exists()) {
        //判斷patch version directory(//tinker/patch.info/patch-641e634c)是否存在
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
        return;
    }

    //tinker/patch.info/patch-641e634c/patch-641e634c.apk
    File patchVersionFile = new File(patchVersionDirectoryFile.getAbsolutePath(), SharePatchFileUtil.getPatchVersionFile(version));

    if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
        //判斷patchVersionDirectoryFile(//tinker/patch.info/patch-641e634c/patch-641e634c.apk)是否存在
        Log.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
        //we may delete patch info file
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
        return;
    }

    ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);

    int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
    if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
        //checkTinkerPackage,(如tinkerId和oldTinkerId不能相等,否則不加載)
        Log.w(TAG, "tryLoadPatchFiles:checkTinkerPackage");
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, returnCode);
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
        return;
    }

    if (isEnabledForDex) {
        //tinker/patch.info/patch-641e634c/dex
        boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
        if (!dexCheck) {
            //檢測dex的完整性,包括dex是否全部生產,是否對dex做了優化,優化后的文件是否存在(//tinker/patch.info/patch-641e634c/dex)
            //file not found, do not load patch
            Log.w(TAG, "tryLoadPatchFiles:dex check fail");
            return;
        }
    }
    /****省略對so res文件進行完整性檢測***************/
    final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
    /***************************************/
    //now we can load patch jar
    if (isEnabledForDex) {
        /********************劃重點---TinkerDexLoader.loadTinkerJars********************/
        boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);

        if (isSystemOTA) {
            // update fingerprint after load success
            patchInfo.fingerPrint = Build.FINGERPRINT;
            patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
            // reset to false
            oatModeChanged = false;

            if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                return;
            }
            // update oat dir
            resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
        }
        if (!loadTinkerJars) {
            Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
            return;
        }
    }

    return;
}

其中 TinkerDexLoader.loadTinkerJars 是用來處理加載 dex 文件。

public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {

 /*****省略部分代碼****************/
 PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
 /***********省略N行代碼,主要是生成一些合法文件列表,對dex文件進行優化**************/
 // 加載Dex
 SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);

}

然后 SystemClassLoaderAdder.installDexes 根據安卓的版本對dex進行安裝啦:

@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
    throws Throwable {
    Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

    if (!files.isEmpty()) {
        files = createSortedAdditionalPathEntries(files);
        ClassLoader classLoader = loader;
        if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
            classLoader = AndroidNClassLoader.inject(loader, application);
        }
        //because in dalvik, if inner class is not the same classloader with it wrapper class.
        //it won't fail at dex2opt
        if (Build.VERSION.SDK_INT >= 23) {
            V23.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 19) {
            V19.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 14) {
            V14.install(classLoader, files, dexOptDir);
        } else {
            V4.install(classLoader, files, dexOptDir);
        }
        //install done
        sPatchDexCount = files.size();
        Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

        if (!checkDexInstall(classLoader)) {
            //reset patch dex
            SystemClassLoaderAdder.uninstallPatchDex(classLoader);
            throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
        }
    }
}

前面我們講dex加載時說加載類一般使用的是 PathClassLoader 和 DexClassLoader,而 PathClassLoader 作為系統類和應用類的加載器。DexClassLoader 用來從.jar和.apk類型的文件內部加載classes.dex文件。

而 install 是怎么做的呢:

/**
 *Installer for platform versions 23.
 */
private static final class V23 {

    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                File optimizedDirectory)
        throws IllegalArgumentException, IllegalAccessException,
        NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
        /* The patched class loader is expected to be a descendant of
         *dalvik.system.BaseDexClassLoader. We modify its
         *dalvik.system.DexPathList pathList field to append additional DEX
         *file entries.
         */
        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
            suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makePathElement", e);
                throw e;
            }

        }
    }
    /*************省略makePathElements方法***************/
}

先獲取 BaseDexClassLoader 的 dexPathList 對象,然后通過 dexPathList 的 makeDexElements 方法將我們要安裝的 dex 轉化成 Element[] 對象,最后將其和 dexPathList 的 dexElements 對象進行合并,就是新的 Element[] 對象,因為我們添加的 dex 都被放在 dexElements 數組的最前面,所以當通過 findClass 來查找這個類時,就是使用的我們最新的 dex 里面的類。不同版本里面的 DexPathList 等類的函數和字段都有一些變化,其他類似。

到此為止,dex的整個加載過程就結束了!

其他使用 Tinker 進行更新的,如 so 庫的更新、library 的更新大家可以在源碼中按照上面的 dex 加載過程看到。

熱更新方案的對比

好了,上面我們也說了幾種熱更新的方案了,其他的熱更新方案大家可以去搜索了解。

上面阿里給出了AndFix和HotFix以及Sophix的對比,現在我們就對時下的幾種熱更新方案進行對比,看看到底哪種好:

方案對比1

從對比中我們也能發現Sophix和Tinker作為兩大巨頭的最新熱更新方案,都是比較厲害的,大家如果有需要的話可以去嘗試下。

因為時間關系,實現自己的熱更新方案還沒有寫完,暫時不放上來了,等我寫完了會放上下一篇的鏈接的。謝謝大家的捧場支持!

本文參加第三期安卓巴士博文比賽:不做將死之蛙 安卓巴士博文大賽第三期為你加溫!

題外話

馬上公司的實習就要結束了,感覺時間過的飛快,自己也要好好準備找工作的事情了。巧的是 @權小陽 舉辦了三屆博文比賽,剛好我實習也是在這三個月,每個月一篇博文也讓我學到很多東西,我在這里要給Sunny點個 N 個贊,做事很用心,很認真,舉辦活動細節考慮的很到位,也很愛幫助別人!聲音也很好聽,人也很漂亮!嗯,就說這么多吧(捂臉)。

當然也認識了很多大神博主,@nanchen,@玖哥,@雞排,@靜心,@凱迪,,,等等。其他的就不一一艾特啦(貌似這么@是沒用的),讀大家的文章也讓我學到了很多東西!

大家雖然都不認識,但全世界那么多人,我們能聚在巴士這個地方,一起學習成長(當然,還有那一起哈不完的牛!此處奸笑beginning,不得不吐槽下,為啥markdown不能添加表情(此處淚奔,,,)),也是一種莫大的緣分吧,希望大家以后的路都能越走越寬,越走越遠!大家加油!

大家加油!

分享到:
我來說兩句
facelist
您需要登錄后才可以評論 登錄 | 立即注冊
所有評論(16)
AnyOner 2017-8-19 19:11
   怎么這次的這么短?   
回復
zeki_10 2017-8-19 21:50
AnyOner:    怎么這次的這么短?    
巴士的博文發表有問題,等周一他們上班調好了我就把剩下的加上!
回復
權小陽 2017-8-21 11:01
一級棒
回復
靜心Study 2017-8-21 14:12
666 找個好工作 妥妥的沒毛病~!加油~
回復
權小陽 2017-8-21 14:31
靜心Study: 666 找個好工作 妥妥的沒毛病~!加油~
你也666
回復
飛仙2016 2017-8-21 14:55
猿猿 我關注你很久了  博文贊
回復
音樂 2017-8-21 19:32
博主的文章條理很清楚!學習學習
回復
zeki_10 2017-8-21 21:42
飛仙2016: 猿猿 我關注你很久了  博文贊
   謝謝!
回復
zeki_10 2017-8-21 21:42
權小陽: 一級棒
   嘿嘿
回復
zeki_10 2017-8-21 21:43
靜心Study: 666 找個好工作 妥妥的沒毛病~!加油~
嗯嗯!一起加油
回復
zeki_10 2017-8-21 21:43
音樂: 博主的文章條理很清楚!學習學習
   謝謝!
回復
靜心Study 2017-8-22 15:22
MrLee-2: 嗯嗯!一起加油
把我要寫的寫了 搞的我都不知道寫不寫了 最后看了n次你的文章 覺得很不錯 可以考慮結合下  
回復
urnotxx 2017-9-16 16:46
  
回復
LFJJY 2017-9-30 13:42
小伙子,工作不用擔心的,你先想想怎么挑吧
回復
chenyangyan 2018-2-22 22:36
熱更新技術真的是越來越多了
回復
just-doit 2018-2-25 16:43
膜拜大神!
回復

領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

两码中特期期