瞭解熱修復,需要有點預熱的知識,先從class文件和dex文件說起
class文件和dex文件
class文件
什麼是class文件
他是一種文件格式
簡單說,就是能被JVM虛擬機識別、加載、並執行的文件格式
而且除了java語言,還有很多其他語言也可以編譯出class文件,當然還有kotlin
如何手動編譯出一個class文件
很簡單
javac hello.java
class文件的作用
記錄一個類文件裏的所有信息,記住是一個類文件,而且是所有信息
Class類文件結構
詳細的可參考【深入Java虛擬機】之二:Class類文件結構
這裏簡要說一下:
-
Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部都是程序運行的必要數據。
-
下表列出了Class文件中各個數據項的具體含義:
magic
每個Class文件的頭4個字節稱爲魔數(magic),它的唯一作用是判斷該文件是否爲一個能被虛擬機接受的Class文件。它的值固定爲0xCAFEBABE。
version
緊接着magic的4個字節存儲的是Class文件的次版本號和主版本號,高版本的JDK能向下兼容低版本的Class文件,但不能運行更高版本的Class文件。
constant_pool
常量池是class文件中非常重要的結構,它描述着整個class文件的字面量信息。
常量池是由一組constant_pool結構體數組組成的,而數組的大小則由常量池計數器指定。
常量池計數器constant_pool_count 的值 =constant_pool表中的成員數+ 1。constant_pool表的索引值只有在大於 0 且小於constant_pool_count時纔會被認爲是有效的。
access_flag
this_class、super_class、interfaces
fields
methods
attributes
來個大圖
好了,看看二進制文件究竟長什麼樣
這個是使用一個工具來查看class文件的內容
爲什麼Android沒使用class文件,而是創造了dex文件呢
- class文件內存佔用大,不適合移動端,最關鍵就是一個class文件只能表述一個類文件的所有屬性
- 堆棧的加載模式,加載速度較慢
- 文件IO操作多,類查找慢
dex文件
什麼是dex文件
能被DVM虛擬機識別、加載、並執行的文件格式
如何手動編譯一個dex文件
在build-tools裏面找到dx.bat
要使用dx命令,記得配置環境變量
- javac命令 生成class文件
javac hello.java
- dx命令 生成dex文件
dx --dex --output hello.dex hello.class
- adb命令把hello.dex文件放到手機內存卡
adb push hello.dex /storage/emulated/0
- 進入shell
adb shell
- dalvikvm命令 執行dex文件裏的hello方法
注意dex文件必須在Andriod手機執行,因爲手機裏纔有DVM虛擬機
dalvikvm -cp /sdcard/hello.dex hello
dex文件的作用
一個class文件只是記錄一個Java類的所有信息
但是一個dex記錄所有類文件的信息,是整個工程的信息
dex文件結構
上圖中的文件頭部分,記錄了dex文件的信息,所有字段大致的一個分部;
索引區部分,主要包含字符串、類型、方法原型、域、方法的索引;
索引區最終又被存儲在數據區,其中鏈接數據區,主要存儲動態鏈接庫,so庫的信息。
dex文件長什麼樣子呢
來張大圖
dex與class異同
當java程序編譯成class後,還需要使用dx工具將所有的class文件整合到一個dex文件,目的是其中各個類能夠共享數據,在一定程度上降低了冗餘,同時也是文件結構更加經湊,實驗表明,dex文件是傳統jar文件大小的50%左右
編年體與紀傳體
紀傳體通過記敘人物活動反映歷史事件的體裁,通過記敘人物活動,反映歷史事件。 如:《秦始皇本記.class》《項羽本紀.class》《高祖本紀.class》
編年體是中國傳統史書的一種體裁,它是以年代爲線索編排有關歷史事件。編年體史書以時間爲中心,按年、月、日順序記述史事。因爲它以時間爲經,以史事爲緯,比較容易反映出同一時期各個歷史事件的聯繫。 例如:《春秋.dex》《左傳.dex》《資治通鑑.dex》。
JVM虛擬機簡介
jvm整體結構與組成
內存裏存儲class文件的不同部分,對應內存空間裏的不同部分
編譯流程
類加載器
jvm的classloader與Android裏的classloader區別較大
下圖爲jvm的類加載器,
Android的類加載器是熱修復的核心,接下來會專門說
類加載流程
jvm內存管理和垃圾回收
java棧區
java棧幀
每個方法從調用到執行完成,就是對應一個棧幀在虛擬機從入棧到出棧的過程
棧幀裏包含局部變量表、棧操作數、動態鏈接、方法入口
A方法調用B方法,就會在調用B方法代碼時,java虛擬就就會創建一個保存B方法的棧幀,然後壓入棧區,當B方法執行完後,這個棧幀就會彈出棧區,這就是使我們經常說的,棧內存不需要我們管理,局部變量會在方法調用結束後,自動回收。
另外,從這裏可以看出,每個方法對應一個棧幀,如果遞歸方法嵌套太深,當棧的深度大於jvm所允許的最大深度時候,會引起Stack Overflow,棧溢出,所以遞歸慎用,
本地方法棧
爲native方法服務的,也是通過棧幀實現對本地方法的調用
方法區
存儲虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯後的數據
這塊區域,永遠佔據內存,知道退出進程
所以常量、靜態變量生命週期很長,只有App退出,纔會被回收,所以,很多內存泄漏都是不合理使用靜態變量引起的
堆區
所有通過new創建的對象的內存都在堆區分配
是虛擬機中最大的一塊內存,是GC要回收的部分
新生代與老生代,簡單說,剛剛創建的對象會存在新生代裏,當新生代對象越來越多,內存不足時候,jvm會通過自己的一套算法,把對象從新生代移動到老生代,這樣新生代就會多出一部分空間了,還能接受新的對象。當新生代和老生代的內存都滿了,再來對象就會oom
爲什麼要分新生代+老生代
這是爲了讓開發者動態調整新生代和老生代的大小,例如在做即時通訊時,臨時的消息對象創建的比較多,就可以把新生代這塊區域調整大一些,便於新對象的分配
垃圾回收
引用計數算法
引用計數器:被引用+1,引用銷燬-1,爲0,則可以被銷燬
循環引用的時候,此算法失效
可達性算法
被GCRoot直接或者間接引用的對象,就不可銷燬
引用類型
強軟弱虛
弱引用的創建與使用
垃圾回收算法
標記-清除算法
好處:不需要讓對象進行移動,僅需要對不存活的對象進行處理,在存活對象較多時候,執行效率高效,但是內存碎片很多
複製算法
好處:當存活的對象比較少時,較爲高效,但是需要另外一塊空間,用於管理移動
標記-整理算法
- 先遍歷把可回收對象掃描出來,如B
- 掃描清除未標記對象
- 把存活的對象,進行移動,沒有內存碎片
以上三種算法各有優缺點,虛擬機根據不同情況,採用不同算法,進行垃圾回收
觸發回收
- jvm無法爲新對象創建內存了
- 手動調用System.gc()方法(並不會馬上執行gc)
- 低優先級的gc線程,被運行時就會執行
Dalvik 虛擬機與Jvm異同之處
- 執行文件不同,一個是class文件,一個是dex
- 類加載系統區別較大
- Dalvik 可以同時存在多個,Jvm只能同時存在一個
- Dalvik是基於寄存器的,jvm是基於棧的
jvm的方法調用是就棧的,前面說的棧幀
Dalvik是基於寄存器的,寄存器是比內存更快的存儲介質
ART虛擬機
雖然Dalvik虛擬機已經不錯了,但是google工程師研發了ATR虛擬機,更加高效
- DVM使用JIT將字節碼轉換爲機器碼,效率低
app每次運行都會把字節碼轉換爲機器碼,再去執行,退出應用,在進入app,又會再次把字節碼轉爲機器碼,效率很低的
- ART採用的是AOT預編譯技術,執行速度更快
在app安裝時候,就把字節碼轉爲本地機器碼,存在本地,因此,只要app啓動,直接執行機器碼,而不是每次轉換。
但是採用ART預編譯技術,app安裝時間快比較長,而且在手機裏佔用空間多
空間換時間
Classlodaer
java裏的classloder
android的classloader
classloader種類
- BootClassLoader
加載framework層的字節碼文件
- PathClassLoader
加載安裝到系統裏的app的class文件
- DexClassLoader
加載指定目錄的class文件
- BaseDexClassloader
PathClassLoader和DexClassLoader的父類
其實一個app最少需要BootClassLoader和PathClassLoader才能正常運行
我們打印下app裏的classlodaer
// 打印所有的ClassLoader
var classLoader = classLoader
if (classLoader != null) {
Log.e("cjx", "ClassLoader---$classLoader")
while (classLoader.parent != null) {
classLoader = classLoader.parent
Log.e("cjx", "ClassLoader---$classLoader")
}
}
ClassLoader的特點
雙親代理模式
- classloader加載字節碼時,先詢問當前classloader是不是加載過此類,如果加載過,直接返回(不會重複加載字節碼)
- 如果沒有加載過,詢問父classloader是不是加載過,如果加載過返回parent加載過的字節碼文件
- 如果整個繼承線路都沒加載過這個字節碼,纔會由子classloader完成加載
由此可見,一個字節碼文件被任意一個classLoader加載過,就不會被其他classLoader加載了,提高了加載效率,也帶來了另外特性
類加載共享功能
一個字節碼文件一旦被頂層classLoader加載過,就會被整個繼承體系所共有
類加載隔離功能
不同繼承路線的classLoader加載的類,肯定不是同一個類,防止被冒充
例如String這個類,肯定在頂層的classLoader裏會把它加載,這樣就避免,你自己寫個classLoader來篡改string這個類的加載過程
什麼樣的類才能叫做是同一個類呢
同一個包名+同一個類名+同一個類加載器加載的類,才叫同一個類
ClassLoader的源碼
如果都找不到,會走findClass方法,看一下這個方法
那麼ClassLoader有哪些子類呢
間接子類:DexClassloader
上面的第二個參數很重要,這個路徑是系統內部的路徑,就是因爲這個參數,才能去把未安裝到app裏的dex文件,加載進來
間接子類:PathClassLoader
其實這兩個間接地子類,什麼也沒做,只是一個能加載外部的dex文件,一個只能加載apk內部的文件,主要邏輯還是他們的父類BaseDexClassloader實現的,我們接着看BaseDexClassloader的findClass方法,看看是如何加載dex文件的
直接子類:BaseDexClassloader
發現直接調用的是DexpathList的findclass方法
Element是類DexPathList的一個內部類,它其中重要的一個變量就是DexFile,就是dex文件。
看看這個Element[]是怎麼實現的
來到makePathElement方法
makePathElements方法核心作用就是將指定路徑中的所有文件轉化成DexFile同時存儲到到Element[]這個數組中。nativeLibraryDirectories 就是lib庫了。
最終在findclass方法中實現。
接着看看dexFile的loadClassBinaryName方法,我們進入DexFile這個類
回顧一下,我們的源碼解析經歷了些什麼
- 首先看了ClassLoder 的雙親委託模式的實現,發現最終指向了findClass()這個方法
- 然後發現他是一個空實現,他等着子類去實現
- ClassLoader有一個直接子類BaseDexClassloader和兩個間接子類DexClassloader 、PathClassLoader其實這兩個間接地子類,什麼也沒做,只是DexClassloader能加載外部的dex文件,PathClassLoader只能加載apk內部的文件,主要邏輯還是他們的父類BaseDexClassloader實現的
- 在BaseDexClassloader裏發現findClass,調用的是DexPathList的findClass方法
- 在DexPathList裏,先看到一個Element這個內部類,他裏面有個重要的變量叫DexFile,Element[]是通過makePathElement實現的
- makePathElement遍歷所有文件,把所有dex加載爲dexFile,並且存到Element[]裏
- Ok終於來到BaseDexClassloader的findClass方法,他會遍歷所有dexElement,通過clss的名字,加載這個類爲class對象
- dexFile加載class的實現是通過native實現的,就這樣