一恰梢、背景
之前在搞Android產(chǎn)物字節(jié)碼分析時發(fā)現(xiàn)的一個問題佛南,這和一個老生常談的定論:“Java訪問常量不會觸發(fā)類加載”有密切關系,估計大家會感興趣嵌言,因此寫下來給大家分享下嗅回。
二、結(jié)論先行
在Android上摧茴,訪問一個常量(符號引用)也會觸發(fā)類加載
這和Android具體的代碼場景有關系绵载,感興趣可以接著往下看~
三、問題分析
3.1 工具利器
推薦工具 010 Editor苛白,它內(nèi)置了很多種二進制文件的結(jié)構(gòu)模版娃豹,可以幫助我們快速查看一些二進制文件的內(nèi)容,例如二進制xml购裙、dex懂版、arsc文件等。
~/.config/SweetScape/010 Editor.ini躏率,如果010editor過了30天試用期躯畴,可以刪除這個文件來清除記錄,即可接著使用薇芝。
推薦工具 python struct蓬抄,直接import struct就行『坏剑可以幫助我們按照字節(jié)快速解析數(shù)據(jù)嚷缭,例如:
struct.unpack('<HHI', f.read(8))
含義:讀取后8個字節(jié)的數(shù)據(jù),<代表按照小端序讀取黄娘,unpack的結(jié)果是一個數(shù)組峭状,HHI代表前兩個元素為short克滴,第三個元素為int,分別對應2优床、2劝赔、4個字節(jié)的數(shù)據(jù)。
3.2 知識儲備
3.2.1 dex的部分格式
dex最上層結(jié)構(gòu)還是比較清晰的:
[圖片上傳失敗...(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指的是類里面的所有信息仍翰,其中中包含了數(shù)據(jù)static_fields,指的是類中所有的static字段的數(shù)據(jù):
[圖片上傳失敗...(image-bc1ec0-1683790977467)]
其中 field_idx_diff指的是這個字段在字段表里面的索引观话,access_flags指的是字段的修飾符予借,包括private、public频蛔、static等等
底部的static_values指的是static final常量的值灵迫,它和上面的class_data.static_fields中前面幾位static final常量是一一對應(按照順序?qū)┑模?/p>
[圖片上傳失敗...(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符號引用度气,但無法查詢到對應的值割按。
結(jié)合3.2.1的信息,其實已經(jīng)能猜出來磷籍,訪問常量同樣會觸發(fā)類加載适荣。因為訪問static字段的指令都是sget,訪問static變量肯定會觸發(fā)類加載院领,那訪問常量也是同樣的道理弛矛。
3.2.3 指令解析的部分源碼
這里直接貼出sget指令解析的源碼地址
經(jīng)過一系列的源碼追蹤,能找到最終解析 sget 后面的字段的地方:
[圖片上傳失敗...(image-3dcca1-1683790977467)]
該函數(shù)中調(diào)用了ResolveType 比然,參數(shù)傳入了class_idx丈氓,這和3.2.1節(jié)的內(nèi)容呼應。接著追蹤:
[圖片上傳失敗...(image-30c3ed-1683790977467)]
其實到這一步已有有答案了,F(xiàn)indClass接著就會去加載一個class 万俗。
不過相信大家還是對哪里使用的static_values感興趣湾笛,接著看:
在一個class初始化的時候:
[圖片上傳失敗...(image-b0249d-1683790977467)]
這里也是和3.2.1節(jié)內(nèi)容呼應,初始化一個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的內(nèi)聯(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這種的饱亿,這個模塊在編譯之后會產(chǎn)生R.jar蚜退,例如截圖:(class反編譯之后的)
[圖片上傳失敗...(image-91d617-1683790977466)]
這里的屬性全是static變量,而javac對不會對static變量做內(nèi)聯(lián)優(yōu)化彪笼。也就是說仍然存在著R.xxx.xxx的各種符號引用关霸,比如這種的:(class的圖形化展示 R.attr.submodule_a)
[圖片上傳失敗...(image-bc3e89-1683790977466)]
這個模塊的class文件會參與到App的編譯,但無需再走javac杰扫。App編譯時會對R.xx.xx賦值(注意上面截圖中屬性值都還是0)并將修飾符改為常量,例如產(chǎn)物:(App的最終產(chǎn)物dex中的R$string)
[圖片上傳失敗...(image-900c49-1683790977466)]
所以膘掰,在最終的代碼中就存在“對一個常量的符號引用”的情況
上述僅發(fā)生在debug包中章姓,打release包時還會對常量的符號引用進行內(nèi)聯(lián)
3.3 本地驗證
上面從理論上說通了訪問一個常量是可能造成類加載的。現(xiàn)在來進行本地驗證:
我這里就拿3.2.4中的普通模塊的R場景進行測試了识埋,測試關鍵代碼如下:
[圖片上傳失敗...(image-cf32e1-1683790977466)]
[圖片上傳失敗...(image-a3ab02-1683790977466)]
日志結(jié)果:
[圖片上傳失敗...(image-d4efcf-1683790977466)]
符合預期凡伊,訪問常量(符號引用)確實造成了類加載!
注意:實驗只能在自定義ClassLoader(插件化)的情況下測試窒舟,因為對于宿主源碼的ClassLoader是PathClassLoader系忙。PathClassLoader在調(diào)用findLoadedClass時就會“偷摸”著把class加載了,會導致rIsLoaded方法第一次訪問時就直接返回true了惠豺。