微信熱更新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%) 較高 一般 最高(99.9%)

</br>
注:

  • 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.添加工程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或采用相同代碼不同配置方式實現;

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

未完待續~

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容