Android熱更新技術探索

熱更新相關概念
  • 組件化---就是將一個app分成多個模塊哥纫,每個模塊都是一個組件(Module),開發(fā)的過程中我們可以讓這些組件相互依賴或者單獨調試部分組件等召川,但是最終發(fā)布的時候是將這些組件合并統(tǒng)一成一個apk南缓,這就是組件化開發(fā)。我之前的開發(fā)方式基本上都是這一種荧呐。具體可以參考Android組件化方案
  • 插件化---將整個app拆分成很多模塊汉形,這些模塊包括一個宿主多個插件,每個模塊都是一個apk(組件化的每個模塊是個lib)倍阐,最終打包的時候將宿主apk和插件apk分開或者聯(lián)合打包概疆。開發(fā)中臀突,往往會堆積很多的需求進項目挽放,超過 65535 后诀黍,插件化就是一個解決方案聚谁。

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


組件化和插件化
  • 熱更新 --- 更新的類或者插件粒度較小的時候猫缭,我們會稱之為熱修復病往,一般用于修復bug;巳鞠柄!比如更新一個bug方法或者緊急修改lib包童漩,甚至一個類等。2016 Google 的 Android Studio 推出了Instant Run 功能 同時提出了3個名詞春锋;

    • “熱部署” – 方法內的簡單修改,無需重啟app和Activity差凹。
    • “暖部署” – app無需重啟期奔,但是activity需要重啟,比如資源的修改危尿。
    • “冷部署” – app需要重啟呐萌,比如繼承關系的改變或方法的簽名變化等。
  • 增量更新---與熱更新區(qū)別最大的一個谊娇,其實這個大家應該很好理解肺孤,安卓上的有些很大的應用,特別是游戲济欢,大則好幾個G的多如牛毛,但是每次更新的時候卻不是要去下載最新版法褥,而只是下載一個幾十兆的增量包就可以完成更新了揍愁,而這所使用的技術就是增量更新了是钥。實現(xiàn)的過程大概是這個樣子的:我們手機上安裝著某個大應用鸥鹉,下載增量包之后蛮穿,手機上的apk和增量包合并形成新的包,然后會再次安裝毁渗,這個安裝過程可能是可見的践磅,或者應用本身有足夠的權限直接在后臺安裝完成。

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


Studio增量更新

而熱更新究竟是什么呢檐春?

有一些這樣的情況, 當一個App發(fā)布之后儡嘶,突然發(fā)現(xiàn)了一個嚴重bug需要進行緊急修復喇聊,這時候公司各方就會忙得焦頭爛額:重新打包App、測試蹦狂、向各個應用市場和渠道換包誓篱、提示用戶升級朋贬、用戶下載、覆蓋安裝窜骄。有時候僅僅是為了修改了一行代碼锦募,也要付出巨大的成本進行換包和重新發(fā)布。老是發(fā)布版本用戶會瘋掉的A诙簟?纺丁!(好吧 猿猿們也會瘋掉准验。赎线。)

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

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

從某種意義上來說室梅,熱更新就是要做一件事戏仓,替換。當替換的東西屬于大塊內容的時候亡鼠,就是模塊化了柜去,當你去替換方法的時候,叫熱更新拆宛,當你替換類的時候,加熱插件讼撒,而且重某種意義上講浑厚,所有的熱更新方案,都是一種熱插件根盒,因為熱更新方案就是在app之外去干這個事钳幅。就這么簡單的理解。無論是替換一個類炎滞,還是一個方法敢艰,都是在干替換這件事請。册赛。這里的替換钠导,也算是幾種hook操作震嫉,無論在什么代碼等級上,都是一種侵入性的操作牡属。

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

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

熱更新方案介紹

熱更新方案發(fā)展至今特纤,有很多團隊開發(fā)過不同的解決方案,包括Dexposed侥加、AndFix捧存,(HotFix)Sophix,Qzone超級補丁的類Nuwa方式官硝,微信的Tinker, 大眾點評的nuwa矗蕊、百度金融的rocooFix, 餓了么的amigo以及美團的robust、騰訊的Bugly熱更新氢架。

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

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

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

Dexposed (阿里熱更新方案一)

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

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

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

Dexposed

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

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

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

AndFix (阿里熱更新方案二)

