Android熱更新技術(shù)探索

熱更新相關(guān)概念
  • 組件化---就是將一個app分成多個模塊,每個模塊都是一個組件(Module),開發(fā)的過程中我們可以讓這些組件相互依賴或者單獨(dú)調(diào)試部分組件等,但是最終發(fā)布的時候是將這些組件合并統(tǒng)一成一個apk,這就是組件化開發(fā)。我之前的開發(fā)方式基本上都是這一種。具體可以參考Android組件化方案
  • 插件化---將整個app拆分成很多模塊,這些模塊包括一個宿主多個插件,每個模塊都是一個apk(組件化的每個模塊是個lib),最終打包的時候?qū)⑺拗鱝pk和插件apk分開或者聯(lián)合打包。開發(fā)中,往往會堆積很多的需求進(jìn)項目,超過 65535 后,插件化就是一個解決方案。

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


組件化和插件化
  • 熱更新 --- 更新的類或者插件粒度較小的時候,我們會稱之為熱修復(fù),一般用于修復(fù)bug?。”热绺乱粋€bug方法或者緊急修改lib包,甚至一個類等。2016 Google 的 Android Studio 推出了Instant Run 功能 同時提出了3個名詞;

    • “熱部署” – 方法內(nèi)的簡單修改,無需重啟app和Activity。
    • “暖部署” – app無需重啟,但是activity需要重啟,比如資源的修改。
    • “冷部署” – app需要重啟,比如繼承關(guān)系的改變或方法的簽名變化等。
  • 增量更新---與熱更新區(qū)別最大的一個,其實這個大家應(yīng)該很好理解,安卓上的有些很大的應(yīng)用,特別是游戲,大則好幾個G的多如牛毛,但是每次更新的時候卻不是要去下載最新版,而只是下載一個幾十兆的增量包就可以完成更新了,而這所使用的技術(shù)就是增量更新了。實現(xiàn)的過程大概是這個樣子的:我們手機(jī)上安裝著某個大應(yīng)用,下載增量包之后,手機(jī)上的apk和增量包合并形成新的包,然后會再次安裝,這個安裝過程可能是可見的,或者應(yīng)用本身有足夠的權(quán)限直接在后臺安裝完成。

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


Studio增量更新

而熱更新究竟是什么呢?

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

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

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

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

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

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

熱更新方案介紹

熱更新方案發(fā)展至今,有很多團(tuán)隊開發(fā)過不同的解決方案,包括Dexposed、AndFix,(HotFix)Sophix,Qzone超級補(bǔ)丁的類Nuwa方式,微信的Tinker, 大眾點評的nuwa、百度金融的rocooFix, 餓了么的amigo以及美團(tuán)的robust、騰訊的Bugly熱更新。

蘋果公司現(xiàn)在已經(jīng)禁止了熱更新,不過估計也組織不了開發(fā)者們的熱情吧!

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

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

Dexposed (阿里熱更新方案一)

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

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

Dexposed 中的 AOP 原理來自于 Xposed。在 Dalvik 虛擬機(jī)下,主要是通過改變一個方法對象方法在 Dalvik 虛擬機(jī)中的定義來實現(xiàn),具體做法就是將該方法的類型改變?yōu)?native 并且將這個方法的實現(xiàn)鏈接到一個通用的 Native Dispatch 方法上。這個 Dispatch 方法通過 JNI 回調(diào)到 Java 端的一個統(tǒng)一處理方法,最后在統(tǒng)一處理方法中調(diào)用 before , after 函數(shù)來實現(xiàn)AOP。在 Art 虛擬機(jī)上目前也是是通過改變一個 ArtMethod 的入口函數(shù)來實現(xiàn)。

Dexposed

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

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

  • 1.引入一個名為 patchloader 的 jar 包,這個函數(shù)庫實現(xiàn)了一個熱更新框架,宿主 apk (可能含有 bug 的上線版本)在發(fā)布時會將這個 jar 包一起打包進(jìn) apk 中;
  • 2.補(bǔ)丁 apk (已修復(fù)線上版本 bug 的版本)只是在編譯時需要這個 jar 包,但打包成 apk 時不包含這個 jar 包,以免補(bǔ)丁 apk 集成到宿主 apk 中時發(fā)生沖突;
  • 3.補(bǔ)丁 apk 將會以 provided 的形式依賴 dexposedbridge.jar 和 patchloader.jar;
  • 4.通過在線下載的方式從服務(wù)器下載補(bǔ)丁 apk ,補(bǔ)丁 apk 集成到宿主 apk 中,使用補(bǔ)丁 apk 中的函數(shù)替換原來的函數(shù),從而實現(xiàn)在線修復(fù) bug 的功能。

