轉載請注明鏈接:https://blog.csdn.net/feather_wch/article/details/87910364
Android 熱修復原理
版本號:2019/3/3-2:10
文章目錄 Android 熱修復原理 思維導圖 技術介紹(31題) 熱修復基本概念 代碼修復 資源修復 Instant Run Sophix方案 不修改AssetManager的引用處 不必下發完整包 不需要在運行時合成完整包 SO庫修復 代碼熱修復 底層熱替換原理(23題) Andfix即時生效的原理 navtive層替換掉原方法 為什么替換ArtMethod的內容就能實現熱修復? ArtMethod的兼容性問題 訪問權限檢查 方法調用時的權限檢查 同包名下的權限問題 反射調用非靜態方法 即時生效的限制 熱修復與Java(68題) 內部類編譯 靜態內部類和非靜態內部類的區別 內部類和外部類的互相訪問 熱部署(底層替換方案) 匿名內部類編譯 域編譯 靜態field初始化/靜態代碼塊 非靜態field初始化/非靜態代碼塊 熱部署方案 final static field編譯 static和final static修飾的區別 final static優化原理 熱部署方案 方法編譯 switch case語句編譯 泛型 為什么需要泛型 類型擦除 類型擦除和多態的沖突 熱部署的方案 Lambda表達式編譯 和匿名內部類的區別 metafctory Android虛擬機中的lambda 熱部署方案 訪問權限檢查 冷啟動類加載原理(26題) 傳統實現方案 插樁實現 dexopt verifyAndOptimizeClass dvmVerifyClass dvmOptimizeClass dvmResolveClass 插樁導致類加載性能差 避免插樁的手Q方案 ART下冷啟動實現 Dalvik和Art加載dex分解的區別 Art中的方案 odex和dex 完整的方案 多態對冷啟動類加載的影響(16題) 多態 方法多態性的實現 field/static方法不具有多態 冷啟動方案的限制 終極方案 Dalvik中全量Dex方案(16題) 冷啟動類加載修復 新的全量Dex方案 對Application的處理 dvmOptResolveClass的問題 資源熱修復技術(25題) 普通的實現方式 資源文件的格式 resources.arsc ResChunk_header 資源id package id type id entry id 運行時資源的解析 資源修復方案 傳統方案 最佳方案 新增資源和id偏移 內容改變的資源 刪除的資源 對于type的影響 優雅地替換AssetManager SO庫熱修復技術(22題) SO庫加載原理 SO庫熱部署方案 SO庫冷部署方案 機型對應的so庫 問題匯總 參考資料
思維導圖
技術介紹(31題)
熱修復基本概念
1、傳統BUG修復流程的弊端?
重新發布版本代價太大 用戶下載安裝成本太高 BUG修復不及時,用戶體驗差。
2、對于這些弊端,有哪些合適的解決辦法?(or 有哪些方案能夠進行BUG的快速修復?)
方案內容缺點 Hybird方案 將需要經常變更的業務邏輯通過H5進行獨立 1. 有學習成本,需要對原有邏輯進行合理的抽象和轉換。 2. 對于無法轉為H5的代碼依舊無法修復 插件化方案 例如Atlas以及DroidPlugin方案 1.移植成本高 2.需要學習插件化工具 3. 改造老代碼的功能量大 熱修復 APP直接從云端下拉補丁和更新
三大優勢
3、熱修復的3大優勢
無需重新發版,實時高效熱修復。 用戶無感知修復,無需下載新的應用,代價小。 修復成功率高
三大領域
4、Android 熱修復的3大領域
代碼修復 資源修復 so修復
傳統框架實現方式
5、傳統熱修復框架的實現方式
框架方案缺點 Xposed 手淘,底層結構替換方案,針對Dalvik虛擬機開發的Java Method Hook技術-Dexposed 1.對于底層Dalvik結構過于依賴 2.無法繼續兼容ART虛擬機(Android 5.0起) Andfix 支付寶,底層結構替換方案,做到了Dalvik和ART環境的全版本兼容。 Hotfix 阿里百川,Andfix升級版,業務邏輯解耦 1.底層結構的替換方案``穩定性差 2.使用范圍限制多 3.不支持資源和so修復 超級補丁技術 QQ控件 Tinker 微信, Amigo 餓了么, Robust 美團,
Sophix概覽
6、Sophix的設計理念
核心理念:非侵入性 打包過程不會侵入到apk的build流程中。也不會增加任何AOP代碼,對開發者透明化。
優勢
7、Sophix框架的優勢
支持代碼修復、資源修復、so修復 集成非常簡單,沒有侵入性。
缺點
8、Sophix的缺點
唯一缺點,是不支持四大組件的增加。但是支持四大組件的增加必然導致代碼侵入性過強。 一般熱修復也使用于修復故障。而不是增加很多新功能。因此也不需要。 可以通過增加Fragment,增加新功能。
代碼修復
9、代碼修復的兩大主要方案
阿里系的底層替換方案。 騰訊系的類加載方案。
10、底層替換方案和類加載方案的優劣
方案優點缺點 底層替換 1.時效性最好 2.加載輕快 3.立即見效 限制很多 類加載 1.修復范圍廣 2.限制少 1.時效性差,需要冷啟動才能見效
底層替換方案
傳統方案
11、底層替換方案是什么?
在已經加載了的類中直接替換掉原有的方法 是在原有類的基礎上進行的修改,因此無法進行方法和字段的增減(這會破壞原有類的結構) 該方案的底層替換具有不穩定性
類方法的增減
12、為什么底層替換方案無法增減原有類的方法?
會導致該類和整個Dex的方法數變化 方法數的變化會造成方法索引的變化,這樣訪問方法時,就無法正常所引導正確的方法。
類字段的增減
13、為什么底層替換方案無法增減原有類的字段?
增加和減少了字段和增減方法一樣,會導致所有字段的索引發生變化。 最嚴重的是, 在app運行時某個類突然增加了字段,而原先已經產生的該類的實例還是原來的結構(這是無法改變的),后續對這個老實例對象訪問新增字段是很致命的。
不穩定性
14、底層替代方案是如何實現的?
無論是Dexposed、AndFix以及其他的Hook方案都是直接修改虛擬機方法的具體字段。 例如修改Dalvik方法的jni函數指針、修改類的訪問權限、修改方法的訪問權限
15、底層替代方案的不穩定性?
1.這種依賴于具體字段的Hook方案,各個廠商會對源代碼進行改造,從而導致不匹配。
例如Andfix里ArtMethod的結構是根據開源Android源碼中的結構寫死的。如果結構發生改變,就會導致替換機制出錯。
無視底層結構的替換方案
16、無視底層具體結構的替換方法
忽略底層ArtMethod結構的差異 所有Android版本都不需要區分 即使Android版本不斷修改ArtMethod的成員,只要保證ArtMethod數據仍然是線性結構排序就沒問題
類加載方案
傳統方案的原理
17、傳統類加載方案原理是什么?
app重新啟動后讓ClassLoader去加載新的類 不重啟app,原來的類還在虛擬機中,就無法加載新的類。
18、騰訊系三類加載方案的實現原理
QQ控件會侵入打包流程,增加無用信息,不優雅。 QFix方案,獲取底層虛擬機的函數,不夠穩定可靠,且無法新增public函數 微信Tinker,完整的全量Dex加載。會對Dex內容非常精細的比較(方法和指令的維度),性能消耗嚴重。
Dex比較維度
19、Dex的比較維度有三種
方法和指令的維度: 粒度過細,性能差 bsbiff: 粒度粗糙 類的維度: 粒度最合適,能夠達到時間和空間平衡的最佳效果
Sophix的方案
20、Sophix的類加載方案
dex的比較維度:類的維度 采用全量合成dex: 利用Android原先的類查找和合成機制,快速合成新的全量Dex-不需要處理合成時方法數超過的問題,也不會破壞性重構dex的結構。 重新排列包中dex的順序。虛擬機查找時優先找到classes.dex中的類,然后才是 classes2.dex、classes3.dex
類插樁
21、Sophix中的dex文件級別的類插樁方案
將舊包和補丁包中的classes.dex的順序進行了重排 讓系統自動實現類覆蓋的目的,大大減少合成補丁的開銷
雙劍合璧
22、兩個方案的合并
底層替換方案和類加載方案合并使用 補丁工具根據實際代碼變動情況: 1. 小修改,在底層替代方案的適用范圍內:底層替代方案-即時生效 1. 其余:類加載方案-即時性差 Sophix底層會判斷機型是否支持熱修復:如果機型底層虛擬機構造不支持,依舊走類加載修復
資源修復
23、熱修復的方案大部分都參考了Instant Run的實現
Instant Run
24、Instant Run中的資源熱修復的原理?
構造一個新的AssetManager. 反射調用addAssetPath,將這個完整的新資源包加入到新AssetManager中。 找到所有引用舊AssetManager的地方,通過反射,將引用處替換為新AssetManager-該Manager包含所有新資源
25、Instant Run的資源熱修復主要工作都是在處理兼容性和查找到AssetManager引用處,替換邏輯很簡單。
Sophix方案
26、Sophix的資源修復方案
構造一個package id = 0x66的資源包,包含兩種資源:1.新增資源 2.原有內容發生改變的資源 直接在原有AssetManager中addAssetPath0x66資源包,不和已經加載的0x7f沖突。不再需要去找到所有引用AssetManager的地方 Android 4.4及以下:需要在原有的AssetManager對象上進行析構和重構。保證addAssetPath生效。Android 5.0開始,addAssetPath(0x66資源包)會直接加載和解析資源。
27、Sophix資源修復方案的優勢
不修改AssetManager的引用處,替換更快更安全(對比Instant Run以及所有copycat的實現) 不必下發完整包,補丁包只包含改動的資源(對比Instant Run、Amigo等方式的實現) 不需要在運行時合成完整包。不占用運行時資源。(對比Tinker的實現)
不修改AssetManager的引用處
28、不修改AssetManager的引用處
直接在原有的AssetManager對象上進行析構和重構。不再需要去替換所有舊AssetManager的引用
不必下發完整包
29、不必下發完整包
構造一個package id = 0x66的資源包,包含新增資源和原有內容發生改變的資源 直接在原有AssetManager中addAssetPath0x66資源包,會優先找到0x66資源包中的資源
不需要在運行時合成完整包
30、不需要在運行時合成完整包
采用dex文件級別的類插樁方案 重新排列包中dex的順序。虛擬機查找時優先找到classes.dex中的類,然后才是 classes2.dex、classes3.dex。系統自動實現類覆蓋。
SO庫修復
31、SO庫修復的原理
本質是對native方法的修復和替換 采用類似類修復的反射注入方式,把補丁so庫的路徑插入到nativeLibraryDirectories數組的最前方,這樣加載so庫的時候是補丁so庫 該方案在啟動期間,反射注入補丁so庫,而不是其他方案手動替換系統的System.load()來實現替換目的
代碼熱修復
底層熱替換原理(23題)
Andfix即時生效的原理
navtive層替換掉原方法
1、Andfix的即時生效原理
Andfix即時生效,不需要重新啟動,但是也有使用限制(不能增減方法和字段,只能替換掉原方法)。 方法:在已經加載的類中,直接在navtive層替換掉原方法,
replaceMethod()
2、AndFix的核心:replaceMethod()
獲取到原有方法的Method對象,并且替換為新方法dest 根據虛擬機類型是art還是dalvik,調用對應替換的方法(art/dalvik_replaceMethod)。 Android 4.4以下是dalvik, 4.4及以上是ART虛擬機
@AndFix / src
/ com
/ alipay
/ enuler
/ andfix
/ AndFix
. java
private static native void replaceMethod ( Method src
, Method dest
) ; @AndFix / jni
/ andfix
. cpp
static void replacMethod ( JNIEnv
* env
, jclass clazz
, jobject src
, jobject dest
) { is ( isArt
) { art_replaceMethod ( env
, src
, dest
) ; } else { dalvik_replaceMethod ( env
, src
, dest
) ; }
} @AndFix / jni
/ art
/ art_method_replace
. cpp
extern
void art_replaceMethod ( JNIEnv
* env
, jobject src
, jobject dest
) { if ( apilevel
> 23 ) { replace_7_0 ( env
, src
, dest
) ; } else if ( apilevel
> 22 ) { replace_6_0 ( env
, src
, dest
) ; } else if ( apilevel
> 21 ) { replace_5_1 ( env
, src
, dest
) ; } else if ( apilevel
> 19 ) { replace_5_0 ( env
, src
, dest
) ; } else { replace_4_4 ( env
, src
, dest
) ; }
}
3、Android 6.0為例解析替換函數:replace_6_0
每個Java方法在art中都一個對應的ArtMethod ArtMethod記錄著Java方法的所有信息:所屬類、訪問權限、代碼執行地址等等。 利用ArtMethod指針對所有成員進行修改。 這樣后續調用該Java方法就會走到新的方法實現中
@AndFix / jni
/ art
/ art_method_replace_6_0
. cpp
void replace_6_0 ( JNIEnv
* env
, jobject src
, jobject dest
) { art
: : mirror
: : ArtMethod
* srcMeth
= ( art
: : mirror
: : ArtMethod
* ) env
- > FromReflectedMethod ( src
) ; art
: : mirror
: : ArtMethod
* destMeth
= ( art
: : mirror
: : ArtMethod
* ) env
- > FromReflectedMethod ( dest
) ; srcMeth
- > declaring_class_
= destMeth
- > declaring_class_
; srcMeth
- > method_index_
= destMeth
- > method_index_
;
}
ArtMethod
4、ArtMethod是什么?
ArtMethod記錄著Java方法的所有信息:所屬類、訪問權限、代碼執行地址等等。
5、字段declaring_class就是方法所屬的類
類Student的test()方法的declaring_class就是Student.class
為什么替換ArtMethod的內容就能實現熱修復?
6、為什么替換了原Java方法對應的ArtMethod的內容就能實現熱修復?虛擬機調用方法的原理?
Android6.0,art虛擬機中ArtMethod的結構如下:包含方法的執行入口
@art / runtime
/ art_method
. h
class ArtMethod FINAL
{ void * entry_point_from_interpreter_
; void * entry_point_from_quick_compiled_code_
;
}
Java代碼在Android中被編譯為Dex Code,art中可以采用解釋模式或者AOT機器碼模式執行 解釋模式: 執行方法時,取出ArtMethod的entry_point_from_interpreter_的方法執行入口地址,跳轉過去執行。 AOT機器碼模式: 執行方法時,取出ArtMethod的entry_point_from_quick_compiled_code_的方法執行入口地址,跳轉過去執行。 簡單的替換entry_point_*字段表明的入口地址,不能夠實現方法的替換。 因為運行期間還會用到ArtMethod里面的其他成員字段 即使是AOT機器碼模式,編譯出的AOT機器碼的執行構成,依舊會有對ArtMethod很多成員字段的依賴 結論:只有替換掉所有原ArtMethod中的成員字段,在所有執行到舊方法的地方,才能完整獲取到所有新方法的信息: 執行入口、所屬class、方法索引號、所屬dex信息等,完美地去跳轉到新方法。
解釋模式
7、什么是解釋模式執行
取出 DEX Code 逐條解釋執行。
AOT機器碼模式
8、說什么是AOT機器碼模式
預先編譯好Dex code對應的機器碼,運行時直接運行機器碼
ArtMethod的兼容性問題
9、AndFix等Hook方案采取的native替換的方法都具有不穩定性
使用的ArtMethod結構完全根據Android源碼中ArtMethod的結構寫死的。 一些廠商修改了ArtMethod的內容和結構就會導致熱修復失效---兼容性很差
ArtMethod的整體替換
10、native替換方法的兼容性的解決辦法
原native替換方法是替換ArtMethod的所有成員,因此需要依賴具體結構。 解決辦法:不構造出ArtMethod具體的成員字段,將ArtMethod進行整體替換
memcpy ( srcMeth
, destMeth
, sizeof ( ArtMethod
) ) ;
ArtMethod的精確尺寸
11、整體替換ArtMethod的核心在于如何精確計算出sizeof(ArtMethod)
該整體替換ArtMethod的方案,在于如果ArtMethod的size計算有偏差,會導致:部分成員沒有替換、替換區域超出了邊界 應用開發者無法知道具體Andorid設備的系統里ArtMethod的尺寸 通過class_linker.cc源碼中LoadClassMembers()->AllocArtMethodArray()中可以知道ArtMethod Array(數組)的ArtMethod是緊密相連的。通過相鄰兩個ArtMethod的起始地址的差值就是ArtMethod的精準大小 類方法分為Direct方法和Virtual方法,各自有各自的ArtMethod數組 direct方法: static方法和所有不可繼承的對象方法 virtual方法: 所有可以繼承的對象方法
12、借助ArtMethod緊密相連的特性,如何精準計算出ArtMethod的大小?
構造一個輔助的類,并具有兩個空方法: f1()、f2()都是static方法,都屬于direct ArtMethod Array NativeStructsModel中只有這兩個方法,因此肯定是相鄰的
public class NativeStructsModel { final public static void f1 ( ) { } final public static void f2 ( ) { }
}
在JNI層計算出f1()和f2()地址的差值。
size_t firstMid
= ( size_t
) env
- > GetStaticMethodId ( nativeStructModelClazz
, "f1" , "()V" ) ; size_t secondMid
= ( size_t
) env
- > GetStaticMethodId ( nativeStructModelClazz
, "f2" , "()V" ) ; size_t methodSize
= secondMid
- firstMid
;
該Size就可以直接作為ArtMethod的尺寸
memcpy ( srcMeth
, destMeth
, methodSize
) ;
線性結構不能變
13、利用技巧獲取到ArtMethod尺寸的優缺點
優勢:對于所有Android版本都不需要區分 注意點:只要ArtMethod數組依舊是線性結構,無論ArtMethod的成員如何改變,都完美兼容。 ArtMethod數組的線性結構會被修改的可能性極低!
訪問權限檢查
方法調用時的權限檢查
14、只替換ArtMethod的內容,被替換的方法有權限訪問該類的其他private方法嗎?
可以 在dex2oat生成AOT機器碼時已經做過檢查和優化,因此機器碼中不存在權限檢查 例如下面:即使func()方法偷梁換柱為其他方法,依舊可以調用private的func()
public class Demo { Demo ( ) { func ( ) ; } private void func ( ) { }
}
同包名下的權限問題
15、補丁中的類在訪問同包名下的類時,會出現訪問權限異常:
具有類com.patch.demo.BaseBug和com.path.demo.MyClass是同一個包com.patch.demo下面的。 此時替換了com.patch.demo.BaseBug的方法test,因為該方法的ArtMethod被完全替換,因此指向的是新的補丁類。 該補丁包中的BaseBug是補丁包的Classloader加載的,和原先的包不是同一個Classloader,判定為不同包。BaseBug.test()中訪問MyClass類,會導致提示無法訪問com.path.demo.MyClass。 校驗邏輯在虛擬機代碼的Class::IsInSamePackage中:會要求Classloader必須相同
16、只需要設置new Class的Classloader為old Class的Classloader就可以解決該問題:
不需要在JNI層處理底層的結構 只需要通過反射進行設置
Field classLoaderField
= Class
. class . getDecalredField ( "classLoader" ) ; classLoaderField
. setAccessible ( true ) ; classLoaderField
. set ( newClass
, oldClass
. getClassLoader ( ) ) ;
反射調用非靜態方法
17、非靜態方法被熱替換后,再反射調用該方法,會拋出異常。
1-下面會報錯: 新BaseBug的test()傳入舊BaseBug,不匹配就會報錯。
BaseBug bb
= new BaseBug ( ) ; Method testMeth
= BaseBug
. class . getDeclaredMethod ( "test" ) ; testMeth
. invoke ( bb
) ;
2- invoke()->InvokeMethod()->VerifyObjectIsClass(): 會檢測Method.invoke()參數傳入的目標對象(舊類的對象),是否是方法對應的ArtMethod所屬的Class(新類)。
inline bool
VerifyObjectIsClass ( Object object
, Class
* c
) { if ( UNLIKELY ( ! object
- > InstanceOf ( c
) ) ) { return flase
; }
}
靜態方法不會有該問題
18、靜態方法為什么不會有該問題?
是在類的級別直接調用的,不會接受對象實例作為參數,也不會有該方面的檢查。
解決辦法
19、非靜態方法被熱替換后,再反射調用該非靜態方法,會拋出異常。解決辦法是:
冷啟動機制
即時生效的限制
20、即時生效這種運行期間修改底層結構的方案具有的限制有哪些?
只能支持方法的替換:已存在類的方法增/減和字段增/減都不適用 反射調用非靜態方法會拋出異常
21、哪些場景是支持的?
方法的替換 新增一個完整的,原先包里不存在的新類
22、優點
一旦符合使用條件,性能極佳,補丁小,加載迅速
23、不滿足即時生效的場景該如何如何處理?
冷啟動修復
熱修復與Java(68題)
內部類編譯
1、外部類有個方法,將其修改為訪問內部類的某方法,會導致補丁包新增一個方法。
2、內部類在編譯期會被編譯為跟外部類一樣的頂級類
靜態內部類和非靜態內部類的區別
3、靜態內部類和非靜態內部類的區別
靜態內部類不持有外部類的引用 非靜態內部類會持有外部類的引用 例如: handler的實現需要采用靜態內部類,避免OOM
4、非靜態內部類編譯時會增加字段this用于持有外部類的引用
5、持不持有外部類引用,都不影響熱部署。
都是一個頂級類,新增一個頂級類,不影響熱部署
內部類和外部類的互相訪問
6、內部類和外部類都是頂級類,是否就表示對方private的內容無法被訪問到?
外部類需要訪問內部類的private 域/方法,編譯期間會為內部類生成access&**相關方法。 內部類需要訪問外部類的private 屬性/方法,編譯期間會為外部類生成access&**相關方法。
熱部署(底層替換方案)
7、補丁前的test()沒有訪問內部類的private屬性/方法, 補丁后的test()訪問了內部類的private屬性/方法,會導致無法使用熱部署/底層替換方案
會新增access&**相關方法,按照限制,在原有類中增加方法,因此無法熱部署 只要避免生成access&**相關方法,就能走熱部署。
8、如何避免編譯器自動生成access&**相關方法
如果一個外部類有內部類: 把外部類所有private屬性/方法的訪問權限更改為其他權限(public、protected、default) 把內部類所有private屬性/方法的訪問權限更改為其他權限(public、protected、default)
匿名內部類編譯
9、匿名內部類在避免新增access&**方法的基礎上,依舊新增了一個內部類和新增了method方法
熱部署允許新增一個類 熱部署不允許新增方法
編譯期的命名規則
10、匿名內部類的名字格式是外部類&+數字
下例中:Thread的匿名內部類,編譯期的名字為:Demo&1
public class Demo { public static void test ( ) { new Thread ( ) { } . start ( ) ; } }
此時有兩個頂級類
11、原有的匿名內部類前插入新的匿名內部類會導致混亂
下例中:有兩個匿名內部類 Demo&1 — Callback.OnClickListener Demo&2 — Thread 補丁會比較新的Demo&1和舊的Demo&1, 然而這兩者完全不同。 會新增OnClick()方法 — 影響熱部署(Demo&1中增加了新方法,刪減了舊方法) 會新增一個匿名內部類 — 不影響,新增類沒事(Demo&2)
public class Demo { public static void test ( ) { new Callback. OnClickListener { public void onClick ( ) { } } new Thread ( ) { } . start ( ) ; } }
熱部署方案
12、在新增/減少匿名內部類時,如何支持熱部署方案?
唯一情況:增加的匿名內部類必須插入到外部內末尾 其余情況:無解,補丁工具無法區分。
域編譯
靜態field初始化/靜態代碼塊
13、熱部署不支持clinit的修復
熱部署不支持method/field的新增 熱部署不支持clinit的修復
14、clinit在Dalvik虛擬機中類加載的時,進行類初始化時調用。
15、靜態field初始化和靜態代碼塊會被編譯到clinit方法中
該方法由編譯器自動合成
16、靜態field初始化和靜態代碼塊在clinit中的順序取決于代碼中出現的先后順序
17、最常見的三種會去加載類的情況
new一個類對象(new-instance指令) 調用類的靜態方法(invoke-static指令) 獲取類的靜態field的值(sget指令)
18、類沒有被加載過時, 加載的流程
dvmResolveClass() dvmLinkClass() dvmInitClass(): 先對父類進行初始化,再調用本類的clinit()
非靜態field初始化/非靜態代碼塊
19、非靜態field初始化/非靜態代碼塊會被編譯到init無參構造函數中,順序和源碼中一致
20、構造函數會自動編譯成init方法
熱部署方案
21、任何靜態field初始化和靜態代碼塊的變更都會編譯到clinit中,無法熱部署,只能冷啟動(處于類加載的初始化期間)
22、非靜態field初始化和非靜態代碼塊的變更都會編譯到init中,只被當作一個普通方法的變更,對熱部署無影響(普通的方法)
final static field編譯
23、final static修飾的field編譯時是否會編譯到clinit中?
作為靜態域,應該都被編譯到clinit中,但是并不完全正確 修飾的基本類型/String常量類型,不會編譯到clinit中
24、下例中類中的field哪些會被編譯到clinit方法中?哪些不會?
public class Demo { static Object o1
= new Object ( ) ; final static Object o2
= new Object ( ) ; static int i1
= 1 ; final static int i2
= 2 ; final static String s1
= new String ( "new String" ) ; final static String s2
= "常量" ;
}
finalt static修飾的基本類型和String常量類型不會編譯到clinit中
25、final static修飾的基本類型/String常量類型是在哪里初始化的?
類加載初始化的dvmInitClass在執行clinit之前,調用initSFields對static域設置默認值。 initSFields設置默認值的目標包括靜態域的所有引用類型/基本類型/String常量類型,但是基本類型/String常量類型在后面的clinit中就不會設置了
static和final static修飾的區別
26、static和final static修飾的區別
final static修飾的原始類型和String類型(非引用類型)的field,不會編譯到clinit中,會提前在類初始化執行的initSField中進行初始化賦值。 final static修飾的引用類型和static修飾的所有類型,仍然在clinit中初始化
final static優化原理
27、對于常量使用final static修飾就能達到優化效果?
錯誤!
只有final static修飾的原始類型和tring類型常量才能得到優化。
28、final static進行優化的原理
可以優化的情況中:要訪問該常量通過const/4指令實現,該指令非常簡單 不可優化的情況中:訪問這些field,通過sget指令。內部包含解析,解析類等操作,屬于重操作。
29、final對于final static修飾的引用類型的唯一作用就是避免該field被修改
熱部署方案
30、final static修飾的field如何進行熱部署?
可以熱部署: 基本類型: 引用該基本類型的地方都會被立即數替換 String常量:所有引用該常量的地方都被常量池索引id替換 熱部署中將所有引用到該final static field的方法都進行替換,走熱部署沒問題。 不可熱部署: final static修飾的引用類型都被翻譯到clinit中,不會熱部署。
方法編譯
混淆
31、混淆可能導致方法內聯和裁剪,而導致method的增減
方法內聯
32、哪些場景會導致方法內聯?
方法沒有被其他任何地方引用 方法足夠簡單,例如只有一行,會在任何調用該方法的地方用該方法的實現進行替換 方法只有一個地方引用到,會在調用處用實現進行替換
33、方法內聯為什么會導致方法的增減?以及導致熱部署失效?
原Class中具有一個test()方法,因為內聯,所以編譯后不再有test()方法 新Ckass中,因為不滿足內聯的條件導致tets()不被內聯,因此多出來test()方法 前后對比,因為新增方法導致不能熱部署,只能冷啟動 反過來方法內聯也會導致方法的減少
方法裁剪
34、方法裁剪
test(context)方法中由于context參數沒有被使用到,因此混淆任務會先生成裁剪過后無參的test()方法,然后再進行混淆。 如果新代碼中,正好使用了參數,不會導致方法裁剪,因此會新增一個具有參數的test(context)方法 方法裁剪導致方法增減,導致不嗯呢剛熱部署
35、如何避免方法裁剪?
保證所有參數被使用,或者進行特殊處理:
public void test ( Context context
) { if ( Boolean
. FALSE
. booleanValue ( ) ) { context
. getApplicationContext ( ) ; }
}
代碼規范
熱部署方案
36、如何避免混淆時的方法內聯和方法裁剪導致熱部署失效的問題?
混淆配置文件中加上配置項-dontoptimize就可以關閉方法的裁剪和內聯
37、混淆庫的預編譯會拖累打包速度,Android虛擬機有自己的一套代碼校驗邏輯
需要加上配置項-dontpreverify
switch case語句編譯
38、資源修復方案中需要對新舊ID進行替換,但是switch case中的id不會被替換
39、switch case 語句編譯實例中解析編譯規則
1-第一個方法較為連續。第二個方法不連續. 2-第一個testContinue()方法中,因為1、3、5連續,使用指令packed-switch,會影響熱部署 3-第二個testNotContinue()中,1、3、10不連續,使用指令sparse-switch
public void testContinue ( ) { int temp
= 2 ; int result
= 0 ; switch ( temp
) { case 1 : result
= 1 ; break ; case 3 : result
= 1 ; break ; case 5 : result
= 1 ; break ; }
} public void testNotContinue ( ) { int temp
= 2 ; int result
= 0 ; switch ( temp
) { case 1 : result
= 1 ; break ; case 3 : result
= 1 ; break ; case 10 : result
= 1 ; break ; }
}
熱部署方案: 反編譯
40、為什么資源id替換不完全?如何解決?
資源id肯定是const final static變量,導致switch case被翻譯成packed-switch指令 采用方案:反編譯(強行替換指令) -> 資源id替換 -> 重新編譯 修改反編譯流程: 遇到packed-switch指令就強轉為sparse-switch指令;:pswitch_N等標簽指令強轉為:sswitch_N指令 資源ID的暴力替換 重新編譯為Dex
泛型
41、泛型可能會導致method的新增
為什么需要泛型
42、Java中的泛型完全在編譯器中實現
由編譯器執行類型檢查和類型推斷 然后生成普通的無泛型的字節碼。泛型知識為了保證類型安全。 這種技術就是擦除(erasure)
43、Java的泛型為什么要采用擦除技術來實現?
泛型從Java5才引入 通過擴展虛擬機指令集來支持泛型是不可以的,也會導致升級JVM具有很多障礙
Object實現泛型
44、Object實現泛型
public class ObjectGeneric { private Object obj
; public void setValue ( Object value
) { obj
= value
; } public Object
getValue ( ) { return obj
; } public static void main ( String args
[ ] ) { ObjectGeneric generic
= new ObjectGeneric ( ) ; generic
. setValue ( true ) ; boolean bool
= ( boolean ) generic
. getValue ( ) ; int n
= ( int ) generic
. getValue ( ) ; }
}
上面1和2在編譯期間都不會報錯,因為符合Java語法。 但是在實際運行中,2會出現java.lang.ClassCastException的異常:
Exception in thread "main" java.lang.ClassCastException: java.base/java.lang.Boolean cannot be cast to java.base/java.lang.Integerat ObjectGeneric.main(ObjectGeneric.java:16)
45、Java5泛型提出之前采用Object實現該效果,但是會導致編譯器無法檢測出類型不匹配的問題
泛型在編譯時就進行類型安全檢測
泛型
46、泛型會在編譯期間進行檢查, 實例:
public class ObjectGeneric < T> { private T obj
; public void setValue ( T value
) { obj
= value
; } public T
getValue ( ) { return obj
; } public static void main ( String args
[ ] ) { ObjectGeneric
< Boolean> generic
= new ObjectGeneric ( ) ; generic
. setValue ( true ) ; boolean bool
= ( boolean ) generic
. getValue ( ) ; }
}
情況2獲取到Int值,會報錯。
類型擦除
47、下面的例子中的類型擦除:
方法setValue(T value)會被處理為setValue(Object value), 因此編寫一個方法為setValue(Object value)會報錯! 泛型設置類具體類型<Integer>本質在字節碼中生成的還是Object類型的參數,只是利用這個進行了類型檢查。
public class ObjectGeneric < T> { private T obj
; public void setValue ( T value
) { obj
= value
; } public void setValue ( Object value
) { obj
= value
; } }
類型擦除和多態的沖突
48、類型擦除會導致本來是想重寫,結果變成了重載
setValue(T value)在字節碼上是setValue(Object obj) 結果setValue(Integer value)是對父類setValue(Object obj)的重載 然而需要的效果是setValue(Integer value)是對父類setValue(T obj)的重寫
public class ObjectGeneric < T> { private T obj
; public void setValue ( T value
) { obj
= value
; } public T
getValue ( ) { return obj
; } class B extends ObjectGeneric < Integer> { private Integer n
; public void setValue ( Integer value
) { n
= value
; } }
}
49、使用 @Override能實現重寫
class B extends ObjectGeneric < Integer> { private Integer n
; @Override public void setValue ( Integer value
) { n
= value
; } }
bridge
50、編譯器會自動合成bridge方法來實現重寫的效果
編譯器自動生成一個setValue(Object Value)來重寫父類的該方法
class B extends ObjectGeneric < Integer> { private Integer n
;
@Override public void setValue ( Integer value
) { n
= value
; }
}
51、虛擬機是通過參數類型+返回類型來確定一個方法,和Java語言規則不同。
該方法用于解決泛型中類型擦除和多態的沖突問題
52、泛型的隱形類型轉換,編譯器會自動加上check-cast類型轉換
不需要程序員進行顯式地類型轉換,而是自動進行類型轉換。
public static void main ( String args
[ ] ) { ObjectGeneric
< Boolean> generic
= new ObjectGeneric ( ) ; generic
. setValue ( true ) ; boolean bool
= generic
. getValue ( ) ; }
熱部署的方案
53、泛型對熱部署的影響
類型擦除的過程中可能會新增bridge方法,導致熱部署失敗 另一方面泛型方法內部會生成一個dalvik/annotation/Signature的系統注解,方法邏輯沒出現變化,但是該方法的注解發生了變化。補丁工具進行判斷會走熱部署進行修復,然而并沒有什么意義(方法邏輯沒有變化,根本不需要修復)
Lambda表達式編譯
54、Lambda表達式簡介
Java 7 才引入的一種表達式 類似匿名內部類,卻有巨大的區別 會導致方法的增減,影響熱部署
55、函數式接口的兩大特征
是一個街口 具有唯一的一個抽象方法 典型的函數式接口Runnable和Comparator
和匿名內部類的區別
56、Lambda表達式和匿名內部類的區別?
關鍵字this: 匿名內部類的this指向匿名類 lambda表達式的this指向包圍lambda表達式的類 編譯方式: 編譯器將匿名內部類編譯成新類,名稱為外部類名+&number 編譯器將lambda表達式編譯成類的私有方法,使用Java7的invokedynamic字節碼指令進行動態綁定該方法
invokedynamic
57、實例解析lanmbda表達式
編譯期間會自動生成私有靜態的lambda$test$ + number(參數類型)的方法 invokedynamic執行lambda表達式 相比于匿名內部類,不會生成外部類名 + & + number的新類。
public class Test { public static void test ( ) { new Thread ( ( ) - > { } ) . start ( ) ; }
}
metafctory
58、invokedynamic指令簡介
java7新增,用于支持動態語言:允許方法調用可以在運行時指定類和方法,不需要編譯時確定。 每個invokedynamic指令出現的位置被稱為動態調用點 invokedynamic指令后會跟著一個指向常量池的調用點限定符(#3, #6) 調用點限定符會被解析為一個動態調用點 invokedynamic指令最終會去執行java.lang.invoke.LambdaMetafactory類的靜態方法: metafctory(),該方法會在運行時聲稱一個實現函數式接口的具體類 該具體類-例如: Test$$Lambda$1.java 會調用私有靜態方法: lambda$test$ + number(參數類型),執行lambda表達式的邏輯
final class Test $$Lambda$
1 implements Runnable { @Hidden public void run ( ) { Test
. lambda$test$
0 ( ) ; }
}
Android虛擬機中的lambda
59、Android虛擬機下是如何解釋lambda表達式的?
android虛擬機首先通過javac把源代碼.java編譯成.class,在通過dx工具優化成適合移動設備的dex字節碼文件 android中如果要使用java8語言特性,需要使用新的jack工具鏈來替代老的工具鏈進行編譯 Jack會將.java文件編譯成.jack文件,最后直接編譯成.dex文件(Dalvik字節碼文件)
60、構建Android Dalvik可執行文件可使用的兩種工具鏈對比
舊版javac工具鏈 javac(.java -> .class)->dx(.class ->. dex) 新版Jack工具鏈 Jack(.java -> .class -> .dex)
Jack
61、Jack是什么?
Java Android Compiler Kit
62、Jack工具鏈中處理lambda的異同
相同點: 編譯期間都會為外部類合成一個static輔助方法,內部邏輯就是lambda表達式的內容 不同點: 老版本中通過invokedynamic指令執行lambda; Jack的.dex中執行lambda表達式和普通方法調用沒有區別 老版本是在運行中生成新類;Jack是在編譯期間生成新類
熱部署方案
63、Lambda表達式會導致熱部署失效的原因
方法的增減: 新增一個lambda表達式,會導致外部類新增一個輔助方法 順序混亂: 合成類的命名規則 = “外部類雷鳴 + Lambda + Lambda所在方法的簽名 + LambdaImpl + 出現的序號”,和匿名內部類一樣的問題
64、不增減lambda表達式,不改變lambda表達式的順序,只是更改Lambda原有內部邏輯,能否走熱部署?
在一定情況下,依舊會出問題,不能走熱部署:
如果lambda表達式訪問外部類非靜態的field和method 編譯期間在.dex文件中會自動生成新的輔助類(Test$$Lambda$1.java), 該類沒有持有外部類的引用 為了訪問非靜態的field和method,會導致需要持有外部類的引用,從而增加一個字段來持有 輔助類的field的增減導致無法熱部署
final class Test $$Lambda$
1 implements Runnable { @Hidden public void run ( ) { Test
. lambda$test$
0 ( ) ; }
}
65、Lambda表達式對熱部署影響的總結
增加/減少一個lambda表達式會導致類方法的錯亂。熱部署失敗! 修改一個原有lambda表達式,因為可能訪問/取消訪問外部類的非靜態field和method的情況,可能導致輔助類的field的增加/減少。熱部署失敗! 調整原有lambda表達式的順序,會導致類方法的錯亂。熱部署失敗!
訪問權限檢查
66、一個類的加載必須經歷resolve、link、init三個階段
類加載階段
67、類加載階段中對父類和當前類實現的接口的權限檢查主要在link階段
如果當前類、實現的接口、父類是非public的,并且加載兩者的classLoader不一樣的情況,直接return 代碼熱修復方案是基于新classLoader的,類加載階段就會報錯
類校驗階段
68、如果補丁類中存在非public類的訪問、非public方法的調用、非public field的調用都會導致失敗
這些錯誤在補丁加載階段是檢測不出來的,補丁會被視作正常加載 直到運行階段,會直接crash
冷啟動類加載原理(26題)
1、冷啟動方案的作用?
熱部署有很多限制 在超出限制的情況下,再通過冷啟動進行補充,使得熱修復一定能成功。
傳統實現方案
Tinker
2、Tinker如何實現冷啟動的?
提供Dex差量包,并整體替換Dex的方案。 通過差量的方式生成patch.dex(補丁dex文件),然后將patch.dex和應用的classes.dex合并成一個完整的dex 加載新dex文件得到dexFile對象并以此構造出Element對象,然后整體替換掉舊的dex Elements數組
3、Tinker方案的優點
自研dex差異算法,補丁包小,不影響類加載性能。
4、Tinker方案的缺點
dex合并,在VM Heap上消耗內存,容易OOM,導致dex合并失敗
5、Tinker如何避免OOM導致的dex合并失敗的問題?
可以在jni層面進行dex的合并,從而避免OOM導致dex合并失敗 但是JNI層實現比較復雜。
插樁實現
6、如果僅僅把補丁類打入補丁包中而不做任何處理會出什么問題?該問題是啥意思?
運行時類加載的時候會異常退出
dexopt
odex
7、加載一個dex文件到本地內存的流程
如果不存在odex文件,首先會執行dexopt dexopt的入口在davilk/opt/OptMain.cpp的main方法 最后調用verifyAndOptimizeClass進行真正的verify(驗證)和optimize(優化)操作
8、dexopt的流程
verifyAndOptimizeClass
9、Apk第一次安裝時的流程
對原Dex執行dexopt->執行到verifyAndOptimizeClass() 會先進行類校驗-dvmVerifyClass(): 校驗成功,則所有類都會打上CLASS_ISPREVERIFIED標志 接著執行類優化-dvmOptimizeClass(),并且打上CLASS_ISOPTIMIZED標志
dvmVerifyClass
10、dvmVerifyClass()方法的作用
類校驗,目的是: 防止類被篡改校驗類的合法性 會對類的每個方法進行校驗,類的所有方法中直接引用的類和當前類都在同一個dex中:return true
dvmOptimizeClass
11、dvmOptimizeClass()方法的作用
類優化,將部分指令優化成虛擬機的內部指令 例如: 方法調用指令 1. invoke-*指令變成了invoke-*-quick指令 1. quick指令直接從vtable表中取,該表是類的所有方法的表(包括繼承的方法),加快了方法的執行速度
dvmResolveClass
12、加載階段中為什么會出現dvmThrowllegalAccessError(運行時異常)?
原Dex中的類B中的某個方法引用到補丁包中的類A 執行到該方法時,會嘗試解析類A: 類B具有CLASS_ISPREVERIFIED標志 然后判斷類A和類B所屬的dex,因為不同,拋出異常dvmThrowllegalAccessError
13、為什么原Dex類B能引用到補丁類A的方法?明明沒打補丁前,都不知道有這個補丁類A?
補丁類A作為補丁,說明原包中肯定有一個原始類A
插樁
14、如何解決dvmThrowllegalAccessError問題?
構造一個單獨沒啥用的幫助類放到一個單獨的Dex中 原Dex中所有類的構造函數都引用這個類 這里需要侵入dex打包流程,利用.class字節碼修改技術,在所有.class文件的構造函數中引用該幫助類 在加載Dex文件時,會走dexopt流程,在dvmVerifyClass校驗時,校驗失敗(類B的所有方法中引用到的類-幫助類,和類B不在一個Dex中)。原dex中所有類沒有CLASS_ISPREVERIFIED標志。并且后續流程也不走,不會打上CLASS_ISOPTIMIZED 因此引用到補丁類A時,解析類A,不會進入CLASS_ISPREVERIFIED標志的后續判斷,也不會拋出異常dvmThrowllegalAccessError
插樁導致類加載性能差
15、插樁為什么會導致類加載的效率很低?
類的加載需要三個階段:dvmResolveClass->dvmLinkClass->dvmInitClass 如果類因為插樁沒有打上CLASS_ISPREVERIFIED和CLASS_ISOPTIMIZED標志,在類的初始化階段,還會重新進行類的verify(驗證)和optimize(優化) 原來驗證和優化操作只有在第一次apk安裝執行dexopt時,才會進行。結果如今每次進行類加載時,都會重復處理,過多的類加載同時進行,性能消耗會更大。
插樁具體性能影響
16、插樁技術對性能影響的具體測試數據
整體上有8~9倍的性能差距 應用啟動上,容易導致白屏。
不插樁插樁 加載700個類 84ms 685ms 啟動應用耗時 4934ms 7240ms
避免插樁的手Q方案
17、手Q方案中避免插樁的思路是什么?
避免在dvmResolveClass中走校驗dex一致性的流程. 也就是提前將補丁類加入到數組中,讓其能直接返回補丁類
void dvmResolve ( ) { ClassObject patchClass
= null
; patchClass
= dvmDexGetResolved ( xxx
) ; if ( patchClass
!= null
) { return patchClass
; } }
18、手Q方案的缺陷?
在dexopt后進行繞過的,dexopt會改變原先的很多邏輯 odex層面的優化會寫死字段和方法的訪問偏移,就會導致嚴重的BUG
ART下冷啟動實現
Dalvik和Art加載dex分解的區別
19、Dalvik在嘗試加載一個壓縮文件的時候只會把classes.dex文件加載到內存中
如果壓縮文件中有多個dex文件,除了classes.dex文件,其他的dex文件都會被無視
20、Art支持壓縮文件中包含多個dex的加載問題
會優先加載classes.dex文件 然后在按順序加載classes2.dex、classes3.dex文件 如果多個dex中有同一個類,只有第一個出現的類才會被加載,不會重復加載
Art中的方案
21、Art中進行冷啟動的方案
把補丁dex文件命名為classes.dex 原apk中的dex依次命名為classes(2,3,4...).dex,并一起打包為一個壓縮文件。 再通過DexFile.loadDex()得到DexFile對象,并將其整個替換舊的dexElements數組即可
22、Art冷啟動方案的注意點
補丁dex必須命名為classes.dex loadDex得到的新DexFile必須完全替換掉dexElements數組,而不是插入
Tinker方案的比較
23、Tinker的冷啟動方案和Sophix新方案的比較圖
odex和dex
24、虛擬機真正執行的是dex文件嗎?
DexFile.loadDex()會嘗試將dex文件解析并加載到native內存中 如果native內存中不存在dex對應的odex,Dalvik和Art分別通過dexopt、dexoat得到一個優化后的odex VM真正執行的是odex還不是dex
25、patch不定的安全性如何保證?
對補丁包進行簽名校驗,能保證補丁包不被篡改。 但是虛擬機執行的是odex文件,而不是dex文件,還需要對odex文件進行md5完整性校驗,防止odex被篡改。
完整的方案
26、Dalvik和Art中完美兼容的冷啟動方案
代碼采用同一套,不會根據Dalvik和Art分開處理。 Dalvik: 采用自行研發的全量Dex方案 Art:本身支持多Dex加載,只需要改名即可。
多態對冷啟動類加載的影響(16題)
多態
1、多態是如何實現的?(利用的是什么技術?)
實現多態的技術是動態綁定 動態綁定是指,在執行期間判斷所引用對象的實際類型,根據實際類型調用對應方法
2、field和靜態方法不具備多態性
Field如下, static方法同理:
class A { String name
= "SuperClass" ;
} public class B extends A { String name
= "B" ;
} A obj
= new B ( ) ;
System
. out
. println ( obj
. name
) ;
3、非靜態非private方法才具有多態性
方法多態性的實現
4、方法多態性的實現流程:
People p
= new Man ( ) ;
p
. talk ( ) ;
p.talk();通過指令invokeVirtual執行 調用p的方法talk(),會拿到該talk()在父類People的vtable中的索引(methodIndex) 然后在子類Man的vtable[methodIndex]中得到虛方法talk,并且執行。 構成了多態
Virtual方法
5、Virtual方法是什么?
Virtual方法就是當前類和繼承自父類的所有方法中,為public/protected/default的方法
6、類加載時會創建vtable
new B()時會加載類B: 方法調用鏈:devmResolveClass -> dvmLinkClass -> createVtable() createVtable(): 創建vtable,存放當前類所有Virtual方法
7、createVtable的流程
復制父類的vtable到子類的vtable 遍歷子類的virtual方法集合: 方法原型一致,表明是重寫父類方法,在相同索引處,用子類方法覆蓋原有父類的方法 方法原型不一致,將子類該方法添加到vtable末尾
invokeVirtual
field/static方法不具有多態
8、為什么field/static方法不具有多態性?
iget/invoke-static(虛擬機指令)是直接在引用類型中查找,而不是從實際類型中查找 如果找不到,再去父類中遞歸查找
冷啟動方案的限制
9、如果新增了一個public/protected/default方法會出現什么情況?
class A { void method1
{ } void method2
{ }
} public class Demo { public static void test_addMethod ( ) { A obj
= new A ( ) ; obj
. method2 ( ) ; }
}
打補丁前: 調用方法-method2 打補丁后:調用方法-method1
類優化(dvmOptimizeClass)
10、類優化階段時對虛方法調用的影響
dex文件第一次加載時,會執行dexopt: verify + optimize 類優化階段時,會將invoke-virtual指令替換為invoke-virtual-quick指令
11、invoke-virtual-quick指令為什么會提高方法的執行效率?
-quick指令后面跟著該方法在類vtable中的索引值 會直接從類的vtable中取出方法,加快執行效率 節省拿到索引值的流程
12、invoke-virtual指令的方法調用的流程?
多了在引用類型的vtable中的索引的步驟 然后才到子類的vtable中取出方法
13、為什么新增了public/protected/default方法會出現方法調用錯亂?
上例分析:
obj.method2()對應的-quick指令保存的索引值是0,對應vtable[0] 補丁前: vtable[0] = method2 補丁后: vtable[0] = method1, vtable[1] = method2 最終導致方法調用錯亂。
終極方案
插樁方案的失敗
14、插樁方案為什么不能采用?
通過Art和Dalvik的冷啟動方案,能對補丁類進行加載,但是在運行時類加載的時候會出現dvmThrowllegalAccessError異常 采用插樁方案能處理該問題,但是性能極差
非插樁手Q方案的失敗
15、手Q的非插樁方案的為什么不能采用?
通過非插樁的方法來繞過dex一致性檢查,雖然不會拋出異常. 但是在多態的情況下因為dexopt的優化導致方法調用錯亂。
完整DEX方案
16、需要采用類似Tinker的完整Dex方案
google開源的dexmerge方案能將補丁dex和原dex合并成一個完整的dex 會出現多dex下方法數超過65535的異常 dexmerge占用內存,且內存不足時有可能會失敗。 在移動端需要合成完整的Dex,實現較為復雜。
Dalvik中全量Dex方案(16題)
冷啟動類加載修復
1、Android的冷啟動類加載方案是如何實現的?
把新dex插入到ClassLoader索引路徑的最前面 在load一個class時,優先加載補丁中的類。
2、遇到的pre-verify問題
一個類中直接引用到的所有非系統類都和該類在同一個dex中,該類會被打上CLASS_ISPREVERIFIED標志 具體判定代碼在虛擬機中的verifyAndOptimizeClass函數
3、騰訊三大熱修復方案如何解決CLASS_ISPREVERIFIED導致的異常問題?
方案缺點 QQ空間 插樁。在每個類中都插入來自于一個特殊dex的hack.class,讓所有類都無法滿足pre-verified條件 性能差 Tinker 合成全量的Dex文件,所有class都在一個dex中,消除class重復的問題。 從dex的方法和指令的維度進行全量合成,比較粒度過細,實現復雜,性能消耗嚴重。 QFix 非插樁。利用虛擬機底層方法,繞過pre-verify檢查 1. 不能增加public函數
新的全量Dex方案
4、全量Dex方案
將原本基線包的dex里面去除掉補丁包中也有的class 補丁 + 去除補丁類的基線包 = 新app中所有類 不變的class需要用到補丁類的時候,自動地去找補丁dex 新補丁類需要用到不變的class時,直接去基線包dex中尋找 這樣沒用到補丁類的基線包class,繼續通過dexopt進行處理,最大的保證了效果
5、全量Dex方案的核心在于:如何在基線包的dex文件中去除掉補丁包中的所有類
從Dex Head中獲取到dex的各個重要屬性 對于需要移除Class,不需要將其所有信息都從dex移除,只需要移除定義的入口即可 不需要刪除Class具體內容
6、如何找到某個dex的所有類定義?虛擬機在dexopt過程中是如何找到的?
dexopt的verifyAndOptimizeClass()中通過dexGetClassDef()找到的類的定義(DexClassDef *) 內部是pHeader->classDefsOff偏移處開始,依次線性排列。
7、如何從基線包中刪除目標類的定義的入口
直接找到pHeader->classDefsOff偏移處,遍歷所有DexClassDef 如果類名包含在補丁中,就將該dexGetClassDef移除
8、Sophix是如何處理 CLASS_ISPREVERIFIED 問題的?
補丁dex文件在補丁壓縮包中,名稱為classes.dex,會將該dex加載到dexElements數組中 原apk的所有dex文件,都會被Dalvik生成DexFile加載到dexElements數組中 這樣所有類,都可以從所有dex中的某一個dex中找到。 loadDex加載刪除了補丁類的原apk的dex文件時,會重新dexopt生成odex文件(CLASS_ISPREVERIFIED)標志只有滿足條件的才會打上。 當原apk中的類引用到補丁類時,因為沒有CLASS_ISPREVERIFIED標志,不會出現dex一致性檢查而拋出異常的情況。
9、實例解析該全量Dex方案如何解決異常問題
錯誤場景:直接將補丁打入補丁包,不做額外處理 原本APK的dex中有類A、類B 現在類B有一個補丁類B 單純將補丁類打入補丁包時,此時APK的dex中有類A、類B,補丁包中有補丁類B 程序運行時會對Apk中的類A和類B,進行校驗和優化,類A引用了類B,且兩者位于同一個dex,給類A打上已經校驗的標志 后續進行了處理,讓類A引用類B時,能指向補丁類B。 類A引用類B時,根據類A的特殊標志,將類A和補丁類B的Dex進行校驗,dex不同,拋出異常。 Sophix場景: 原本APK中dex有類A、類B 打補丁后,原本APK的Dex中只有類A,補丁包中有補丁類B Apk重新運行時,dexopt校驗,類A引用補丁類B,但是兩者不在同一個dex中。校驗失敗 后續運行時,類A引用補丁類B,類A不具有特殊標志,不走檢查dex一致性的流程,直接走重新校驗和優化。
multidex的原理
10、multidex的原理
將一個apk中所有類拆分到classes.dex、classes2.dex、classes3.dex... 然后將dex文件都加載進去,在運行時遇到本dex不存在的類,可以到其他dex中找
對Application的處理
11、Application的處理
Application必然是加載在原來的老dex里面。 加載補丁后,如果Application類使用其他在新dex里的類,由于不在一個dex中,application如果被打傷了CLASS_ISPREVERIFIED標志,就會拋出異常 java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
12、如何處理Application的pre-verified標志問題?
將其標志位進行清除。 在JNI層清除:類的標志位于ClassObject的accessFlags成員中
claszzObj->accessFlags &= ~CLASS_ISPREVERIFIED;
dvmOptResolveClass的問題
13、如果入口Application沒有pre-verified,會有更嚴重的問題
Dalvik虛擬機發現某個類沒有pre-verified,會在初始化類的時候,進行Verify操作 會掃描類中使用到的所有類,并執行dvmOptResolveClass() 會在Application類初始化的時候,補丁還沒加載,提前加載到原始Dex中的類 補丁加載完畢后,已經加載的類如果用到新dex中的類,遇到pre-verified就會報錯
14、問題原因
無法把補丁加載提前到dvmOptResolveClass之前,也就是比入口Application初始化更早的時期。 常見于多dex的情形,存在多dex時,無法保證Application用到的類和其在同一個dex中
15、多dex情況下如何解決該問題
方法1:將Application用到的非系統類都和Application同一個dex里。保證具有pre-verified標志,補丁加載完畢后,再清除標志。 方法2: 1. 將Application中除了熱修復框架的代碼,都放到其他單獨類中,讓Application不直接調用非系統類。讓其處于同一個dex。 2. 保險起見,Application用反射訪問這個單獨類中,讓Application和其他類徹底隔絕。
16、Android multi-dex機制對方法1的支持
multi-dex機制會自動將Application用到的類都打包到主dex中 只要把熱修復初始化放在attachContext最前面,就OK。
資源熱修復技術(25題)
普通的實現方式
Instant Run
1、Instant Run中的資源熱修復的原理?
構造一個新的AssetManager. 反射調用addAssetPath,將這個完整的新資源包加入到新AssetManager中。 找到所有引用舊AssetManager的地方,通過反射,將引用處替換為新AssetManager-該Manager包含所有新資源
2、Instant Run的資源熱修復主要工作都是在處理兼容性和查找到AssetManager引用處,替換邏輯很簡單。
3、AssetManager是什么?
Android中有所資源包都通過AssetManager的addAssetPath()將資源路徑添加進去。 Java層的AssetManager只是一個包裝。底部navtive層由C++ AssetManager。
4、addAssetPath的解析流程
1. 通過傳入的`資源包`路徑,得到其中的`resources.arsc`1. 解析`resources.arsc`的格式1. 存放在底層AssetManager的`mResources`成員中
AssetManager
5、AssetManager的結構
一個Android進程只包含一個ResTable ResTable具有成員變量mPackageGroups: 包含所有解析過的資源包 任何一個資源包中都包含resource.arsc: 記錄所有資源的id分配情況和所有資源中的字符串 底層AssetManager就是解析該resource.arsc,將解析后的信息存儲到mPackageGroup中
class AssetManager : public AAssetManager
{ mutable ResTable
* mResoucres
;
}
class ResTable { Vector
< PackageGroup
* > mPackageGroups
;
}
資源文件的格式
resources.arsc
6、resources.arsc文件是什么?
實際上由一個個ResChunk拼接起來 從文件頭開始,每個ResChunk的頭部都是一個ResChunk_header結構,表明了ResChunk的大小和數據類型
ResChunk_header
7、resources.arsc的解析流程
通過ResChunk_header的type成員判斷出其實際類型,采用相應方法進行解析 解析完畢后,通過size成員,從ResChunk + size得到下一個ResChunk的起始位置 依次解析完整個文件的數據內容
資源id
8、resources.arsc中包含若干個package
package中包含所有資源信息 資源信息指: 資源名稱 資源ID
9、默認情況下有aapt工具打包出來的包只有一個package
10、資源id的是一個32位數字,可以通過aapt工具解析可以看到。
十六進制表示: 0xPPTTEEEE, 如: 0x7f 04 0019 PP是package id,如:0x7f TT是類型 id,如:0x04 EEEE是資源項id,如:0x0019
package id
11、package id是什么?
表明了是哪個資源包,如0x7f就是id = 0x7f的資源包
type id
12、type id是什么?
表明資源的具體類型。 依賴于Type String Pool-類型字符串池中具體的內容 例如池中依次是attr、drawable、mipmap、layout 0x04就表示是layout布局類型的資源
entry id
13、 entry id是什么?
資源項id,表明在 0x7f的資源包中,類型為0x04(layout)中,第0x0019的資源項
運行時資源的解析
14、默認的apk的資源包的package id是多少?
由Android SDK編譯出的apk,會經過aapt工具進行打包的,其package id就是0x7f
15、系統資源包是什么?
系統的資源包,就是framework-res.jar package id = 0x01
16、資源包重復加載導致的問題
app啟動時,系統會構建AssetManager并且將0x01和0x07的資源包添加進去 如果通過AssetManager.addAssetPath()添加補丁包的資源,會導致0x07資源包添加兩次,會導致的問題: Android 5.0開始,添加不會有問題,會默默將后來的包添加到原來的資源的同一個PackageGroup下面。讀取時,會發現補丁包中新增的資源會生效。修改原app的資源不會生效。 Android 4.4及以下,addAssetPath直接將補丁包的路徑添加到mAssetPath中,但不會進行加載解析,補丁包里面的資源會完全不生效。
資源修復方案
傳統方案
17、市面上的傳統的方案
對資源做差量包,運行時合成完整包再加載。 運行時多了合成的操作,耗時,占用內存 類似Instant Run的方案。 修改aapt,在打包時對補丁包資源進行重新編號。對于aapt等SDK工具包的修改,不利于日后的升級。
最佳方案
18、最佳方案
構造一個package id = 0x66的資源包,包含兩種資源:1.新增資源 2.原有內容發生改變的資源 直接在原有AssetManager中addAssetPath0x66資源包,不和已經加載的0x7f沖突 直接在原有的AssetManager對象上進行析構和重構。不再需要去找到所有引用AssetManager的地方
新增資源和id偏移
19、新增資源導致id的偏移
原來具有資源 0x7f020001(A圖片)、0x7f020002(B圖片) 新增資源后是,0x7f020001(A圖片)、0x7f020002(新圖片)、0x7f020003(B圖片)—新增資源的插入位置是隨機的,跟appt有關。 因為新增的資源是在0x66資源包中,打包工具需要更正id為: 1. 原資源保持不變: 0x7f020001(A圖片)、0x7f020002(B圖片) 1. 新增資源: 0x66020001(新圖片)
內容改變的資源
20、原有內容改變的資源需要代碼熱修復的配合
原來引用資源:setContentView(0x7f030000) 引用修改后的資源: setContentView(0x66020000) 需要將代碼中引用該id的方法進行修改,通過代碼熱部署修改引用的id
刪除的資源
21、刪除的資源如何處理
不需要處理 新代碼中沒有引用,自然用不到該資源。
對于type的影響
22、對補丁包中的Type String Pool需要進行修正
原來池中有: attr(0x01)、drawable(0x02) 因為attr的資源沒有變動,所以補丁包中只有: drawable 刪除池中的attr,保證0x01能引用到drawable: attr(0x01)
優雅地替換AssetManager
23、在Android 5.0開始,不需要替換AssetManager
只需要在AssetManager中add進入0x66的資源包即可
24、Android 4.4及以下的版本,需要替換AssetManager
這些版本調用AssetManager.addAssetPath不會加載資源,只會添加到mAssetPath中,不會解析資源包。 但是不需要和Instant Run一樣構造新的AssetManager,并且進行各種兼容和反射工作。
25、利用AssetManager的析構和構造方法,實現資源的真正加載。
先反射調用AssetManager的析構方法: 將Java層的AssetManager置為空殼(null) 反射調用構造方法,調用addAssetPath()添加所有資源包: 系統會自動加載解析所有add過的資源包。 對mStringBlocks置空并且重新賦值:該成員記錄了所有加載過的資源包的String pool,不進行重構會導致崩潰。
SO庫熱修復技術(22題)
SO庫加載原理
1、Java API提供兩個接口來加載SO庫
System.loadLibrary(String libName) 1. 參數-SO庫名稱,位于apk壓縮文件的libs目錄 1. 最后復制到apk安裝目錄下 System.load(String pathName) 1. 參數-so庫在磁盤中的完整路徑 1. 加載一個自定義外部so庫文件
2、JNI編程中,native方法分為動態注冊和靜態注冊兩種
動態注冊
3、動態注冊的native方法
必須實現JNI_OnLoad方法 需要實現一個JNINativeMethod[]數組 動態注冊的native方法映射通過加載so庫過程中調用JNI_OnLoad方法調用完成
靜態注冊
4、靜態注冊的native方法
必須是Java + 類完整路徑 + 方法名的格式 靜態注冊的native方法映射是在該native方法第一次執行時完成。前提該so庫已經load過
SO庫熱部署方案
動態注冊native方法
5、動態注冊的native方法實時生效的方案?
該方法每調用一次JNI_OnLoad方法就會重新進行一次映射 先加載原來的so庫,再加載補丁so庫,就能映射為補丁中的新方法
Art
6、ART中能做到實時生效(熱部署)嗎?
可以
Dalvik
7、Dalvik中能做到實時生效(熱部署)嗎?
不能 第二次load補丁so庫,依舊執行的是原來so庫的JNI_OnLoad()方法
8、為什么Dalvik加載補丁so庫,執行的是原始so庫的load方法?
下面兩個方法可能有問題: dlopen: 返回動態鏈接庫的句柄 dlsym: 通過dlopen得到的句柄,來查找一個symbol dlopen具有bug: Dalvik中通過so的name去solist中查找,因為加載原始so庫時,該列表中已經存儲,直接返回原始so庫的句柄
9、Dalvik的Bug如何規避?
對補丁so庫進行改名 原始so庫路徑為: /data/data/…/files/libnative-lib.so 補丁so庫路徑改為:/data/data/…/files/libnative-lib-時間戳.so 通過改名,添加時間戳,保證name是全局唯一,這樣能正確得到動態鏈接庫的句柄
靜態注冊native方法
10、靜態注冊native方法的映射都是在native方法的第一次執行時完成的映射。如果native方法在加載補丁so庫前已經執行過了。會出現問題。
11、如果保證靜態注冊native方法能夠熱部署?
JNI API提供了解注冊的接口 把目標類的所有native方法都解注冊,無論是動態注冊還是靜態注冊的,后面都需要重新映射
env
- > UnregisterNative ( claze
) ;
12、經過解注冊處理后,熱部署也不一定會成功
補丁so庫是否能成功加載,取決于在hashtable中的位置 如果順序是:補丁so庫、原始so庫。則熱部署修復成功。 如果順序是: 原始so庫、補丁so庫。則失敗。
SO庫冷部署方案
接口調用替換方案
13、使用sdk提供的接口替換System加載so庫的接口
用SOPatchManager.loadLibrary()替代System.loadLibrary(),優先加載sdk指定目錄下的補丁so SOPatchManager.loadLibrary()的加載策略如下: 如果存在補丁so庫,直接加載。 如果不存在補丁so庫,調用System.loadLibrary加載apk目錄下的so庫
14、替換方案的優點
不需要對不同sdk版本進行兼容,因為都有System.loadLibrary這個接口
15、替換方案的缺點
調用System接口的地方都需要替換為sdk的接口 如果是已經混淆好的第三方庫的so庫,無法進行接口替換。
反射注入方案
16、System.loadLibrary(“native-lib”)的底層原理
本質是在nativeLibraryDirectories數組中遍歷
17、反射注入方案
將補丁so庫的路徑,插入到nativeLibraryDirectories數組的最前面 就能達到加載so庫時,直接加載補丁so庫的目的。
sdk23前后的區別
18、sdk < 23時,只需要將補丁so庫的路徑,插入到nativeLibraryDirectories數組的最前面
19、sdk >= 23時,需要用補丁so庫路徑來構建Element對象,然后插入到nativeLibraryPathElements數組的最前面
20、反射注入方案的優缺點
優點: 不具有侵入性 缺點: 需要針對sdk進行適配
機型對應的so庫
21、so庫具有多種cpu架構的so文件
如armeabi、arm64-v8a、x86 難點在于如何根據機型,選擇對應的so庫文件
22、如何選擇機型最合適的primaryCpuAbi so文件?
sdk>=21,反射拿到ApplicationInfo對象的primaryCpuAbi即可 sdk<21,不支持64位,直接把Build.CPU_ABI、Build.CPU_ABI2作為primaryCpuAbi即可
問題匯總
參考資料
混淆庫官方文檔 Android 新一代編譯 toolchain Jack & Jill 簡介 官方文檔Compiling with Jack gradle中如何使用Lambda Android動態鏈接庫加載原理及HotFix方案介紹
總結
以上是生活随笔 為你收集整理的Android 热修复原理 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。