AndFix 是一個 Android App 的在線熱補丁框架随闺。使用此框架,我們能夠在不重復發(fā)版的情況下蔓腐,在線修改 App 中的 Bug 矩乐。AndFix 就是 “Android Hot-Fix”的縮寫。支持 Android 2.3到6.0版本,并且支持 arm 與 X86 系統(tǒng)架構的設備散罕。完美支持 Dalvik 與 ART 的 Runtime分歇。AndFix 的補丁文件是以 .apatch 結尾的文件。它從你的服務器分發(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 變更都會導致所有補丁被刪除,如果 appversion 沒有改變职抡,則會加載已經保存的所有補丁。

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

4.然后在需要的地方調用 PatchManager 的 addPatch 方法加載新補丁误甚,比如可以在下載補丁文件之后調用缚甩。

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

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

AndFix 更新的原理:

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

具體的實現(xiàn)主要都是我們在 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類的集合,此類適合大并發(fā)
    mLoaders = new ConcurrentHashMap<String, ClassLoader>();//初始化存放類對應的類加載器集合
}

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

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文件的信息
    //根據(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文件加載到內存
    }
}/*************省略初始化鳖藕、刪除、加載具體方法實現(xiàn)*****************/

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方法
        }
    }
}

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

從上面的 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 虛擬機闷愤,所以需要對不同的手機系統(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的屬性設置成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ù)的指針給補丁方法
    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 層的內部函數(shù)讥脐,得到 ClassObject 對象
2.修改訪問權限的屬性為 public
3.得到新舊方法的指針遭居,新方法指向目標方法,實現(xiàn)方法的替換旬渠。

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

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

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

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

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

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

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

方案對比

巴巴再次證明我是最強的叭披,誰都沒我厲害!M婊洹涩蜘!因為我啥都支持,而且沒缺點熏纯。同诫。簡直就是無懈可擊!
那么我們就來項目集成下看看具體的使用效果吧樟澜!具體就拿支持的方法級替換來演示吧误窖!
先去創(chuàng)建個應用:

創(chuàng)建Sophix應用

獲取 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 坐標版本依賴:

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效果

然后我們的用戶開始用了,發(fā)現(xiàn)一個bug溪厘!“彈出框彈出的內容是錯誤的胡本!”,用戶可不管別的桩匪,馬上給我改好按虮瘛!

此時的開發(fā)er估計心頭千萬頭草泥馬在奔騰了傻昙,求神拜佛上線不要出問題闺骚,剛上線就出問題了,“where is my 測試erW钡怠F!”不說了贾惦,趕緊修吧胸梆,最暴力的方法就是 SecondActivity 的 Toast 中彈出“彈出框內容彈正常啦敦捧!”一句代碼搞定!bingo碰镜!

如果沒有熱更新兢卵,可能就要搞個臨時版本或者甚至發(fā)布一個新版本,但是現(xiàn)在我們有了 Sophix ,就不需要這么麻煩了绪颖。
首先我們去下載補丁打包工具(不得不說秽荤,工具確實比較粗糙(丑)。柠横。窃款。)


阿里補丁工具

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

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


配置

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

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

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


生成的補丁文件

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


補丁上傳

補丁發(fā)布

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

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

調試工具

然后有兩種方式可以加載補丁包,一種是掃二維碼楷力,還有一種是加載本地補丁jar包喊式,模擬器上實在不好操作胺趸А!2砹簟夏哭!最后我屈服了,借了同學的手機掃二維碼加載補丁包了献联。方庭。。然后就會有 log 提示


調試工具加載補丁包

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


更新版本
更新Toast

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

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

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

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

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


方案對比1

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

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末斜友,一起剝皮案震驚了整個濱河市炸裆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鲜屏,老刑警劉巖烹看,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異洛史,居然都是意外死亡惯殊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門也殖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來土思,“玉大人,你說我怎么就攤上這事忆嗜±四” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵霎褐,是天一觀的道長址愿。 經常有香客問我,道長冻璃,這世上最難降的妖魔是什么响谓? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮省艳,結果婚禮上娘纷,老公的妹妹穿的比我還像新娘。我一直安慰自己跋炕,他們只是感情好赖晶,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般遏插。 火紅的嫁衣襯著肌膚如雪捂贿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天胳嘲,我揣著相機與錄音厂僧,去河邊找鬼。 笑死了牛,一個胖子當著我的面吹牛颜屠,可吹牛的內容都是我干的。 我是一名探鬼主播鹰祸,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼为迈,長吁一口氣:“原來是場噩夢啊……” “哼莫杈!你這毒婦竟也來了付燥?” 一聲冷哼從身側響起还栓,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敬锐,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呆瞻,經...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡台夺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了痴脾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颤介。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赞赖,靈堂內的尸體忽然破棺而出滚朵,到底是詐尸還是另有隱情,我是刑警寧澤前域,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布辕近,位于F島的核電站,受9級特大地震影響匿垄,放射性物質發(fā)生泄漏移宅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一椿疗、第九天 我趴在偏房一處隱蔽的房頂上張望漏峰。 院中可真熱鬧,春花似錦届榄、人聲如沸浅乔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽靖苇。三九已至席噩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間顾复,已是汗流浹背班挖。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芯砸,地道東北人萧芙。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像假丧,于是被迫代替她去往敵國和親双揪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內容