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值進行對比看是否一致,因為檔案可能被篡改。