AndFix (阿里熱更新方案二)

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

AndFix 更新實現(xiàn)過程:


AndFix 更新實現(xiàn)過程

1.首先添加依賴

compile 'com.alipay.euler:andfix:0.3.1@aar'

2.然后在Application.onCreate()中添加以下代碼:

patchManager = new PatchManager(context);
patchManager.init(appversion); //current version
patchManager.loadPatch();

3.可以用這句話獲取 appversion,每次 appversion 變更都會導(dǎo)致所有補(bǔ)丁被刪除,如果 appversion 沒有改變,則會加載已經(jīng)保存的所有補(bǔ)丁。

String appversion= getPackageManager().getPackageInfo(getPackageName(), 0).versionName;

4.然后在需要的地方調(diào)用 PatchManager 的 addPatch 方法加載新補(bǔ)丁,比如可以在下載補(bǔ)丁文件之后調(diào)用。

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

6.通過網(wǎng)絡(luò)傳輸或者 adb push 的方式將 apatch 文件傳到手機(jī)上,然后運(yùn)行到 addPatch 的時候就會加載補(bǔ)丁。

AndFix 更新的原理:

1.首先通過虛擬機(jī)的 JarFile 加載補(bǔ)丁文件,然后讀取 PATCH.MF 文件得到補(bǔ)丁類的名稱
2.使用 DexFile 讀取 patch 文件中的 dex 文件,得到后根據(jù)注解來獲取補(bǔ)丁方法,然后根據(jù)注解中得到雷鳴和方法名,使用 classLoader 獲取到 Class,然后根據(jù)反射得到 bug 方法。
3.jni 層使用 C++ 的指針替換 bug 方法對象的屬性來修復(fù) bug。

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

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

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

public AndFixManager(Context context) {
    mContext = context;
    mSupport = Compat.isSupport();//判斷Android機(jī)型是否適支持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);//存儲關(guān)于patch文件的信息
    //根據(jù)你傳入的版本號和之前的對比,做不同的處理
    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文件加載到內(nèi)存
    }
}/*************省略初始化、刪除、加載具體方法實現(xiàn)*****************/

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

那么 patch 補(bǔ)丁文件是如何加載的呢?其實 patch 補(bǔ)丁文件本質(zhì)上是一個 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對應(yīng)的值加入到集合中,對應(yīng)的值就是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對應(yīng)的class類的集合List
            mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
                    classes);//修復(fù)bug方法
        }
    }
}

循環(huán)獲取補(bǔ)丁對應(yīng)的 class 類來修復(fù) bug 方法,mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),classes):
篇幅所限,代碼可點擊左下角“閱讀原文”查看。

從上面的 bug 修復(fù)源碼可以看出,就是在找補(bǔ)丁包中有 @MethodReplace 注解的方法,然后反射獲取原 apk 中方法的位置,最后進(jìn)行替換。
而最后調(diào)用的 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 虛擬機(jī),而是 Art 虛擬機(jī),所以需要對不同的手機(jī)系統(tǒng)做不同的處理。
首先看 Dalvik 替換方法的實現(xiàn):

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的屬性設(shè)置成Native方法

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

    meth->nativeFunc = dalvik_dispatcher;//把方法的實現(xiàn)替換成native方法
}

Art 替換方法的實現(xiàn):

//不同的art系統(tǒng)版本不同處理也不同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);    
    //把一些參數(shù)的指針給補(bǔ)丁方法
    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_;//把補(bǔ)丁方法替換掉
    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 層的內(nèi)部函數(shù),得到 ClassObject 對象
2.修改訪問權(quán)限的屬性為 public
3.得到新舊方法的指針,新方法指向目標(biāo)方法,實現(xiàn)方法的替換。

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

最近我在學(xué)習(xí) C++,頓時感覺到還是這種可以控制底層的語言是多么強(qiáng)大,不過安卓可以通過 JNI 調(diào)用 C++,也就沒什么可吐槽的了!

好的,現(xiàn)在 AndFix 我們分析了一遍它的實現(xiàn)過程和原理,其優(yōu)點是不需要重啟即可應(yīng)用補(bǔ)丁,遺憾的是它還是有不少缺陷的,這直接導(dǎo)致阿里再次拋棄了它,缺陷如下:

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

2.不支持 YunOS
3.無法添加新類和新的字段
4.需要使用加固前的 apk 制作補(bǔ)丁,但是補(bǔ)丁文件很容易被反編譯,也就是修改過的類源碼容易泄露。
5.使用加固平臺可能會使熱補(bǔ)丁功能失效(看到有人在 360 加固提了這個問題,自己還未驗證)。

