微信Tinker 熱修復介紹及接入(一)

聲明:本文爲博主原創文章,轉載請註明出處:小嵩的博客

本系列傳送門:
微信Tinker 熱修復介紹及接入(一)
Tinker 原理深入理解(二)
Tinker 合併及加載補丁過程源碼分析 (三)

什麼是熱修復?

定義 : 熱修復(HotFix)是以補丁的方式動態修復緊急Bug,不再需要重新發布App,不需要用戶重新下載覆蓋安裝的方式來實現代碼的替換修改。這裏就不多囉嗦了,可以自行搜索網上的介紹。

目前主流HotFix方案對比

HotFix方案 Tinker QZone AndFix Robust
類替換 yes yes no no
So替換 yes no no no
資源替換 yes yes no no
全平臺支持 yes yes no yes
即時生效 no no yes yes
性能損耗 較小 較大 較小 較小
補丁包大小 較小 較大 一般 一般
開發透明 yes yes no no
複雜度 較低 較低 複雜 複雜
Rom體積 Dalvik較大 較小 較小 較小
成功率 較高(95%) 較高(96%) 一般 最高(99.9%)

注:

  • Tinker的成功率數據,是從微信團隊張紹文同學那兒打聽得到的,該數據是微信APP自身的成功率,可信度高;
  • Robust的成功率數據,來自美團Robust開源項目官方文檔。
  • QZone成功率和Tinker在同一水平的樣子。
  • AndFix 是公司以前就接入的,內部測試成功率只有80%左右(僅供參
    考),而且修復起來還有諸多限制。

Tinker的原理

微信Tinker原理圖 

Tinker流程圖

Tinker的優勢和特性  

  綜合考慮來說,Tinker的補丁包以及功能全面性、穩定性是比較吸引人的,並且功能還能做到類替換 、資源替換以及So替換。這樣一來它就不僅僅是熱修復了,還能做到熱更新。因此我們最後採用了Tinker (其實還是因爲微信幾億設備也是用的Tinker這套方案,靠譜點)。

 微信和阿里還提供了補丁後臺託管,版本管理SDK ,不缺錢或者不想因爲熱修復對項目代碼造成侵入性的話,也可以直接使用微信或阿里封裝好的傻瓜式接入方案,微信 Tinker Patch 方案目前是補丁包日請求量1w以內免費;阿里雲 Sophix 目前還在公測階段,暫時不收費

微信 Tinker Patch 官方地址:Tinker Patch
阿里 SopHix 官方地址:Sophix

接入Tinker步驟

基於1.7.11版本,最新版可參考 Tinker GitHub 項目主頁。

1.添加工程gradle plugin依賴

在項目的build.gradle中,添加tinker-patch-gradle-plugin的依賴

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.11')
    }
}

2.添加tinker庫依賴及插件應用

  在app的gradle文件app/build.gradle,我們需要添加tinker的庫依賴以及apply tinker的gradle插件:

//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
...
...
dependencies {
    //可選,用於生成application類 
    provided('com.tencent.tinker:tinker-android-anno:1.7.11')
    //tinker的核心庫
    compile('com.tencent.tinker:tinker-android-lib:1.7.11') 
}

3.gradle配置Tinker的一些參數

這步可參考Tinker 開源項目 sample中的app/build.gradle。

4.自定義Application代理類

  程序啓動時會加載默認的Application類,這導致我們補丁包是無法對它做修改了。如何規避?在這裏我們並沒有使用類似InstantRun hook Application的方式,而是通過代碼框架的方式來避免,這也是爲了儘量少的去反射,提升框架的兼容性。

  這裏我們要實現的是完全將原來的Application類隔離起來,即其他任何類都不能再引用我們自己的Application。將代碼都放到代理類ApplicationLike中來,我們需要做的其實是以下幾個工作:

  • 將我們項目原來的Application類以及它的Base類的所有代碼拷貝到創建的ApplicationLike繼承類中,例如SampleApplicationLike。你也可以直接將自己的Application改爲繼承ApplicationLike,然後做改動;
  • Application的attachBaseContext方法實現要單獨移動到onBaseContextAttached中;
  • 對ApplicationLike中,引用application的地方改成getApplication();
  • 對其他引用Application或者它的靜態對象方法的地方,改成引用ApplicationLike的靜態對象與方法;

更詳細的內容大家可以參考sample例子裏SampleApplicationLike的做法。
GitHub地址: tinker/tinker-sample-android/app/build.gradle

對於爲何放棄Instant Run 實現,而採用代理的方案,張紹文同學是這麼解釋的:

Tinker張紹文博客截圖

詳情可參考微信Android團隊技術分享博客,地址鏈接:WeMobileDev/article

5.Tinker SDK初始化以及調用

初始化

創建一個類繼承自ApplicationLike ,並添加DefaultLifeCycle註解,指定需要自動生成的Application路徑和名稱,將AndroidManifest.xml裏面的application名稱設置爲它 :

 <application
        android:name=".app.SampleApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">

