微信熱更新Tinker 使用及爬坑(一)

什么是熱修復

**定義 **: 熱修復(HotFix)是以補丁的方式動態(tài)修復緊急Bug薛夜,不再需要重新發(fā)布App,不需要用戶重新下載覆蓋安裝的方式來實現(xiàn)代碼的替換修改版述。這里就不多啰嗦了梯澜,可以自行搜索網(wǎng)上的介紹。

目前主流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
性能損耗 較小 較大 較小 較小
補丁包大小 較小 較大 一般 一般
開發(fā)透明 yes yes no no
復雜度 較低 較低 復雜 復雜
Rom體積 Dalvik較大 較小 較小 較小
成功率 較高(95%) 較高 一般 最高(99.9%)

</br>
注:

  • Tinker的成功率數(shù)據(jù)渴析,是從微信團隊張紹文同學那兒打聽得到的晚伙,該數(shù)據(jù)是微信APP自身的成功率,可信度高俭茧;
  • Robust的成功率數(shù)據(jù)咆疗,來自美團Robust開源項目官方文檔。
  • QZone成功率和Tinker應該在同一水平(或稍低點)的樣子母债。
  • AndFix 是公司以前就接入的午磁,內(nèi)部測試成功率只有80%左右(僅供參
    考),而且修復起來還有諸多限制毡们。

Tinker的原理

微信Tinker原理圖
Tinker流程圖

Tinker的優(yōu)勢和特性

綜合考慮來說迅皇,Tinker的補丁包以及功能全面性、穩(wěn)定性是比較吸引人的漏隐,并且功能還能做到類替換 喧半、資源替換以及So替換。這樣一來它就不僅僅是熱修復了青责,還能做到熱更新挺据。因此我們最后采用了Tinker (其實還是因為微信幾億設備也是用的Tinker這套方案,靠譜點)脖隶。

微信和阿里還提供了補丁后臺托管扁耐,版本管理SDK ,不缺錢或者不想因為熱修復對項目代碼造成侵入性的話产阱,也可以直接使用微信或阿里封裝好的傻瓜式接入方案婉称,微信 Tinker Patch 方案目前是補丁包日請求量1w以內(nèi)免費;阿里云 Sophix 目前還在公測階段构蹬,暫時不收費王暗。

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

接入Tinker步驟

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的一些參數(shù)

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

4.自定義Application代理類

程序啟動時會加載默認的Application類藻烤,這導致我們補丁包是無法對它做修改了绷雏。如何規(guī)避头滔?在這里我們并沒有使用類似InstantRun hook Application的方式,而是通過代碼框架的方式來避免坤检,這也是為了盡量少的去反射,提升框架的兼容性膘婶。

這里我們要實現(xiàn)的是完全將原來的Application類隔離起來悬襟,即其他任何類都不能再引用我們自己的Application逝段。將代碼都放到代理類ApplicationLike中來奶躯,我們需要做的其實是以下幾個工作:

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

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

對于為何放棄Instant Run 實現(xiàn)奸焙,而采用代理的方案,張紹文同學是這么解釋的:

Tinker張紹文博客截圖

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

5.Tinker SDK初始化以及調(diào)用

初始化

創(chuàng)建一個類繼承自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放到項目里,但構(gòu)造方法需要設置好代理類的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);
    }

}

調(diào)用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 打開右上側(cè)Gradle,并雙擊assembleDebug,生成基準包。

assembleDebug

6.2 安裝基準包

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

平時開發(fā)測試時我們可通過AS 開發(fā)工具下方的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到手機之后,我們在基準包代碼中已經(jīng)寫了如下代碼少孝,此時返回基準包觸發(fā)該代碼继低,則可把補丁包合并到基準包實現(xiàn)熱更新:

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的辦法,采用直接手動創(chuàng)建Application抑钟,并在構(gòu)造方法中(第二個參數(shù))涯曲,設置代理類。
  • 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或采用相同代碼不同配置方式實現(xiàn);

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

未完待續(xù)~

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末非洲,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蜕径,更是在濱河造成了極大的恐慌两踏,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梦染,死亡現(xiàn)場離奇詭異,居然都是意外死亡帕识,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門肮疗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人族吻,你說我怎么就攤上這事〕瑁” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵巍举,是天一觀的道長。 經(jīng)常有香客問我凝垛,道長懊悯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任梦皮,我火速辦了婚禮炭分,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘剑肯。我一直安慰自己捧毛,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呀忧,像睡著了一般师痕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上而账,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天胰坟,我揣著相機與錄音,去河邊找鬼泞辐。 笑死笔横,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的咐吼。 我是一名探鬼主播狠裹,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汽烦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起莉御,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤撇吞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后礁叔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牍颈,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年琅关,在試婚紗的時候發(fā)現(xiàn)自己被綠了煮岁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡涣易,死狀恐怖画机,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情新症,我是刑警寧澤步氏,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站徒爹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏界阁。R本人自食惡果不足惜胖喳,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一精续、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧顷级,春花似錦确垫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽控硼。三九已至艾少,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間幔妨,已是汗流浹背谍椅。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工雏吭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沾谜。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓基跑,卻偏偏與公主長得像媳否,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子篱竭,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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