1. 程式人生 > >Android熱修復技術Tinker在Android中的實踐

Android熱修復技術Tinker在Android中的實踐

Tinker初體驗

先到Github上下載Tinker原始碼,裡面包含了tinker-sample-android,使用AndroidStudio匯入該例子工程即可。

匯入工程後,執行程式 ,出現如下錯誤:

Error:A problem occurred configuring project ':app'.
> Tinker does not support instant run mode, please trigger build by assembleDebug or disable instant run in 'File->Settings...'.

意思是說Tinker不支援 install run 模式,請手動 build assembleDebug 或者把 install run 模式禁用掉。

我把 install run 模式關閉了,然後執行成功了。如下圖所示:

這裡寫圖片描述

為了驗證熱修復功能,我在MainActivity的不佈局里加一個Button:

<Button
    android:id="@+id/division"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true"
    android:layout_below="@+id/showInfo"
android:text="division"/>

然後監聽Button的點選事件,在onClick方法裡做除法運算(1/0),當我們點選Button的時候肯定會閃退,因為除數不能為0

findViewById(R.id.division).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), "" + (1 / 0), Toast.LENGTH_LONG).show
(); } });

在MainActivity新增完程式碼後,重新執行,效果圖如下:

這裡寫圖片描述

點選我們新新增的Button(DIVISION),閃退報錯:

java.lang.ArithmeticException: divide by zero
   at tinker.sample.android.app.MainActivity$6.onClick(MainActivity.java:114)
   at android.view.View.performClick(View.java:5207)
   at android.view.View$PerformClick.run(View.java:21177)
   at android.os.Handler.handleCallback(Handler.java:739)
   at android.os.Handler.dispatchMessage(Handler.java:95)
   at android.os.Looper.loop(Looper.java:148)
   at android.app.ActivityThread.main(ActivityThread.java:5438)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)

假設這個版本已經發布到了市場,使用者安裝使用了,我們如何不更新App的情況下,修復這個Bug呢?接下來Tinker就閃亮登場了。

在我們你執行該工程的時候,Tinker會在app/build/bakAPK目錄下生成類似下面的檔案:

  • app-debug-0207-15-28-17.apk
  • app-debug-0207-15-28-17-R.txt

如下圖所示:

這裡寫圖片描述

app-debug-0207-15-28-17.apk 就相當於使用者正在使用的APK(old apk),後面用到的patch apk都是基於old apk和new apk的不同(diff)生成的。

app-debug-0207-15-28-17-R.txt R.txt 也是用於生成patch apk的。

介紹完了app/build/bakAPK下的兩個檔案,還需要配置app/gradle.build

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-0207-15-28-17.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-0207-15-28-17-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

如上面的配置所示,我們需要把tinkerOldApkPath設定為上面介紹的app-debug-0207-15-28-17.apk
tinkerApplyResourcePath設定為app-debug-0207-15-28-17-R.txt

好了,最基本的配置就介紹到這裡了,現在我們來修復一下上面除數為0的bug,很簡單,直接讓出一個不為零的除數就可,程式碼如下所示:

findViewById(R.id.division).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), "" + (1 / 1), Toast.LENGTH_LONG).show();
    }
});

如果熱修復成功,不出意外的話,當我們點選按鈕的的時候 ,應該會彈出Toast顯示1。

要想熱修復成功,首先要有patch apk,這個patch apk是根據old apk和new apk通過diff演算法得出的,老版本(old apk)有bug,然後我們我們通過程式碼把這個bug修復了(new apk),通過Tinker diff演算法就得出了patch apk,然後old apk(也就是使用者正在使用的版本)從遠端伺服器下載這個patch apk然後通過Tinker的onReceiveUpgradePatch方法實現熱修復,如下所示:

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

為了簡單起見,這個patch apk我就不真的從伺服器下載了,就直接放到本地了。

接下來點選Android Studio右側的gradle選單,找到tinkerPatchDebug雙擊它,如下圖所示:

這裡寫圖片描述

build完成之後會在app/outpus/tinkerPatch/debug/下生成patch_signed_7zip.apk檔案,如下所示:

這裡寫圖片描述

根據上面的程式碼

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

我們只要把patch_signed_7zip.apk檔案放到SD卡的根目錄就可以了,然後點選按鈕LOAD PATCH ,如果 不出意外的話,一會就會彈出Toast(patch success,please restart process)

這裡寫圖片描述

然後殺死程序,重新進入app,點選Button(DIVISION),彈出Toast:

這裡寫圖片描述

至此,通過Tinker實現了熱修復功能。

Tinker實戰

上面是我們直接執行Tinker官方的sample對Tinker的使用有了個瞭解,現在如何在一個全新的工程使用Tinker。就以我以前做過的一個專案為例,該專案是我最近一個公司的產品,主要是做藝術品電商這塊的(出現了一個小bug),當我進入藝術品詳情後介面出彈出拒絕訪問的Toast,如下圖所示:

