熱修復原理淺析(二)

原文鏈接:https://juejin.im/post/5d492717f265da03d316a985

瞭解熱修復,需要有點預熱的知識,先從class文件和dex文件說起

class文件和dex文件

class文件

什麼是class文件

他是一種文件格式

簡單說,就是能被JVM虛擬機識別、加載、並執行的文件格式

而且除了java語言,還有很多其他語言也可以編譯出class文件,當然還有kotlin


上圖摘抄自【深入Java虛擬機】之二:Class類文件結構

如何手動編譯出一個class文件

很簡單
javac hello.java

class文件的作用

記錄一個類文件裏的所有信息,記住是一個類文件,而且是所有信息

Class類文件結構

詳細的可參考【深入Java虛擬機】之二:Class類文件結構

這裏簡要說一下:

  1. Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部都是程序運行的必要數據。

  2. 下表列出了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文件呢

  1. class文件內存佔用大,不適合移動端,最關鍵就是一個class文件只能表述一個類文件的所有屬性
  2. 堆棧的加載模式,加載速度較慢
  3. 文件IO操作多,類查找慢

dex文件

什麼是dex文件

能被DVM虛擬機識別、加載、並執行的文件格式

如何手動編譯一個dex文件

在build-tools裏面找到dx.bat

要使用dx命令,記得配置環境變量

  1. javac命令 生成class文件

javac hello.java

  1. dx命令 生成dex文件

dx --dex --output hello.dex hello.class

  1. adb命令把hello.dex文件放到手機內存卡

adb push hello.dex /storage/emulated/0

  1. 進入shell

adb shell

  1. dalvikvm命令 執行dex文件裏的hello方法

    注意dex文件必須在Andriod手機執行,因爲手機裏纔有DVM虛擬機

dalvikvm -cp /sdcard/hello.dex hello

dex文件的作用

一個class文件只是記錄一個Java類的所有信息

但是一個dex記錄所有類文件的信息,是整個工程的信息

dex文件結構

上圖中的文件頭部分,記錄了dex文件的信息,所有字段大致的一個分部;

索引區部分,主要包含字符串、類型、方法原型、域、方法的索引;

索引區最終又被存儲在數據區,其中鏈接數據區,主要存儲動態鏈接庫,so庫的信息。

dex文件長什麼樣子呢

來張大圖

一張圖理解dex

dex與class異同

當java程序編譯成class後,還需要使用dx工具將所有的class文件整合到一個dex文件,目的是其中各個類能夠共享數據,在一定程度上降低了冗餘,同時也是文件結構更加經湊,實驗表明,dex文件是傳統jar文件大小的50%左右



class與dex異同之處

編年體與紀傳體

紀傳體通過記敘人物活動反映歷史事件的體裁,通過記敘人物活動,反映歷史事件。 如:《秦始皇本記.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直接或者間接引用的對象,就不可銷燬

引用類型

強軟弱虛

弱引用的創建與使用

垃圾回收算法

標記-清除算法

好處:不需要讓對象進行移動,僅需要對不存活的對象進行處理,在存活對象較多時候,執行效率高效,但是內存碎片很多

複製算法

好處:當存活的對象比較少時,較爲高效,但是需要另外一塊空間,用於管理移動

標記-整理算法
  1. 先遍歷把可回收對象掃描出來,如B
  2. 掃描清除未標記對象
  3. 把存活的對象,進行移動,沒有內存碎片

以上三種算法各有優缺點,虛擬機根據不同情況,採用不同算法,進行垃圾回收

觸發回收

  1. jvm無法爲新對象創建內存了
  2. 手動調用System.gc()方法(並不會馬上執行gc)
  3. 低優先級的gc線程,被運行時就會執行

Dalvik 虛擬機與Jvm異同之處

  1. 執行文件不同,一個是class文件,一個是dex
  2. 類加載系統區別較大
  3. Dalvik 可以同時存在多個,Jvm只能同時存在一個
  4. 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的特點

雙親代理模式

  1. classloader加載字節碼時,先詢問當前classloader是不是加載過此類,如果加載過,直接返回(不會重複加載字節碼)
  2. 如果沒有加載過,詢問父classloader是不是加載過,如果加載過返回parent加載過的字節碼文件
  3. 如果整個繼承線路都沒加載過這個字節碼,纔會由子classloader完成加載

由此可見,一個字節碼文件被任意一個classLoader加載過,就不會被其他classLoader加載了,提高了加載效率,也帶來了另外特性

類加載共享功能

一個字節碼文件一旦被頂層classLoader加載過,就會被整個繼承體系所共有

類加載隔離功能

不同繼承路線的classLoader加載的類,肯定不是同一個類,防止被冒充

例如String這個類,肯定在頂層的classLoader裏會把它加載,這樣就避免,你自己寫個classLoader來篡改string這個類的加載過程

什麼樣的類才能叫做是同一個類呢

同一個包名+同一個類名+同一個類加載器加載的類,才叫同一個類

ClassLoader的源碼

如果都找不到,會走findClass方法,看一下這個方法

那麼ClassLoader有哪些子類呢

源碼目錄

間接子類:DexClassloader

DexClassloader源碼查看

上面的第二個參數很重要,這個路徑是系統內部的路徑,就是因爲這個參數,才能去把未安裝到app裏的dex文件,加載進來

間接子類:PathClassLoader

PathClassLoader源碼查看

其實這兩個間接地子類,什麼也沒做,只是一個能加載外部的dex文件,一個只能加載apk內部的文件,主要邏輯還是他們的父類BaseDexClassloader實現的,我們接着看BaseDexClassloader的findClass方法,看看是如何加載dex文件的

直接子類:BaseDexClassloader

BaseDexClassloader源碼查看

發現直接調用的是DexpathList的findclass方法

DexPathList源碼

Element是類DexPathList的一個內部類,它其中重要的一個變量就是DexFile,就是dex文件。

看看這個Element[]是怎麼實現的

來到makePathElement方法

makePathElements方法核心作用就是將指定路徑中的所有文件轉化成DexFile同時存儲到到Element[]這個數組中。nativeLibraryDirectories 就是lib庫了。
最終在findclass方法中實現。

接着看看dexFile的loadClassBinaryName方法,我們進入DexFile這個類

DexFile源碼查看

回顧一下,我們的源碼解析經歷了些什麼

  1. 首先看了ClassLoder 的雙親委託模式的實現,發現最終指向了findClass()這個方法
  2. 然後發現他是一個空實現,他等着子類去實現
  3. ClassLoader有一個直接子類BaseDexClassloader和兩個間接子類DexClassloader 、PathClassLoader其實這兩個間接地子類,什麼也沒做,只是DexClassloader能加載外部的dex文件,PathClassLoader只能加載apk內部的文件,主要邏輯還是他們的父類BaseDexClassloader實現的
  4. 在BaseDexClassloader裏發現findClass,調用的是DexPathList的findClass方法
  5. 在DexPathList裏,先看到一個Element這個內部類,他裏面有個重要的變量叫DexFile,Element[]是通過makePathElement實現的
  6. makePathElement遍歷所有文件,把所有dex加載爲dexFile,並且存到Element[]裏
  7. Ok終於來到BaseDexClassloader的findClass方法,他會遍歷所有dexElement,通過clss的名字,加載這個類爲class對象
  8. dexFile加載class的實現是通過native實現的,就這樣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章