七、測試
因為布局簡單且不是重點,這里就給出一張Demo 的運行圖片,剩下的就靠想像了。

1、編譯基礎包
沒有基礎包,那要補丁有什么用?所以,第一步就是打包一個apk。
在Terminal 中使用命令行./gradlew assembleDebug。不會命令行無所謂,Android Studio 為我們提供了圖形化操作,根據下圖操作即可:

如果你是要release 簽名的打包,則雙擊assembleRelease,不過還要配置簽名文件,這個后面再說。
編譯完成后,可以在build 目錄下會自動創建一個bakApk 文件夾,里面就有打包好的apk 文件,因為之后的所有生成的補丁包都以這個apk 會標準,所以這就是那個基礎包文件(相當于應用市場上的app)。

如果這個apk 文件是release 簽名且是要放到應用市場上的,那么你必須將apk 與R.txt(如果有使用混淆的話,還會有一個mapping.txt)這幾個文件保存好,切記。
現在就把這個tinker-local-debug-1206-11-48-42.apk 安裝到手機上(相當于是用戶手機上的app)。

1.點擊"say someting"按鈕吐司"Hello"。
2.點擊"get string from .so"按鈕吐司"hello LQR"。
3.點擊"show info"按鈕顯示"patch is not loaded",說明當前沒有加載補丁。
2、修復java 代碼
下面是"say someting"按鈕點擊時,調用的方法,使用Toast 顯示Hello 字符串:
```
public void say(View view) {
Toast.makeText(getApplicationContext(), "Hello", Toast.LENGTH_SHORT).show();
}
```
1)修復代碼
現在我想讓它吐司Hello World,所以代碼修改為:
```
public void say(View view) {
Toast.makeText(getApplicationContext(), "Hello World", Toast.LENGTH_SHORT).show();
}
```
2)制作補丁包
先將基礎包(前面那個tinker-local-debug-1206-11-48-42.apk 文件)重命名為old-app.apk,然后雙擊tinkerPatchDebug,操作如下圖所示:

編譯完成后,build/outputs/apk/tinkerPatch 會產生3 個補丁包,我們要的就是patch_signed_7zip.apk。

對于build/outputs/apk/tinkerPatch 目錄下文件及文件夾的詳細說明,請參考:[Tinker 官方Wiki:輸出文件詳解](https://github.com/Tencent/tinker/wiki/Tinker-接入指南)。
3)下發補丁包
將patch_signed_7zip.apk 放到手機的SD 卡目錄下:不一定是SD 卡目錄,位置由我們開發者決定,Demo 中調用
TinkerInstaller.onReceiveUpgradePatch(context, 補丁包的本地路徑)方法時,第二個參數指定了是SD 卡,故如此操作。

4)打補丁
可以看到,在install patch 之前,點擊"say someting"按鈕時,還是吐司"Hello"。點擊"install patch"按鈕后,會提示"patch success,please restart process",說明Tinker 已經打上補丁了。這時點擊"show info",可以看到"patch is not loaded",說明當前補丁還沒有生效。最后,點擊"kill myself"按鈕,殺死當前 app(進程)。

在重新打開app,再點擊"say someting"按鈕,吐司"Hello World"。再點擊"show info",可以看到"patch is loaded",說明app 重啟后補丁生效了。

小結: Tinker 熱修復無法讓補丁實時生效,在重啟進程后,補丁才會生效。Tinker 會在app 重啟后自動應用補丁。
3、修復so 庫
在一開始制作基礎包時,工程中就已經加入了一些so 文件,存放在src/main/jniLibs 目錄下,因為Android Studio 默認的庫目錄是libs(與src 同級),所以這里需要在app 的build.gradle 文件中進行配置,指定so 庫所在文件夾。

