在Android上,訪問常量真的不會觸發(fā)類加載嗎?

一、背景

之前在搞Android產物字節(jié)碼分析時發(fā)現的一個問題,這和一個老生常談的定論:“Java訪問常量不會觸發(fā)類加載”有密切關系,估計大家會感興趣,因此寫下來給大家分享下。

二、結論先行

在Android上,訪問一個常量(符號引用)也會觸發(fā)類加載

這和Android具體的代碼場景有關系,感興趣可以接著往下看~

三、問題分析

3.1 工具利器

推薦工具 010 Editor,它內置了很多種二進制文件的結構模版,可以幫助我們快速查看一些二進制文件的內容,例如二進制xml、dex、arsc文件等。

~/.config/SweetScape/010 Editor.ini,如果010editor過了30天試用期,可以刪除這個文件來清除記錄,即可接著使用。

推薦工具 python struct,直接import struct就行??梢詭椭覀儼凑兆止?jié)快速解析數據,例如:

struct.unpack('<HHI', f.read(8))

含義:讀取后8個字節(jié)的數據,<代表按照小端序讀取,unpack的結果是一個數組,HHI代表前兩個元素為short,第三個元素為int,分別對應2、2、4個字節(jié)的數據。

3.2 知識儲備

3.2.1 dex的部分格式

dex最上層結構還是比較清晰的:

[圖片上傳失敗...(image-529efb-1683790977467)]

我們需要關注其中的:

  • dex_string_ids dex字符串常量池

  • dex_type_ids dex中所有的類名描述

  • dex_field_ids dex中所有的字段描述,舉個例子

[圖片上傳失敗...(image-f9cc98-1683790977467)]

class_idx指的是它所在的類名索引(從dex_type_ids中查找),type_idx指的是類型名稱(從dex_type_ids中查找),name_idx指的是字段名索引(從dex_string_ids中查找)

  • dex_class_defs dex中所有類的描述,舉個例子:

[圖片上傳失敗...(image-6717c5-1683790977467)]

class_data指的是類里面的所有信息,其中中包含了數據static_fields,指的是類中所有的static字段的數據:

[圖片上傳失敗...(image-bc1ec0-1683790977467)]

其中 field_idx_diff指的是這個字段在字段表里面的索引,access_flags指的是字段的修飾符,包括private、public、static等等

底部的static_values指的是static final常量的值,它和上面的class_data.static_fields中前面幾位static final常量是一一對應(按照順序對應)的:

[圖片上傳失敗...(image-5b6239-1683790977467)]

因為static變量是在類的<clinit>方法中進行賦值的,所以只有常量在static_values里面有值

由上可知:在dex中,static和static final字段儲存位置是一樣的,僅修飾符不一樣

3.2.2 訪問static字段的指令

這個拿實際的例子比較好說明:

// 訪問一個static 變量/常量
R.string.dialog_loading_title

它對應的字節(jié)碼指令:

[圖片上傳失敗...(image-fe40c7-1683790977467)]

0160 03f3

忽略01,關鍵是 60 03f3:

60 代表指令 sget,含義是獲取一個static字段

03f3 代表字段的索引,十進制為1011,在對應的字段表中為:

[圖片上傳失敗...(image-165a40-1683790977467)]

通過03f3能從字段表中讀取該字段,能獲取到class_idx類索引,可以拿到類的名稱。也就是通過 60 03f字節(jié)碼只能查詢到R.string.dialog_loading_title符號引用,但無法查詢到對應的值。

結合3.2.1的信息,其實已經能猜出來,訪問常量同樣會觸發(fā)類加載。因為訪問static字段的指令都是sget,訪問static變量肯定會觸發(fā)類加載,那訪問常量也是同樣的道理。

3.2.3 指令解析的部分源碼

這里直接貼出sget指令解析的源碼地址

經過一系列的源碼追蹤,能找到最終解析 sget 后面的字段的地方:

[圖片上傳失敗...(image-3dcca1-1683790977467)]

該函數中調用了ResolveType ,參數傳入了class_idx,這和3.2.1節(jié)的內容呼應。接著追蹤:

