大概目錄:
* Tinker 編譯相關問題?
* Tinker 庫中有什么類是不能修改的?
* 什么類需要放在主 dex 中?
* 我應該使用哪個作為補丁包下發,如何做多次修復?
* 如何對 Library 文件作補丁?
* 如何對資源文件作補丁,為什么有時候會提示大量沒有改變的圖片發生變更?
* Tinker 中的 dex 配置 'raw' 與 'jar' 模式應該如何選擇?
* 如何兼容多渠道包?
* tinker 是否兼容加固?
* Google Play 版本是否可以有 Tinker 相關代碼?
* tinker 與 instant run 的兼容問題?
* 每次編譯我應該保留哪些文件,如何兼容 AndResGuard?
* tinkerId 應該如何選擇?
* 如何使生成的補丁包更小?
* 關于使用的 ClassLoader 問題?
* 什么時候調用 installTinker?
* Proguard 5.2.1 applymapping 出現 Warning?
* TinkerPatch 補丁管理后臺與 Tinker 的關系?
* Tinker 的最佳實踐?
一、Tinker 編譯相關問題?
編譯過程相關的 issue 請先查看是否是以下情況:
無法打開 sample 工程: 請使用單獨的 IDE 窗口打開
tinker-sample-android
工程;tinkerId is not set
:是因為沒有正確地配置 IDE 的git
路徑,若不是通過clone
方式下載tinker
,需要本地手動commit
一次。這里你也可以使用其他字符作為tinkerId
;對于編譯與補丁時發生的異常,請到 Tinker 自定義擴展 中查看具體錯誤碼的原因。并通過
"Tinker."
過濾Tinker
相關的日志提交到issue
中;若自定義
TinkerResultService
,請務必將新的Service
添加到Manifest
中;權限問題;請務必已經將讀取
sdk
權限添加到AndroidManifest.xml
中,并且已允許權限運行;若使用
DefaultLifeCycle
注解生成 Application,需要將原來 Application 的實現移動到ApplicationLike
中,并將原來的 Application 類刪掉;關于 Application 的改造這一塊大家比較疑惑,這塊請認真閱讀自定義 Application 類,大部分的 app 應該都能在半小時內完成改造。
如果出現
Class ref in pre-verified class resolved to unexpected implementation
異常,請確認以下幾點:Application中傳入ApplicationLike
的參數時是否采用字符串而不是Class.getName
方式;新的 Application 是否已經加入到dex loader pattern
中;額外添加到dex loader pattern
中類的引用類也需要加載到loader pattern
中。
二、Tinker 庫中有什么類是不能修改的?
Tinker 庫中不能修改的類一共有 26 個,即 com.tencent.tinker.loader.*
類。加上你的 Appliction 類,只有 26 個類是無法通過 Tinker 來修改的。即使類似 Tinker.java
等管理類,也是可以通過 Tinker 本身來修改。
注意,在 1.7.6 版本之前,我們需要手動將不能修改的類添加到 tinkerPatch.dex.loader pattern
中。對于 1.7.6 以后的版本會自動生成。
但是若使用 newApk 或者命令行編譯,需要手動添加 Application 類與 loader 類
三、什么類需要放在主 dex 中?
Tinker 并不干涉你分包與多 dex 的加載邏輯,但是你需要確保以下幾點:
com.tencent.tinker.loader.*
類,你的 Application 類需要在主 dex,并且已經在dex.loader
中配置;若你自定義了
TinkerLoader
類,你需要將TinkerLoader
的自定義類,以及它用的到類也放在主 dex,并且已經在dex.loader
中配置;ApplicationLike
的繼承類也需要放在主 dex 中,但是它無須在dex.loader
中配置,因為它是可以使用 Tinker 修改的類。最后,如果你需要在加載其他 dex 之前加載 Tinker 的管理類,你也可以將com.tencent.tinker.*
都加入到主 dex。你的
ApplicationLike
實現類的直接引用類以及在調用Multidex install
之前加載的類也都需要放到主 dex 中。
注意:Tinker 會自動生成需要放在主 dex 的 keep 規則。在 1.7.6 版本之前,你需要手動將生成規則拷貝到自己的 multiDexKeepProguard
文件中。例如 Sample 中的 multiDexKeepProguard file("keep_in_main_dex.txt")
。在 1.7.6 版本之后,這里會通過腳本自動處理,無須手動填寫。
另外,如果 minsdkverion >= 21
,multiDexEnabled
會被忽略。我們可以在 build/intermediates/multi-dex
查找最終的 keep
規則以及結果。
四、我應該使用哪個作為補丁包下發,如何做多次修復?
patch_signed_7zip.apk
是已簽名并且經過 7z 壓縮的補丁包,但是你最好重命名一下,不要讓它以 .apk
結尾,這是因為有些運營商會挾持以 .apk
結尾的資源。
另外一點,我們在發起補丁請求時,需要先將補丁包先拷貝到 dataDir 中。因為在 sdcard 中,補丁包是極其容易被清理軟件刪除。這里可以參考 UpgradePatchRetry.java 的實現。
對于補丁包的版本問題,我們可以在 packageConfig
中增加,例如 sample 中的
packageConfig {
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
Tinker 支持對同一基準版本做多次補丁修復,在生成補丁時,oldApk 依然是已經發布出去的那個版本。即補丁版本二的 oldApk 不能是補丁版本一,它應該依然是用戶手機上已經安裝的基準版本。
五、如何對 Library 文件作補丁?
當前官方并沒有直接將補丁的 lib 路徑添加到 DexPathList
中,理論上這樣可以做到程序完全沒有感知的對 Library 文件作補丁。這里主要是因為在多 abi 的情況下,某些機器獲取的并不準確。當前對 Library 文件作補丁可參考 Tinker API 概覽,tinker 1.7.7 版本中也提供了一鍵反射的方案給大家選擇。
大家可以根據自己的項目需要選擇合適的方案,事實上,無論是對 Library 還是 Application,官方都是采用盡量少去反射的策略,這也是為了提高 Tinker 框架的兼容性。上線前,我們應當嚴格測試補丁是否正確加載了修改后的 So 庫。
六、如何對資源文件作補丁,為什么有時候會提示大量沒有改變的圖片發生變更?
Tinker 采用全量合成方式實現資源替換,這里有以下幾點是使用者需要明確的:
remoteView
是無法修改,例如transition
動畫,notification icon
以及桌面圖標;對于資源文件的更新(尤其是
assets
),需要注意代碼中是否采用直接讀取sourceApk
路徑方式讀取,這樣方式是無法更新的;
Tinker 只會將滿足res pattern
的資源放在最后的合成補丁資源包中。一般為了減少合成資源大小,官方不建議輸入classes.dex
或lib
文件的pattern
;若一個文件
:assets/classes.dex
,它既滿足dex pattern
,又滿足res pattern
。Tinker 只會處理dex pattern
,然后在合成資源包會忽略assets/classes.dex
的變更。library 也是如此。只要資源發生變成的前提下 Tinker 才會合成新的資源包,這一定程度會增加占 Rom 體積,請在考慮后使用。
注意:若出現資源變更,我們需要使用 applyResourceMapping
方式編譯,這樣不僅可以減少補丁包大小,同時防止 remote view id
變更造成的異常情況。最后我們應該查看編譯過程中生成的 resources_out.zip
是否滿足我們的要求。
有時候會發現大量明明沒有改變的 png
發現變更,解壓發現的確兩次編譯這些 png
的 md5 不一致。經分析,aapt
在其中一次編譯將 png
優化成 8-bit,另外一次卻沒有,從而導致 png
改變了。如果你們 app 出現了這種情況,官方建議關閉 aapt
對 png
的優化:
aaptOptions{
cruncherEnabled false
}
若你對安裝包大小非常 care,可以提前使用命令行工具將所有圖片手動優化一次。我們也可以選擇一些有損壓縮工具,獲得更大的壓縮效果。
如果你確認 png
并沒有修改,你可以在 tinker
的配置使用 ignoreChange
來忽略所有 png
文件的修改。
res {
ignoreChange = ["*.png"]
}
七、Tinker 中的 dex 配置 'raw' 與 'jar' 模式應該如何選擇?
它們應該說各有優劣勢,大概應該有以下幾條原則:
如果你的
minSdkVersion
小于 14,那你務必要選擇 'jar' 模式;以一個
10M
的 dex 為例,它壓縮成 jar 大約為4M
,即 'jar' 模式能節省6M
的 ROM 空間。對于 'jar' 模式,我們需要驗證壓縮包流中 dex 的 md5,這會更耗時,在小米 2S 上數據大約為 'raw' 模式
126 ms
,'jar' 模式為246 ms
。
因為在合成過程中 Tinker 已經校驗了各個文件的 Md5,并將它們存放在 /data/data/..
目錄中。默認每次加載時 Tinker 并不會去校驗 tinker 文件的 Md5,但是你也可通過開啟 loadVerifyFlag
強制每次加載時校驗,但是這會帶來一定的時間損耗。
簡單來說,'jar' 模式更省空間,但是運行時校驗的耗時大約為 'raw' 模式的兩倍。如果你沒有打開運行時校驗,推薦使用 'jar' 模式。
八、如何兼容多渠道包?
關于渠道包的問題,若使用 flavor
編譯渠道包,會導致不同的渠道包由于 BuildConfig
變化導致 classes.dex
差異。這里建議的方式有:
將渠道信息寫在
AndroidManifest.xml
或文件中,例如channel.ini
;將渠道信息寫在
apk
文件的zip comment
中,這種是建議方式,例如可以使用項目 packer-ng-plugin 或者可使用V2 Scheme
的 walle;若不同渠道存在功能上的差異,建議將差異部分放于單獨的 dex 或采用相同代碼不同配置方式實現;
事實上,tinker
也支持多 flavor
直接編譯多個補丁包,具體可參考多 Flavor 打包。
九、tinker 是否兼容加固?
tinker 1.7.8 可以通過 isProtectedApp
開啟加固支持,這種模式僅僅可以使用在加固應用中。
加固廠商 | 測試 |
---|---|
騰訊云·樂固 | Tested |
愛加密 | Tested |
梆梆加固 | Tested |
360加固 | Tested,需要 5 月 8 日以后加固的版本 |
其他 | 請自行測試,只要滿足下面規則的都可以支持 |
這里是否支持加固,需要加固廠商明確以下兩點:
不能提前導入類;
在
art
平臺若要編譯oat
文件,需要將內聯取消。
十、Google Play 版本是否可以有 Tinker 相關代碼?
由于 Google play 的使用者協議,對于 GP 渠道我們不能使用 Tinker 動態更新代碼,這里會存在應用被下架的風險。但是在 Google play 版本,我們依然可以存在 Tinker 的相關代碼,但是我們需要屏蔽補丁的網絡請求與合成相關操作。
十一、tinker 與 instant run 的兼容問題?
事實上,若編譯時都使用 assemble*
,tinker
與 instant run
是可以兼容的。但是不少用戶基礎包與補丁包混用兩種模式導致補丁過大,所以 tinker
編譯時禁用 instant run
,我們可以在設置中禁用 instant run
或使用 assemble
方式編譯。
大家日常 debug
時若想開啟 instant run
功能,可以將 tinker
暫時關閉:
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = false
}
十二、每次編譯我應該保留哪些文件,如何兼容 AndResGuard?
正如 sample 中 app/build.gradle,每個可能用到 Tinker 發布補丁的版本,需要在編譯后保存以下幾個文件:
編譯后生成的
apk
文件,即用來編譯補丁的基礎版本;若使用
proguard
混淆,需要保持mapping.txt
文件;需要保留編譯時的
R.txt
文件;若你同時使用了資源混淆組件 AndResGuard,你也需要將混淆資源的
resource_mapping.txt
保留下來,同時將r/*
也添加到res pattern
中。具體我們可以參考 build.gradle。
微信通過將補丁編譯與 Jenkins 很好的結合起來,只需要點擊一個按鈕,即可方便的生成補丁包。也可以參考 tinkerpatch-andresguard-sample。
十三、tinkerId 應該如何選擇?
tinkerId
是用來區分基準安裝包的,我們需要嚴格保證一個基準包的唯一性。在設計的初期,官方使用的是基準包的 CentralDirectory
的 CRC
,但某些 APP 為了生成渠道包會對安裝包重新打包,導致不同的渠道包的 CentralDirectory
并不一致。
編譯補丁包時,Tinker 會自動讀取基準包 AndroidManifest
的 tinkerId
作為 package_meta.txt
中的 TINKER_ID
。將本次編譯傳入的 tinkerId
,作為 package_meta.txt
中的 NEW_TINKER_ID
。當前 NEW_TINKER_ID
并沒有被使用到,只是保留作為配置項。如果我們使用 git rev
作為 tinkerid
,這時只要使用 git diff TINKER_ID NEW_TINKER_ID
即可獲得所有的代碼差異。
我們需要保證 tinkerId
一定是要唯一性的,這里推薦使用 git rev
或者 svn rev
。如果我們升級了客戶端版本,但 tinkerId
與舊版本相同,會導致可能會加載舊版本的補丁。這里我們一定要注意,升級可客戶端版本,需要更新 tinkerId
!
十四、如何使生成的補丁包更小?
對于代碼來說,我們最好記住以下幾條規則:
編譯補丁包時,
proguard
使用applymapping
模式;對于多 dex 的情況,保持原本的分包規則,盡量減少由于分包變化而帶來的變更。在生成補丁包過程中,對于 class 分包的變化將會輸出
Warning:Class Moved
日志,我們應該盡量減少這種變化;大量靜態常量的改變與資源 R 文件的變更,這里推薦使用
applyResouceMapping
方式保持資源 ID。大量類分包的改變對補丁包的影響不大,但是對于合成的時間消耗與占 ROM 的體積影響更大。我們每次生成補丁后,都應該查看TinkerPatch
輸出文件夾的日志;其他的例如使用
force jumbo
模式以及使用7zip
壓縮補丁包。
十五、關于使用的 ClassLoader 問題?
Tinker 沒有使用 parent classloader
方案,而是使用 Multidex
插入 dexPathList
方式,這里主要考慮到分平臺內部類可能存在校驗 classloader
的問題。
若
SDK >= 24
,即 Android N 版本,當補丁存在時,Tinker 將PathClassloader
替換為AndroidNClassLoader
,但是它依然繼承與PathClassLoader
。我們依然可以像以往那樣對它進行類似makeDexElements
的操作。;若
SDK < 14
,Tinker 沒有對classloader
做處理,這里需要注意補丁的Dex
是插入在dexElement
的前方。
十六、什么時候調用 installTinker?
首先我們推薦在最開始的時候就是執行 installTinker
操作,但是即使你不去 installTinker
,也不會影響 Tinker
對代碼、So 與資源的加載。installTinker
只是做了以下幾件事件:
回調
LoadReporter
,返回加載結果;初始化各個自定義類與 Tinker 實例,可以調用 Tinker 相關 API,發起升級補丁以及處理相關的回調。
事實上,微信只在主進程與 :patch
進程執行 installTinker
操作。其他進程只要不處理回調結果,不發起補丁請求即可。在 SampleUncaughtExceptionHandler 中,為了防止 Crash
并沒有執行 installTinker
,全部使用的是 TinkerApplicationHelper 中的 API,詳細可以查看 Tinker API 概覽。
十七、Proguard 5.2.1 applymapping 出現 Warning?
這是因為 5.2.1 增加了內聯函數的行輸出信息導致,你可以使用以下幾種方法解決:
使用 5.1 版本
proguard
;將內聯函數的優化關掉;
自己對
mapping
文件去除內聯函數的行信息。
如果使用 4.X 版本的 Proguard
強烈建議升級到 5.1 版本。可以先下載 5.1 的 Proguard
, 然后通過以下方式指定:
classpath files('proguard-5.1.jar')
若使用 gradle 編譯,與 multiDexKeepProguard
不同,我們無需將生成的 tinker_proguard.pro
拷貝到自己的配置中。另外一個方面,若 applymapping
過程出現沖突,我們可以采取以下幾個方法:
添加
ignoreWarning
;需要注意的是如果某些類的確需要采用新的mapping
,這樣補丁后 App 會出問題,一般并不建議采用這種方式;修改基準包的
mapping
文件;我們需要根據新的mapping
文件,修正基準包的mapping
文件。例如將warning
項刪掉或者將新mapping
中keep
的項復寫到基準的mapping
中。可以參考腳本 proguard_warning.py 與 merge_mapping.py。
注意,如果想通過直接刪除舊 mapping
文件的沖突項,需要注意刪除類的內部類是否存在混淆沖突。
十八、TinkerPatch 補丁管理后臺與 Tinker 的關系?
TinkerPatch 平臺是第三方開發基于 CDN 分發的補丁管理后臺。它提供了補丁后臺托管,版本管理,一鍵傻瓜式接入等功能,讓我們可以無需修改任何代碼即可輕松接入Tinker。
我們可以根據自己的需要選擇接入,它是獨立于 Tinker 項目之外。
十九、Tinker 的最佳實踐?
為了使補丁的成功率更高,官方在 Sample 中還做了以下工作:
由于合成進程可能被各種原因殺死,使用 UpgradePatchRetry.java 來做重試功能,提高成功率;
防止補丁后程序無法啟動,使用 SampleUncaughtExceptionHandler.java 做
crash
啟動保護。這里更推薦的是進入安全模式,使用配置的方式強制清理或者升級補丁;為了防止
BuildConfig
的改變導致大量類的變更,使用 BuildInfo.java 非 final 的變量來中轉。為了加快補丁應用同時保持用戶體驗,SampleResultService.java 在應用退入后臺或手機滅屏時,才殺掉進程。你也可以在殺掉進程前,直接通過發送 broadcast 或 service intent 的方式盡快的重啟進程。
把
jumboMode
打開,防止由于字符串增多導致force-jumbol
,導致更多的變更。使用
zip comment
方式生成渠道包。