代理類SampleApplicationLike 代碼:

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends ApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);

        SampleApplicationContext.application = getApplication();
        SampleApplicationContext.context = getApplication();
        TinkerManager.setTinkerApplicationLike(this);

        TinkerManager.initFastCrashProtect();
        //should set before tinker is installed
        TinkerManager.setUpgradeRetryEnable(true);

        //optional set logIml, or you can use default debug log
        TinkerInstaller.setLogIml(new MyLogImp());

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerManager.installTinker(this);

        Tinker.with(getApplication());//初始化熱更新SDK
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

}

寫好之後Sync一下,它會在編譯時自動生成SampleApplication。如果不想通過註解自動生成,我們也可以手動寫這個Application放到項目裏,但構造方法需要設置好代理類的path:

package tinker.sample.android.app;

import com.tencent.tinker.loader.app.TinkerApplication;

public class SampleApplication extends TinkerApplication {

    public SampleApplication() {
        super(7, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

}

調用Tinker合併與清除補丁:

loadPatch :

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");  

loadLibrary :

    // #method 1, hack classloader library path
                TinkerLoadLibrary.installNavitveLibraryABI(getApplicationContext(), "armeabi");
                System.loadLibrary("stlport_shared");

                // #method 2, for lib/armeabi, just use TinkerInstaller.loadLibrary
//                TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "stlport_shared");

                // #method 3, load tinker patch library directly
//                TinkerInstaller.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "stlport_shared");

cleanPatch:

Tinker.with(getApplicationContext()).cleanPatch();

6.補丁包生成與安裝

6.1 打開右上側Gradle,並雙擊assembleDebug,生成基準包。

assembleDebug

6.2 安裝基準包

app/build/bakApk 下,可以看到生成了基準包Apk以及R文件、mapping(mapping文件混淆下纔會有),然後將該Apk安裝到手機中。

平時開發測試時我們可通過AS 開發工具下方的Terminal 窗口 輸入如下命令將APK Push到手機:

//APK已安裝情況
adb install -r app/build/bakApk/app-debug-0620-14-12-54.apk 
//APK未安裝
adb install app/build/bakApk/app-debug-0620-14-12-54.apk 

然後將app/build/bakApk 下生成的文件路徑填入gradle 的ext 中

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-debug-0620-14-12-54.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0620-14-12-54-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

6.3 生成補丁包

oldApk路徑填好之後,開始修改Bug,bug改完之後,雙擊tinkerPatchDebug,這個gradle命令會對當前代碼和oldApk進行差異對比,在app/build/output/tinkerPatch下生成補丁。

這裏寫圖片描述

生成的補丁信息,我們需要的補丁包是patch_signed_7zip.apk:

這裏寫圖片描述

6.4 補丁包下載安裝

補丁包生成之後,我們則可把它放到服務器後臺,客戶端通過接口去下載補丁包了,測試中我們一樣是通過adb 將文件push到手機sd卡根目錄:

adb push ./aipai/build/outputs/tinkerPatch/offical/debug/patch_signed_7zip.apk /storage/sdcard0/

補丁包push到手機之後,我們在基準包代碼中已經寫了如下代碼,此時返回基準包觸發該代碼,則可把補丁包合併到基準包實現熱更新:

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk"); 

爬坑及小技巧:

1.TinkerId 設置問題。

git項目中會有TinkerId,如果是通過非Clone方式拉取的代碼,則需要push一次同步到Git中纔會有,如果爲了測試方便,也可以直接在 gtadle.properties文件指定tinkerId,如:TINKER_ID = 1

2.Java1.8 兼容問題

在gradle中設置 JavaVersion 爲1.8,導致Application代理失敗造成一啓動就崩潰問題,有兩種辦法:

  • 去除gradle tinker-android-anno 依賴庫,不通過DefaultLifeCycle註解自動生成Application的辦法,採用直接手動創建Application,並在構造方法中(第二個參數),設置代理類。
  • anno 註解不支持 jackOptions 因此需要通過添加 lambda插件來兼容Java1.8
    //添加插件
    apply plugin: ‘me.tatarka.retrolambda’

3.補丁包push到sd卡:

adb push ./app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/

4.安裝apk:

adb install app/build/bakApk/app-debug-0620-14-12-54.apk

adb install -r app/build/bakApk/app-debug-0620-14-12-54.apk

5.多渠道打包:

通過flavor 生成渠道包的情況下,會因爲BuildInfo不同而導致Apk的Dex文件不同,從而導致每個渠道的補丁包都需要一對一,那麼假如有幾十個渠道,則同樣需要幾十個渠道的補丁包,這是非常不合理的。那麼怎麼辦呢?

解決方案:
1.將渠道信息寫在AndroidManifest.xml或文件中,例如channel.ini;
2.將渠道信息寫在apk文件的zip comment中,這樣一來,所有渠道包的Dex文件都是相同的,我們就可以通過assembleRelease 生成的基準包,來打補丁包。所有渠道都可以共用這個補丁包。至於這種渠道打包方式的工具,可以使用GitHub上開源的 packer-ng-plugin 或者可使用美團點評使用了V2 Scheme簽名的 walle
3.若不同渠道存在功能上的差異,建議將差異部分放於單獨的dex或採用相同代碼不同配置方式實現;

強烈建議採取第二種方式!!!

未完待續~

接下來這篇文章主要講解一下Tinker的實現原理:Tinker原理深入理解(二)

歡迎交流討論,有問題也非常歡迎指出不足之處~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章