下面是"get string from .so"按鈕點擊時調用的方法:
```
public void string_from_so(View view) {
String string = JniUtil.hello();
Toast.makeText(getApplicationContext(), string, Toast.LENGTH_SHORT).show();
}
```
這個JniUtil 的代碼如下:
```
public class JniUtil {
public static String LIB_NAME = "LQRJni";
public JniUtil() {}
static {
System.loadLibrary(LIB_NAME);
}
public static native String hello();
}
```
加載so 庫有2 點需要注意:
1.System.loadLibrary(libname)加載固定目錄下的庫文件,而System.load(filename)加載指定目錄下的庫文件。
2.System.loadLibrary(libname)的參數libname 指的是庫的模塊名,不是so 文件的名字,如libLQRJni.so 文件的模塊名實際上是LQRJni。
> so 文件的制作代碼包含在Demo 中,有興趣的朋友可以嘗試自己制作。
1)替換so 文件
回歸正題,現在so 庫中得到的文字是"Hello LQR",現在變一下,我需要得到的文字是"Hello CSDN_LQR",將新的so 文件替換掉舊的so 文件即可。

2)檢查Tinker 的lib 匹配規則
在app 的build.gradle 文件中,我們前面在第三部分《Tinker 的配置及任務》的第2 節《配置Tinker 與任務》中,有如下一段配置:
```
lib {
pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.so"]
}
```
這就是Tinker 的lib 匹配規則,在生成補丁的過程中,它會去把符合這個規則的庫文件拿出來與基礎包中的庫文件進行匹配,從而將有差異的庫文件放入到補丁包中。而Tinker 官方Demo 的配置中是沒有"src/main/jniLibs//.so"這一段的,這將導致Tinker 在產生補丁包時不會去檢查src/main/jniLibs 目錄下的文件變化,進而補丁包中不會包含修復好的so 文件,這很重要,切記。
3)生成補丁與下發補丁包
生成補丁與下發補丁包的過程與之前的操作一致,這里不再重覆,不過我們來看看tinkerPatch 跟之前有什么區別吧:

最后記得將patch_signed_7zip.apk 放到手機的SD 卡目錄下。
4)卸載補丁
補丁是可以打多個的,用補丁的版本號做區分,在卸載的時候,可以根據補丁的版本號來卸載,也可以把之前所有的補丁卸載掉,實際開發中,看項目需求來解決用哪種方式來卸載補丁,這里我選擇清理之前所有的補丁,下面是"uninstall patch"按鈕的點擊事件:
```
public void uninstall_patch(View view) {
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
Tinker.with(getApplicationContext()).cleanPatch();
}
```
卸載補丁之前,要先殺死當前App 的其他進程。卸載補丁之后,App 是不安全的(因為此時Tinker 已經初始化完成),最好,需要重啟一下App。
5)打補丁
現在我們再來打一次補丁,操作如下圖所示:

