在接入Tinker時, 在Android N上出現(xiàn)補丁不生效的情況,這里主要討論出現(xiàn)此狀況的原因及解決方法.
一 初始方案
在接入Tinker的時候, 按照改動小的前提,根據(jù)Tinker及TinkerManager的README.md進行接入
1. 接入
- 在gradle中對Tinker熱修復進行引入;
- 原Application直接繼承TinkerApplication類;
- 在TinkerApplicationLike代理類中,對Tinker及TinkerManager進行初始化;查詢補丁并對補丁進行安裝;
2. 效果
在osVersion<24的手機上,補丁下載及應用成功.(魅族MX6)
在osVersion>=24的手機上,出現(xiàn)crash:
java.lang.ClassCastException: *Application cannot be cast to *Application
原因是在Activity中使用了此代碼: (*Application) getApplication()
二 解決方案
此問題也有開發(fā)者遇到過: https://github.com/Tencent/tinker/issues/433
Tinker開發(fā)者回復的建議有兩條:
- 按照文檔完成改造
- 使用類似tinkerpatch的一鍵接入功能
三 按照文檔完成改造
開發(fā)者建議的第一條方式, 對項目原有的Application進行改造.對其他引用Application或者它的靜態(tài)對象與方法的地方蹂匹,改成引用ApplicationLike的靜態(tài)對象與方法.簡單來說,就是將原Application中的所有邏輯遷移到繼承DefaultApplicationLike的Application代理類中.
改造方案
- 將項目中對Application及ApplicationContext的引用全局替換.
- 測試發(fā)現(xiàn),Dagger2的Activity/Fragment自動注入方式與Tinker改造不能很好兼容:
在Activity初始化時,會調用AndroidInjection.inject();
方法, 會將getApplication()
強轉為HasActivityInjector接口,然后調用activityInjector()
這個方法.
后果
改動量大, 需要廢棄AndroidDagger注入.
四 使用類似tinkerpatch的一鍵接入功能
tinkerPatch的github中沒有提供具體的實現(xiàn)類,都是些抽象類/接口類,它的核心代碼沒開源,以下方案參照TinkerPatch混淆后JAR包的實現(xiàn).
1. 主要思路
- 通過插件修改AndroidManifest.xml,將入口Application改為代理Application類:
/**
* Tinker代理的Application類.
* <p>
* 用于對Tinker做初始化及代理真正Application的生命周期及主要公共方法.
*/
public class TinkerProxyApplication extends TinkerApplication
- 反射替換Application
在代理application中,反射替換真正的application凹蜈。主要方法是monkeyPatchApplication
:
/**
* 將當前APP的TinkerApplication通過反射替換成項目中實際用到的Application.
* <p>
* 難點就在于兼容性和找到所有TinkerApplication的引用處.
* <p>
* 與Instant Run的邏輯基本一致.具體代碼
* See <a >MonkeyPatcher</a>
*/
public static void monkeyPatchApplication(Application bootstrap, Application realApplication) throws Throwable;</pre>
2. 結論
這種方式的優(yōu)點在于接入容易,但是無法保證兼容性限寞,特別在反射失敗的情況,是無法回退的踪区。
但是考慮到舊版InstantRun和TinkerPatch的機制都是如此,估計不會很差, 但crash風險難避免.
五 原理剖析
1. 為什么只在osVersion>=24的機器上才出現(xiàn)ClassCastException,而且是Application轉Application?
1) 背景
Tinker沒有使用parent classloader方案昆烁,而是使用Multidex插入dexPathList方式,這里主要考慮到分平臺內部類可能存在校驗classloader的問題缎岗。
若SDK>=24, 即Android N版本静尼,當補丁存在時,將PathClassloader替換為AndroidNClassLoader, 但是它依然繼承于PathClassLoader传泊。我們依然可以像以往那樣對它進行類似makeDexElements的操作鼠渺。
若SDK<24, Tinker沒有對classloader做處理,這里需要注意補丁的Dex是插入在dexElement的前方,這樣加載類時,會優(yōu)先找到修改后的補丁類.
2) 解釋
Application是在應用啟動時一定由原生PathClassloader去加載的.
在SDK>=24的情況下, 除Application及Tinker初始化相關的類外, 其余類都是由新建的AndroidNClassLoader加載,所以(*Application) getApplication()
強轉時, getApplication得到的對象是在原生PathClassloader中的, 和AndroidNClassLoader中的*Application是兩個不同的對象,強轉會發(fā)生異常ClasssCastExcepiton.
當SDK<24的情況, 又分為大于或等于23,大于或等于19和大于或等于14及小于14這四種情況,主要考慮到各版本修改dexElement的方式不同,思路一致.以下為Tinker使用classLoader加載dex的方法:
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files) throws Throwable {
//...
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
classLoader = AndroidNClassLoader.inject(loader, application);
}
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
//...
}</pre>
可以看出,如果SDK>=24且沒有使用加固,才會使用AndroidNClassLoader. 其余情況是使用原生的PathClassLoader,所以SDK<24時應用補丁是不會出問題的.
2. Tinker為什么對Android N做特殊處理?
AndoidN混合使用AOT編譯眷细,解釋和JIT三種運行時拦盹,降低安裝時間、內存占用, 提升系統(tǒng)與應用性能溪椎。
AOT: Ahead-Of-Time 預編譯,在應用程序安裝的過程中普舆,ART就已經將所有的字節(jié)碼重新編譯成了機器碼。運行過程中無需進行實時的編譯工作校读,只需要進行直接調用沼侣。
解釋: 在運行過程中才將編譯生成的中間代碼, 生成目標平臺的成機器碼.
JIT: Just-in-time 即時編譯,一句一句編譯源代碼,但是會將翻譯過的代碼緩存起來以降低性能損耗.
1) 編譯模式
Android N的編譯模式有12種歉秫,在不同時機采用不同編譯模式.主要介紹以下兩種:
[speed]模式蛾洛,即最大限度的編譯機器碼,它的表現(xiàn)與AOT編譯一致,會占用比較多Rom空間
[speed-profile]模式雁芙,即只根據(jù)“熱代碼”的profile配置來編譯轧膘。這也是Android N中混合編譯的核心模式。
2) [speed-profile]模式的機制
在應用運行時分析運行過的代碼以及“熱代碼”兔甘,并將profile配置存儲下來谎碍。
在設備空閑與充電等時機,ART中的
BackgroundDexOptService
會“漸進式編譯”這份配置中的“熱代碼”洞焙,代碼編譯信息記錄在base.art文件中.在APP啟動時蟆淀,一次性把“熱代碼”加載到緩存太援,將對應的class插入到
PathClassLoader
的ClassTable
中,將method更新到dexCache中.-
APP在加載類時,會優(yōu)先從
ClassTable
中查找扳碍,從而達到預先加載代替用時查找以提升應用的性能.
熱代碼工作機制
3) Tinker不支持
如果base.art文件在補丁前已經存在,它們都是無法通過熱補丁更新的.
而且,如果補丁修改的類部分存在于base.art, 則只能更新一部分類,此時一部分類是新的,一部分是舊的仙蛉,由于在dex2oat時fast*
已經將類能確定的各個地址寫死笋敞,新舊類互相調用時就可能出現(xiàn)地址錯亂。
3. Tinker應對方案 - 運行時替換PathClassLoader
完全廢棄掉PathClassloader, 采用新建PathClassloader來加載后續(xù)的所有類,即可達到將cache無用化的效果, 這樣就避免了補丁無效或地址錯亂的情況.
1) 新建的AndroidNClassLoader干了什么事情?
1.將原PathClassloader中的dexPathList
信息反射賦值給AndroidNClassLoader.
2.在調用findClass
查找class時,如果是在com.tencent.tinker.loader;
這個pacakage中的類,則由原PathClassloader去加載,原因是這個包含AndroidNClassLoader
及其他Tinker初始化類的package,已經由原PathClassloader加載過了, 其余類則由AndroidNClassLoader去加載.
簡單來說, 此AndroidNClassLoader單純只是去加載后續(xù)的類而已.
2) 項目如何改造?
由于Application類是通過PathClassloader加載的,為了實現(xiàn)Application類與應用程序的邏輯解耦荠瘪,有兩種方式:
A.采用類似InstantRun的實現(xiàn)夯巷;在代理application中,反射替換真正的application哀墓。
B.采用代理Application實現(xiàn)的方法趁餐;即Application的所有實現(xiàn)都會被代理到其他類,Application類不會再被使用到篮绰。
B方案就是前面第三節(jié)講的按照文檔完成改造, 這種方式沒有兼容性的問題后雷,但是會帶來一定的接入成本。微信采用了B方案, 考慮到要應對Android數(shù)億用戶, 涉及到反射的框架往往都不能經受兼容性的考驗.
4. 如何按照A方案改造?
A方案中, InstantRun的實現(xiàn)做了什么事?
1.利用Gradle提供的Transform API插樁.并修改AndroidManifest.xml文件,將原Application替換成代理Application.
2.將代理Application看作一個宿主程序吠各,目的是將app作為資源dex加載起來. 代理Application會初始化原Application,代理原Application的生命周期, 并替換所有當前app的代理application為原Application,使得之后訪問到的applicaiton仍然是原application.
InstantRun的主要原理是通過設置父ClassLoader, 優(yōu)先加載所有發(fā)生改變的patch代碼類. 如果資源發(fā)生變化,則反射替換AssetManager,將發(fā)生改變的資源路徑添加進來. 然后根據(jù)改動情況,選擇是熱部署/溫部署/冷部署使其生效.在最新gradle3.0中已經用ContentProvider去實現(xiàn)了.
1) 如何替換呢?
1.替換ActivityThread的mInitialApplication
為原Application
2.替換mAllApplications
中所有的代理Application為原Application
3.替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為原Application臀突。
2) TinkerPatch是如何對原Application進行改造的呢?
可以發(fā)現(xiàn)前文第四節(jié)使用類似tinkerpatch的一鍵接入功能與InstantRun機制差不多. TinkerPatch中替換application的方法如下所示, 與InstantRun中替換的代碼也幾無二致.
3) 方案A的反射兼容問題有多大?
Tinker團隊之前做過測試,100萬人會有幾十個在替換的時候出現(xiàn)問題.
4) 代理Application為什么要通過反射初始化ApplicationLike及原Application?
-
防止在Dalvik中拋出拋出unexpected DEX異常.
補丁前,代理類和它的直接引用類(ApplicationLike)在同一個dex文件中,所以被打上了preverify標志.
但是補丁后,代理類和它的直接引用類就不再同一個dex中了.
如果在代理類中new關鍵字去加載它的直接引用類的話.
dvmResolveClass
會校驗兩個類在是否相同dex中,如果不在就會拋出unexpected DEX異常.而反射則不會走到校驗preverify的方法中,所以不會拋異常. -
在Android N上, 補丁可能失效.
如果在啟動Application中直接new構造Application及ApplicationLike, 會導致AndroidNClassLoader加載的Application及ApplicationLike仍是舊類.甚至會由于新舊類的相互引用導致地址錯亂.
推測是因為加載啟動Application時,已經將類能確定的各個地址(比如它的直接引用類) 寫死贾漏,所以對原Application及ApplicationLike有修改會失效.
反射最直接的目的是為了隔離開這兩個類,并使得補丁能對原Application及ApplicationLike生效.
5. Tinker應對方案的缺點及改進
這種方案的缺點是會廢棄Android N上base.art這種混合編譯的好處, 會給應用帶來最高可達大約15%的性能損耗,且會占用更多的ROM空間.
針對上述Android N的問題, 且考慮到dex合成的ROM過大, OTA后存在黑屏等情況, Tinker根據(jù)平臺區(qū)分dex合成方式. Dalvik平臺合成完整dex; Art平臺只合成需要的類,即下圖的mini.dex.
然而, 不久后這種優(yōu)先加載補丁dex方案遇到問題,主要是因為不能兼容ART環(huán)境下的方法內聯(lián)策略.
原因:因為補丁dex只覆蓋了舊dex的一部分類,一旦被覆蓋的類被內聯(lián)到了調用者里, 即使調用了覆蓋類的方法,執(zhí)行流程也并未調到新方法中. 因為調用者調用補丁類中的方法/成員/字符串查找都還是用的舊索引.
解決方案:ART平臺下使用全量DEX,這樣所有方法都在NewDex中就不怕內聯(lián)了.所以又回到了之前的方案...
六 總結
在Android N上出現(xiàn)補丁不生效的原因,主要是因為Tinker針對N上混合編譯的實施了折中方案.
目前有幾種思路去選擇:
-
將Application中的邏輯遷移到代理類中.
優(yōu)點: 微信也是按這種方式適配,兼容性高.
缺點: 改動量較大.
-
類似InstantRun,反射替換成真正的application.
優(yōu)點: 與InstantRun及TinkerPatch機制類似,較為成熟.
缺點: 兼容性問題難以避免, 當前線程在反射替換時是無法回退的.InstantRun在Gradle3.0之后已不使用此方案,后續(xù)維護成本高.
-
對Android N及以上系統(tǒng)不應用補丁.
優(yōu)點: 改動小.按照上文的第一節(jié)的初始方案改動即可, 在7.0以下的系統(tǒng)可應用成功.
缺點: 不能對application類進行熱修復, 在7.0及以上的系統(tǒng)中失效.