Sophix---阿里終極熱修復(fù)方案

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

方案對比

巴巴再次證明我是最強(qiáng)的,誰都沒我厲害?。?!因為我啥都支持,而且沒缺點。。簡直就是無懈可擊!
那么我們就來項目集成下看看具體的使用效果吧!具體就拿支持的方法級替換來演示吧!
先去創(chuàng)建個應(yīng)用:

創(chuàng)建Sophix應(yīng)用

獲取 AppId:24582808-1
AppSecret:da283640306b464ff68ce3b13e036a6e 以及 RSA 密鑰**。三個參數(shù)配置在 application 節(jié)點下面:

    <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 坐標(biāo)版本依賴:

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

項目結(jié)構(gòu)也很簡單:


項目結(jié)構(gòu)

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);
            }
        });
      }
}

其實就是有一個文本框顯示當(dāng)前版本,還有一個按鈕用來跳轉(zhuǎn)到 SecondActivity
而SecondActivity的內(nèi)容:

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, "彈出框內(nèi)容彈出錯啦!", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

也很簡單,只有一個按鈕,按鈕點擊之后彈出一個 Toast 顯示“彈出框內(nèi)容彈出錯啦!”
就這樣,我們的一個上線 app 完成了(粗糙是粗糙了點),下面來看下效果吧(請諒解我第一次錄屏的渣渣技術(shù),以后會做的越來越好)


bug效果

然后我們的用戶開始用了,發(fā)現(xiàn)一個bug!“彈出框彈出的內(nèi)容是錯誤的!”,用戶可不管別的,馬上給我改好?。?/p>

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

如果沒有熱更新,可能就要搞個臨時版本或者甚至發(fā)布一個新版本,但是現(xiàn)在我們有了 Sophix ,就不需要這么麻煩了。
首先我們?nèi)ハ螺d補(bǔ)丁打包工具(不得不說,工具確實比較粗糙(丑)。。。)


阿里補(bǔ)丁工具

舊包:<必填> 選擇基線包路徑(有問題的 APK)。
新包:<必填> 選擇新包路徑(修復(fù)過該問題 APK)。
日志:打開日志輸出窗口。
高級:展開高級選項
設(shè)置:配置其他信息。
GO!:開始生成補(bǔ)丁。

所以首先我們把舊包和新包添加上之后,配置好之后看看會發(fā)生什么吧!
強(qiáng)制冷啟動是補(bǔ)丁打完后重啟才生效。


配置

正在生成補(bǔ)丁
補(bǔ)丁生成成功

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

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


生成的補(bǔ)丁文件

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


補(bǔ)丁上傳

補(bǔ)丁發(fā)布

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

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

調(diào)試工具

然后有兩種方式可以加載補(bǔ)丁包,一種是掃二維碼,還有一種是加載本地補(bǔ)丁jar包,模擬器上實在不好操作啊?。。∽詈笪仪?,借了同學(xué)的手機(jī)掃二維碼加載補(bǔ)丁包了。。。然后就會有 log 提示


調(diào)試工具加載補(bǔ)丁包

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


更新版本
更新Toast

當(dāng)然了,目前我們還是在調(diào)試工具上加載的補(bǔ)丁包,我們接下來將補(bǔ)丁包發(fā)布后就可以不用調(diào)試工具,直接打開 app 就可以實現(xiàn)打補(bǔ)丁了,這樣就完成了 bug 的修復(fù)!
其實這么看起來確實是非常簡單就實現(xiàn)了熱修復(fù),主要我們的生成補(bǔ)丁工作都是交給阿里提供的工具實現(xiàn)了,其實我們也能看得出來,Sophix 和前面介紹的 AndFix 很像,不同的地方是補(bǔ)丁文件已經(jīng)給出工具可以一鍵生成了,而且支持的東西更多了。其他比如 so 庫和 library 以及資源文件的更新大家可以查看官方文檔了解。

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

所以阿里爸爸一直在進(jìn)步著呢,知道技術(shù)存在問題就要去解決問題,這不,從Dexposed-->AndFix-->HotFix-->Sophix,技術(shù)是越來越成熟了。
下面介紹另外一個大廠的幾種熱更新方案

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

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


方案對比1

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

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

篇幅所限,所載部分只占全文的三分之一,省略作者對于Qzone超級補(bǔ)丁 & 微信Tinker 騰訊熱更新方案的詳細(xì)對比,可以點擊左下角“閱讀原文”查看全部,大量內(nèi)容不容錯過。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容