[圖片上傳失敗...(image-30c3ed-1683790977467)]

其實到這一步已有有答案了,FindClass接著就會去加載一個class 。

不過相信大家還是對哪里使用的static_values感興趣,接著看:

在一個class初始化的時候:

[圖片上傳失敗...(image-b0249d-1683790977467)]

這里也是和3.2.1節(jié)內容呼應,初始化一個class時會去獲取它在dex中的定義 ClassDef

截圖是在初始化class的static字段,而對static final的賦值請見EncodedStaticFieldValueIterator

[圖片上傳失敗...(image-15eb1b-1683790977467)]

[圖片上傳失敗...(image-10ad5e-1683790977466)]

這里的static_value_off_和3.2.1中的static_values對應,代表著常量的值

3.2.4 javac的內聯(lián)優(yōu)化

在訪問一個常量時,javac總是會幫我們把常量的符號引用變成對值的直接引用,所以從這個角度說,訪問一個常量確實不會觸發(fā)類加載。例子:

class A {
  public static final NAME = "abc";
}

// 編譯前
String localName = A.NAME;

// 編譯后
String localName = “abc”

但凡事都有意外,什么情況下常量不會被優(yōu)化呢?拿個實際的場景舉例子:

在Android的一個普通模塊中定義了新的資源,例如R.string.name這種的,這個模塊在編譯之后會產生R.jar,例如截圖:(class反編譯之后的)

[圖片上傳失敗...(image-91d617-1683790977466)]

這里的屬性全是static變量,而javac對不會對static變量做內聯(lián)優(yōu)化。也就是說仍然存在著R.xxx.xxx的各種符號引用,比如這種的:(class的圖形化展示 R.attr.submodule_a)

[圖片上傳失敗...(image-bc3e89-1683790977466)]

這個模塊的class文件會參與到App的編譯,但無需再走javac。App編譯時會對R.xx.xx賦值(注意上面截圖中屬性值都還是0)并將修飾符改為常量,例如產物:(App的最終產物dex中的R$string)

[圖片上傳失敗...(image-900c49-1683790977466)]

所以,在最終的代碼中就存在“對一個常量的符號引用”的情況

上述僅發(fā)生在debug包中,打release包時還會對常量的符號引用進行內聯(lián)

3.3 本地驗證

上面從理論上說通了訪問一個常量是可能造成類加載的。現在來進行本地驗證:

我這里就拿3.2.4中的普通模塊的R場景進行測試了,測試關鍵代碼如下:

[圖片上傳失敗...(image-cf32e1-1683790977466)]

[圖片上傳失敗...(image-a3ab02-1683790977466)]

日志結果:

[圖片上傳失敗...(image-d4efcf-1683790977466)]

符合預期,訪問常量(符號引用)確實造成了類加載!

注意:實驗只能在自定義ClassLoader(插件化)的情況下測試,因為對于宿主源碼的ClassLoader是PathClassLoader。PathClassLoader在調用findLoadedClass時就會“偷摸”著把class加載了,會導致rIsLoaded方法第一次訪問時就直接返回true了。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 1.手畫一下Android系統(tǒng)架構圖,描述一下各個層次的作用? Android系統(tǒng)架構圖 從上到下依次分為六層: ...
    __素顏__閱讀 6,070評論 1 107
  • layout: wikititle: Android逆向分析筆記categories: Reverse_Engin...
    超哥__閱讀 10,902評論 1 17
  • 代碼編譯的結構:本地機器碼--->字節(jié)碼。越來越多的程序語言選擇了與操作系統(tǒng)和機器指令集無關的、平臺中立的格式作為...
    一條小袍袍YoY閱讀 267評論 0 0
  • 類加載機制 如下圖所示,JVM類加載機制分為五個部分:加載,驗證,準備,解析,初始化,下面我們就分別來看一下這五個...
    舉頭望明月泣閱讀 1,250評論 0 0
  • JAVA 類的加載過程 Child c= new Child ();為例進行說明1).因為new用到了Child....
    Rtia閱讀 826評論 0 1

友情鏈接更多精彩內容