可以看到,在install patch 之前,點擊"get string from .so"按鈕時,還是吐司"Hello LQR"。點擊"install patch"按鈕后,會提示"patch success,please restart process",說明Tinker 已經打上補丁了。這時點擊"show info",可以看到"patch is not loaded",說明當前補丁還沒有生效。最后,點擊"kill myself"按鈕,殺死當前app(進程)。現在重啟app,理想狀態下,當我們再次點擊"get string from .so"按鈕時,會吐司"Hello CSDN_LQR"。
然而,吐司還是"Hello LQR",并沒有變化,而且點擊"show info"按鈕后,可以看到"patch is loaded",說明補丁已經加載了,這是為啥?
再來看看下面的操作:
重啟app 后,先點擊"load library(hack)"按鈕,再點擊"get string from .so"按鈕,出現了,吐司變成了"Hello CSDN_LQR"。
上圖是Tinker 官網Wiki 的文檔部分截圖,從紅線部分可以知道,因為部分手機判斷abi 并不準確(可能因為Android 碎片化比較嚴重吧),Tinker 沒有區分abi,自然也不會在app 啟動時,自動加載對應的so 庫,這需要開發者自己判
斷。
下面是"load library(hack)"按鈕點擊調用的方法:
```
public void load_library_hack(View view) {
String CPU_ABI = android.os.Build.CPU_ABI;
// 將tinker library 中的CPU_ABI 架構的so 注冊到系統的library path 中。
TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI);
}
```
這是Tinker 提供的使用Hack 方式加載補丁中的so 庫,只是一個方法調用而已,并沒有什么特別。對于非Hack 方式加載補丁的方式,我本人是沒有測試成功的,很奇怪,搞不明白問題的原因,官方的文檔也寫得不清不楚的,有知道本Demo加載不成功的原因的朋友請不吝賜教一下哈,thx。
> 小結: Tinker 雖然會在app 重啟后自動加載補丁,但不會自動加載補丁中的so 文件,開發者需自己判定好abi 來加載so 文件。
4、修復資源文件
這部分跟前面的重合度極高,故不做演示了,你可以在補丁包中對本demo 中的頭像進行替換試試,與修復java 文件的操作基本一致,這部分需要提醒的是,app 的build.gradle 文件中Tinker 配置有如下這一段:
```
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
...
}
```
不難理解,這就是Tinker 對資源文件的匹配規則,日常開發夠用,如果你的項目中把資源文件放到了這里沒有的目錄下,需要修改這部分的配置。
- 第一章 熱修復設計
- 第一節、AOT/JIT & dexopt 與dex2oat
- 一、AOT/JIT
- 二、dexopt 與dex2oat
- 第二節、熱修復設計之CLASS_ISPREVERIFIED 問題
- 一、前言
- 二、建立測試Demo
- 三、制作補丁
- 四、加載補丁
- 五、CLASS_ISPREVERIFIED
- 第三節、熱修復設計之熱修復原理
- 一、Android 熱修復
- 二、Android 虛擬機和編譯加載順序
- 三、混合模式的理解
- 四、源碼類到機器執行的文件過程
- 五、補丁包
- 六、類補丁生效原理
- 七、Davlik 虛擬機的限制
- 八、Davlik Class resolved by unexpected DEX: 限制和處理方式
- 九、類加載器的雙親委派加載機制
- 第四節、Tinker 的集成與使用(自動補丁包生成)
- 一、簡述
- 二、Tinker 組件依賴
- 三、Tinker 的配置及任務
- 四、Tinker 封裝與拓展
- 五、編寫Application 的代理類
- 六、常用API
- 七、測試
- 八、細節
- 第二章 插件化設計
- 第一節、Class 文件與Dex 文件的結構解讀
- 一、Class 文件
- 二、Dex 文件
- 三、Class 文件和Dex 文件對比
- 第二節、Android 資源加載機制詳解
- 第三節、四大組件調用原理
- 第四節、so 文件加載機制
- 第五節、Android 系統服務實現原理
- 第三章 組件化框架設計
- 第一節、阿里巴巴開源路由框——ARouter 原理分析
- 第二節、APT 編譯時期自動生成代碼&動態類加載
- 第三節、Java SPI 機制
- 第四節、AOP&IOC
- 第五節、手寫組件化架構
- 第四章 圖片加載框架
- 第一節 圖片加載框架選型
- 第二節 Glide 原理分析
- 第三節 手寫圖片加載框架實戰
- 第五章 網絡訪問框架設計
- 第一節 網絡通信必備基礎
- 第二節 OkHttp 源碼解讀
- 第三節 Retrofit2 源碼解析
- 第六章 RXJava響應式編程框架設計
- 第一節 RXJava之鏈式調用
- 第二節 RXJava之擴展的觀察者模式
- 第三節 RXJava之事件變換設計
- 第四節 Scheduler 線程控制
- 第七章 IOC架構設計
- 第一節 依賴注入與控制反轉
- 第二節 ButterKnife 原理上篇、中篇、下篇
- 第三節 IOC架構設計之Dagger2架構設計
- 第八章 Android架構組件 JetPack
- 第一節 LiveData的工作原理
- 第二節 Navigation 如何解決tabLayout 問題
- 第三節 ViewModel 如何感知View 生命周期及內核原理
- 第四節 Room 架構方式方法
- 第五節 dataBinding 為什么能夠支持MVVM
- 第六節 WorkManager 內核揭秘
- 第七節 Lifecycles 生命周期