這裡寫圖片描述

通過檢視程式碼發現犯了一個低階錯誤,因為在介面裡需要訪問購物車的數量,然後顯示在介面購車圖示的右上角,忘記了判斷使用者是否登入,竟然犯了這種低階錯誤,真是老臉一紅啊,但是畢竟是自己的錯誤,就要大膽的承認,知恥而後勇,哈哈。有bug的程式碼如下(再呼叫API之前應該判斷使用者是否登入):

@Override
public void onResume() {
    super.onResume();
    //if (UserManagerControl.isLogin()) {應該加上判斷
        orderController.getShoppingCartCount();
   //}
}

要想加入熱修復功能,

第一步,加入Tinker的依賴包,我直接參考tinker-sample-android app下的build.gralde配置,把主要的拷貝到我們的build.gradle. 這個非常簡單,配置比較多,我就不再這裡貼出來了。注意一定要記得修改ext下的 tinkerOldApkPath tinkerApplyResourcePath等屬性。

第二步,把工程目錄下的build.gradle加入Tinker plugin:

dependencies {
    classpath 'com.android.tools.build:gradle:2.2.3'
    classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}

其中TINKER_VERSION是在gradle.properties檔案裡配置的:

TINKER_VERSION=1.7.7

第三步,把一些需要的類拷貝到自己的工程,我新建了一個tinker package專門放tinker相關的類,如下圖所示:

這裡寫圖片描述

第四步,通過上面的tinker-sample-android的例子,我們知道我們需要在AndroidManifest.xml清單檔案中配置Tinker生成的Application(SampleApplication),如下所示:

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

如果我們要修改這個生成的Application的名字或者報名,怎麼修改呢?找到SampleApplicationLike把他的註解修改下即可:

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike

現在是在自己的專案中使用,首先Application的包名肯定要換的,我改成了com.hoolay.app.SampleApplication
我的專案中本來就存在了自己定義的Application(HoolayApplication)了,那怎麼辦呢?
直接讓HoolayApplication繼承自Tinker生成的SampleApplication即可。可能找不到SampleApplication這個類,需要重新rebuild一下,這個類就自動生成了。

配置完了AndroidManifest.xml裡的Application,記得配置Tinker Service:

<!--Tinker -->
<service
    android:name="com.hoolay.tinker.SampleResultService"
    android:exported="false"/>

到這裡為止,我們的專案就已經把Tinker熱修復的功能整合進來了。就下來就是測試了。

現在有bug的版本,已經上線了,使用者正在使用,現在把這個bug修復一下(很簡單加上判斷即可):

if (UserManagerControl.isLogin()) {
    orderController.getShoppingCartCount();
}

為了不修改佈局,我直接在點選購物車的時候載入patch了,便於測試用Toast提示:

String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk";
File patchFile = new File(patchPath);
if (patchFile.exists() && patchFile.length() > 0) {
    ToastUtils.showLongToast(getContext(),"正在合併熱修復檔案,請耐心等待");
    TinkerInstaller.onReceiveUpgradePatch(getContext().getApplicationContext(),
            patchPath);
}

要生成一個熱修復的包(patch apk),怎麼生成上面已經介紹了。然後把生成的patch_signed_7zip.apk放到SD 卡根目錄去。

進入藝術品詳情提示拒絕訪問,然後點選底部的購物車按鈕,提示正在合併熱修復檔案,請耐心等待,等待一段時間後,提示patch success,please restart process,流程如下圖所示:

這裡寫圖片描述

最後呢,我們重啟App 程序,進入藝術品詳情發現不會彈出拒絕訪問提示,這說明熱修復成功了。

至此,我們就把一個沒有Tinker熱更新的專案,改造成支援熱更新功能了。

需要注意的是,一般正式的專案不會這樣去需要使用者點選某個按鈕來載入修復包(patch apk),而是使用者一開始進入我們app的時候,第一個介面就應該去伺服器訪問(一般一個App中都會有一個介面,用來獲取伺服器給客戶端的全域性配置)。

假設伺服器返回有更新的包,應該就應該彈框去下載patch包,這個時候使用者是什麼都做不了的,最好加個進度條。當下載完成後,然後合併patch包,最後在跳到主介面。

最好使用HTTPS保證資料的安全,當我們訪問伺服器是否有修復包的時候,如果有,伺服器應該返回修復包patch的下載地址和檔案的MD5值,然後客戶端下載完成後對檔案進行MD5,然後和伺服器的返回的MD5值進行對比看是否一致,因為檔案可能被篡改。

未完待續

1、接下來測試熱修改資原始檔,如圖片的熱更新

2、測試SO檔案、第三方庫的熱更新

3、混淆的情況下的熱更新

4、一個Old apk下,做多次熱更新

5、Tinker和不同廠商加固不相容問題

6、如何相容多渠道包?

7、開發中應該保留哪些檔案,做好備份。

8、如何使生成的補丁包更小