Tinker為什么要使用代理Application?

在接入Tinker時, 在Android N上出現(xiàn)補丁不生效的情況,這里主要討論出現(xiàn)此狀況的原因及解決方法.

一 初始方案

在接入Tinker的時候, 按照改動小的前提,根據(jù)Tinker及TinkerManager的README.md進行接入

1. 接入
  1. 在gradle中對Tinker熱修復進行引入;
  2. 原Application直接繼承TinkerApplication類;
  3. 在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ā)者回復的建議有兩條:

  1. 按照文檔完成改造
  2. 使用類似tinkerpatch的一鍵接入功能

三 按照文檔完成改造

開發(fā)者建議的第一條方式, 對項目原有的Application進行改造.對其他引用Application或者它的靜態(tài)對象與方法的地方蹂匹,改成引用ApplicationLike的靜態(tài)對象與方法.簡單來說,就是將原Application中的所有邏輯遷移到繼承DefaultApplicationLike的Application代理類中.

改造方案
  1. 將項目中對Application及ApplicationContext的引用全局替換.
  2. 測試發(fā)現(xiàn),Dagger2的Activity/Fragment自動注入方式與Tinker改造不能很好兼容:
    在Activity初始化時,會調用AndroidInjection.inject();方法, 會將getApplication() 強轉為HasActivityInjector接口,然后調用activityInjector()這個方法.

后果
改動量大, 需要廢棄AndroidDagger注入.

四 使用類似tinkerpatch的一鍵接入功能

tinkerPatch的github中沒有提供具體的實現(xiàn)類,都是些抽象類/接口類,它的核心代碼沒開源,以下方案參照TinkerPatch混淆后JAR包的實現(xiàn).

1. 主要思路
  1. 通過插件修改AndroidManifest.xml,將入口Application改為代理Application類:
/**
 * Tinker代理的Application類.
 * <p>
 * 用于對Tinker做初始化及代理真正Application的生命周期及主要公共方法.
 */
public class TinkerProxyApplication extends TinkerApplication
  1. 反射替換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的問題缎岗。

  1. 若SDK>=24, 即Android N版本静尼,當補丁存在時,將PathClassloader替換為AndroidNClassLoader, 但是它依然繼承于PathClassLoader传泊。我們依然可以像以往那樣對它進行類似makeDexElements的操作鼠渺。

  2. 若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]模式的機制
  1. 在應用運行時分析運行過的代碼以及“熱代碼”兔甘,并將profile配置存儲下來谎碍。

  2. 在設備空閑與充電等時機,ART中的BackgroundDexOptService會“漸進式編譯”這份配置中的“熱代碼”洞焙,代碼編譯信息記錄在base.art文件中.

  3. 在APP啟動時蟆淀,一次性把“熱代碼”加載到緩存太援,將對應的class插入到PathClassLoaderClassTable中,將method更新到dexCache中.

  4. 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?
  1. 防止在Dalvik中拋出拋出unexpected DEX異常.

    補丁前,代理類和它的直接引用類(ApplicationLike)在同一個dex文件中,所以被打上了preverify標志.

    但是補丁后,代理類和它的直接引用類就不再同一個dex中了.

    如果在代理類中new關鍵字去加載它的直接引用類的話. dvmResolveClass會校驗兩個類在是否相同dex中,如果不在就會拋出unexpected DEX異常.而反射則不會走到校驗preverify的方法中,所以不會拋異常.

  2. 在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.

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上混合編譯的實施了折中方案.

目前有幾種思路去選擇:

  1. 將Application中的邏輯遷移到代理類中.

    優(yōu)點: 微信也是按這種方式適配,兼容性高.

    缺點: 改動量較大.

  2. 類似InstantRun,反射替換成真正的application.

    優(yōu)點: 與InstantRun及TinkerPatch機制類似,較為成熟.

    缺點: 兼容性問題難以避免, 當前線程在反射替換時是無法回退的.InstantRun在Gradle3.0之后已不使用此方案,后續(xù)維護成本高.

  3. 對Android N及以上系統(tǒng)不應用補丁.

    優(yōu)點: 改動小.按照上文的第一節(jié)的初始方案改動即可, 在7.0以下的系統(tǒng)可應用成功.

    缺點: 不能對application類進行熱修復, 在7.0及以上的系統(tǒng)中失效.

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末候学,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子纵散,更是在濱河造成了極大的恐慌梳码,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伍掀,死亡現(xiàn)場離奇詭異掰茶,居然都是意外死亡,警方通過查閱死者的電腦和手機硕盹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門符匾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘩例,你說我怎么就攤上這事啊胶。” “怎么了垛贤?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵焰坪,是天一觀的道長。 經常有香客問我聘惦,道長某饰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮黔漂,結果婚禮上诫尽,老公的妹妹穿的比我還像新娘。我一直安慰自己炬守,他們只是感情好牧嫉,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著减途,像睡著了一般酣藻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鳍置,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天辽剧,我揣著相機與錄音,去河邊找鬼税产。 笑死怕轿,一個胖子當著我的面吹牛,可吹牛的內容都是我干的砖第。 我是一名探鬼主播撤卢,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼梧兼!你這毒婦竟也來了放吩?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤羽杰,失蹤者是張志新(化名)和其女友劉穎渡紫,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體考赛,經...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡惕澎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了颜骤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唧喉。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖忍抽,靈堂內的尸體忽然破棺而出八孝,到底是詐尸還是另有隱情,我是刑警寧澤鸠项,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布干跛,位于F島的核電站,受9級特大地震影響祟绊,放射性物質發(fā)生泄漏楼入。R本人自食惡果不足惜哥捕,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嘉熊。 院中可真熱鬧遥赚,春花似錦、人聲如沸阐肤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泽腮。三九已至,卻和暖如春衣赶,著一層夾襖步出監(jiān)牢的瞬間诊赊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工府瞄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碧磅,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓遵馆,卻偏偏與公主長得像鲸郊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子货邓,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內容

  • 熱修復這種 非官方支持 的 非常規(guī) 開發(fā)方式秆撮,在采用前一定要權衡清楚其作用與代價。 一. Java層熱修復方案 由...
    liaowenhao閱讀 1,815評論 0 3
  • 我將熱修復原理落地實踐MyHotFix 1.熱修復技術介紹 1.1 什么是熱修復 為了修復剛發(fā)版時出現(xiàn)的緊急bug...
    keyboard3閱讀 6,454評論 3 9
  • Tinker使用 前言 寫在前面的話换况,在上家公司一直在主導組件框架的開發(fā)职辨,所以對Android領域組件化,熱更新的...
    徐正峰閱讀 1,892評論 6 6
  • 艾米札記閱讀 184評論 0 3
  • 每年生日戈二,我都會許愿舒裤,每次都是一樣的兩個愿望,自青春期記事起就沒變過觉吭,“我要不想長大腾供,我要永遠不到18歲”“我要幸...
    Lucie陸陸閱讀 423評論 0 3