日常工作中有時(shí)候可能會(huì)遇到需要統(tǒng)計(jì)某個(gè)方法的使用地方会喝,項(xiàng)目里有沒(méi)有代碼調(diào)用了某些違規(guī)函數(shù)陡叠,某類到底被哪些類給依賴了等等問(wèn)題,這種需求通常會(huì)通過(guò)寫
python
腳步去掃描整個(gè)項(xiàng)目代碼肢执,這種方式優(yōu)點(diǎn)是簡(jiǎn)單枉阵,但缺點(diǎn)也十分的明顯,就是效率很低预茄,因?yàn)槟_步通常都是一行行的文本內(nèi)容掃描然后通過(guò)正則去匹配兴溜,即便是遇到了注釋或空行也照樣會(huì)進(jìn)行匹配,顯然的效率極低耻陕,其實(shí)對(duì)于這類問(wèn)題還可以使用字節(jié)碼掃描的方式去實(shí)現(xiàn)拙徽,效率十分的高,即便是要在十幾萬(wàn)個(gè)類中掃描某方法也僅僅是幾秒的時(shí)間就能完成了诗宣。
前言
所謂字節(jié)碼掃描就是通過(guò)讀取class文件膘怕,解析class文件結(jié)構(gòu),最后通過(guò)檢索class的內(nèi)部結(jié)構(gòu)召庞,如常量池岛心、方法表、局部變量表等等裁眯,去找我們想要的信息布讹。舉個(gè)列子京革,譬如我們想要知道class A 被哪些類引用了,只需要掃描所有class常量池的CONSTANT_Class_info
結(jié)構(gòu)就可以了,又譬如我們想知道某方法(譬如是getUserInfo)它被哪些代碼依賴了宾符,也是可以通過(guò)掃描常量池里面的CONSTANT_Methodref_info
結(jié)構(gòu)可以獲取到嚼沿。在增量編譯系統(tǒng)里蒸绩,假如A類被修改了祭钉,那么使用到A類的代碼也得被找出來(lái)重新編譯,早期的Gradle版本實(shí)現(xiàn)增量編譯功能它改,就是通過(guò)這種掃描class字節(jié)碼結(jié)構(gòu)的方式去查找類依賴關(guān)系的(18年看過(guò)Gradle 3.x的源碼是這樣實(shí)現(xiàn)的疤孕,現(xiàn)在的版本不清楚有沒(méi)有修改過(guò))。
認(rèn)識(shí)class內(nèi)部結(jié)構(gòu)
假設(shè)我們有下面Demo代碼
package com.nls.lib;
/**
* Create by nls on 2022/1/19
* description: Test
*/
public class Test {
public int a;
public void test1() {
System.out.println("123");
}
}
編譯成class文件后央拖,我們用二進(jìn)制編輯工具打開(kāi)是這樣的 一般人都看不懂這串16進(jìn)制數(shù)據(jù)祭阀,實(shí)際上這些16進(jìn)制字節(jié)流數(shù)據(jù)是按照一定的格式去組合的鹉戚,它對(duì)應(yīng)的格式如下:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
其中u4代表的是4字節(jié)長(zhǎng)度,u2专控,代表的是2字節(jié)長(zhǎng)度抹凳,如此類推。
JDK也提供了javap
工具伦腐,可以把上面的16進(jìn)制二進(jìn)制流轉(zhuǎn)換成可讀的class結(jié)構(gòu)赢底,命令如下:
javap -verbose Test.class
-
魔法數(shù)
class文件最開(kāi)始4字節(jié)內(nèi)容是魔法數(shù),固定內(nèi)容CAFE BABE
柏蘑,所有class文件都是一樣幸冻,譬如例子里面的 -
版本號(hào)
跟在魔法數(shù)后面的是副版本號(hào)(2字節(jié)長(zhǎng)度)跟主版本號(hào)(2字節(jié)長(zhǎng)度),Demo里面的是jdk1.8.0
-
常量池
跟在版本號(hào)后面的是常量池長(zhǎng)度,用2字節(jié)長(zhǎng)度標(biāo)識(shí)革半,Demo這里是0x21趁啸,轉(zhuǎn)換成10進(jìn)制是33,意思是常量池里面有33種數(shù)據(jù)
每種常量類型對(duì)應(yīng)著不一樣的數(shù)據(jù)結(jié)構(gòu)赏胚,但不管是哪種常量,它的第一個(gè)字節(jié)都是tag字段商虐,用來(lái)表示此常量是什么類型觉阅,譬如Demo里面常量池里的第一個(gè)常量tag字段是0x0A,轉(zhuǎn)換成10進(jìn)制是10秘车,對(duì)應(yīng)的就是CONSTANT_Methodref_info
常量典勇,格式如下:
CONSTANT_Methodref_info {
u1 tag; //10
u2 class_index; //類索引
u2 name_and_type_index; //字段名索引
}
CONSTANT_Methodref_info
常量描述的正是構(gòu)造函數(shù)init
方法。當(dāng)類引用了一個(gè)外部方法時(shí)宫静,常量池里就會(huì)多一條CONSTANT_Methodref_info
常量數(shù)據(jù)走净。
跟在第一個(gè)CONSTANT_Methodref_info
常量后面的是0x9券时,對(duì)應(yīng)的是CONSTANT_Fieldref_info
常量,格式如下:
CONSTANT_Fieldref_info {
u1 tag; //9
u2 class_index; //類索引
u2 name_and_type_index; //方法名索引
}
每個(gè)字段的含義跟上面的CONSTANT_Methodref_info
結(jié)構(gòu)類似伏伯,其中0x14轉(zhuǎn)換成10進(jìn)制是20橘洞,0x15轉(zhuǎn)換成10進(jìn)制21,代表的也是常量池里面的索引id舵鳞,對(duì)照著上面的常量池表我們可以得出這條CONSTANT_Fieldref_info
常量數(shù)據(jù)描述的正是System
類的out
字段震檩。當(dāng)類引用了一個(gè)外部字段時(shí),常量池里就會(huì)多一條CONSTANT_Fieldref_info
常量數(shù)據(jù)蜓堕。
通過(guò)這種方式就可以把常量池里面的所有常量都解析出來(lái)了抛虏,這里只是拋磚引玉,介紹一下常量池的解析方式套才,剩下的常量解析這里就不再一一分析了迂猴。
-
訪問(wèn)標(biāo)志
跟在常量池后面是類訪問(wèn)標(biāo)志,Java提供了下面幾種標(biāo)志類型:
標(biāo)志名稱 | 標(biāo)志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 類型 |
ACC_FINAL | 0x0010 | final 類型 |
ACC_SUPER | 0x0020 | JDK1.0.2之后編譯出來(lái)的類默認(rèn)會(huì)帶上此標(biāo)志 |
ACC_INTERFACE | 0x0200 | 接口標(biāo)志 |
ACC_ABSTRACT | 0x0400 | abstract抽象類型標(biāo)志 |
ACC_SYNTHETIC | 0x1000 | 非代碼生成標(biāo)志 |
ACC_ANNOTATION | 0x2000 | 注解類型標(biāo)志 |
ACC_ENUM | x4000 | 枚舉類型標(biāo)志 |
-
類索引 父類索引 接口索引
在訪問(wèn)標(biāo)志后面是本類索引 父類索引以及接口索引,0x05是本類的索引id傻寂,意思是在常量池里的第5個(gè)位置有本類的索引信息息尺,對(duì)照著上面的常量池結(jié)構(gòu)可以知道本類正是com/nls/lib/Test
同理在常量池里的第6個(gè)位置是本類的父類索引信息,這里是java/lang/Object
由于Demo里的Test類并沒(méi)有實(shí)現(xiàn)任何接口疾掰,所以跟在后面的接口索引信息是空 -
字段表
跟在接口索引表后面是字段表搂誉,字段表記錄了類里面定義的所有的字段信息。首先是2字節(jié)長(zhǎng)度字段用來(lái)描述字段數(shù)静檬,Demo里面的Test類只有一個(gè)字段炭懊,所以這里是0x01
field_info {
u2 access_flags; //訪問(wèn)類型
u2 name_index; // 字段名索引
u2 descriptor_index; //字段簽名索引
u2 attributes_count; // 屬性數(shù)
attribute_info attributes[attributes_count]; //屬性表
}
字段的訪問(wèn)類型又有以下幾種
標(biāo)志名稱 | 標(biāo)志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 類型 |
ACC_PRIVATE | 0x0002 | private 類型 |
ACC_PROTECTED | 0x0004 | protected 類型 |
ACC_STATIC | 0x0008 | 靜態(tài)類型 |
ACC_FINAL | 0x0010 | final 類型 |
ACC_VOLATILE | 0x0040 | volatile 類型 |
ACC_TRANSTENT | 0x0080 | transient 類型 |
ACC_SYNCHETIC | 0x1000 | 編譯器自動(dòng)產(chǎn)生 |
ACC_ENUM | ACC_ENUM | 枚舉類型 |
Demo的Test類的a字段類型為public 對(duì)應(yīng)的值就是0x01,跟在后面的是名字索引跟簽名索引等等信息拂檩,代表的就是在常量池里面的索引號(hào)侮腹,如下:
對(duì)照著上面的常量池表結(jié)構(gòu),索引id 7的位置是a稻励,就是本字段名字父阻,索引id 8的位置是
I
代表著是本字段是int
類型,類里面每增加一個(gè)字段字段表里面就會(huì)多一條field_info
結(jié)構(gòu)數(shù)據(jù)
-
方法表
跟在字段表后面的是方法表钉迷,方法表記錄的是本類的所有方法信息至非,先用2字段長(zhǎng)度記錄方法表大小,這里是0x02糠聪,代表著Demo里面有兩個(gè)方法(除了代碼里面的test1
方法 還有編譯器自動(dòng)生成的init
方法)
在class字節(jié)碼里方法的定義結(jié)構(gòu)定義如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法結(jié)構(gòu)定義是跟字段結(jié)構(gòu)的定義是一樣的荒椭,這里重點(diǎn)介紹一下方法結(jié)構(gòu)里面的屬性表,方法是有方法實(shí)體的舰蟆,方法里面的代碼會(huì)以Code
屬性被存放在屬性表里趣惠。Code
屬性結(jié)構(gòu)定義如下:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
其中code
字段數(shù)組存放的就是編譯后的方法實(shí)體代碼了狸棍,code
里面也是一些列的字節(jié)流,這些16進(jìn)制的字節(jié)流對(duì)應(yīng)著一條條的jvm
指令
實(shí)戰(zhàn)
通過(guò)上面的分析味悄,我們對(duì)class的內(nèi)部結(jié)構(gòu)有了一定的了解了草戈,Java會(huì)把編譯后的類成員、類方法侍瑟、以及方法實(shí)體整理歸類好并且放到同一個(gè)地方去唐片,這大大的方便了代碼的檢索任務(wù),譬如需要搜索類成員只需要遍歷class字節(jié)碼的字段表就可以了涨颜,需要搜索某方法只需要遍歷class字節(jié)碼的方法表就可以了费韭,需要搜索類依賴了哪些外部類只需要遍歷常量池里的CONSTANT_Class_info
常量就可以 了。
Case one 類依賴掃描
手y在做32位uid轉(zhuǎn)64位任務(wù)時(shí)庭瑰,需要掃描出哪些地方使用了Uint32
類星持,前面介紹class文件結(jié)構(gòu)時(shí)我們已經(jīng)介紹了,當(dāng)類依賴了某個(gè)外部類時(shí)弹灭,常量池里就會(huì)有一條與之對(duì)應(yīng)的CONSTANT_Class_info
常量督暂,因?yàn)槲覀兛梢栽O(shè)計(jì)以下方案:
- 把整個(gè)項(xiàng)目的所有class字節(jié)碼讀取到
ClassPool
里面 - 遍歷
ClassPool
里面的所有class對(duì)象 - 遍歷class常量池里面的
CONSTANT_Class_info
常量,名字是Uint32
就是要查找目標(biāo)類
首先我們需要讀取.class文件穷吮,然后按照上面介紹的class文件結(jié)構(gòu)逻翁,逐字節(jié)的把class內(nèi)容解析出來(lái),class文件結(jié)構(gòu)比較復(fù)雜捡鱼,這里我們可以使用ASM的ClassReader或proguard的ProgramClassReader等等現(xiàn)成的解析邏輯卢未。
ClassPool
本質(zhì)上就是個(gè)Map結(jié)構(gòu),把讀取出來(lái)的class對(duì)象以key value的形式保存起來(lái)
public class ClassPool
{
private final TreeMap<String, Clazz> classes = new TreeMap<>();
public void addClass(Clazz clazz)
{
addClass(clazz.getName(), clazz);
}
public void classesAccept(ClassVisitor classVisitor)
{
Iterator iterator = classes.values().iterator();
while (iterator.hasNext())
{
Clazz clazz = (Clazz)iterator.next();
clazz.accept(classVisitor);
}
}
}
最后是遍歷所有class對(duì)象的常量池結(jié)構(gòu)堰汉,拿到CONSTANT_Class_info
常量后比較它的名字是否Uint32
即可,代碼大致如下:
/**
* Create by nls on 2022/5/30
* description: Uint32ClassMatcher
*/
class Uint32ClassMatcher(private val visitor: ClassVisitor) : ClassVisitor, ConstantVisitor {
override fun visitAnyConstant(clazz: Clazz?, constant: Constant?) {
}
override fun visitAnyClass(clazz: Clazz) {
clazz.constantPoolEntriesAccept(this)
}
override fun visitClassConstant(clazz: Clazz, classConstant: ClassConstant) {
val className = classConstant.getName(clazz)
if (className == "com/yy/mobile/yyprotocol/core/Uint32" ||
className == "tv/athena/live/streambase/services/core/Uint32"
) {
visitor.visitAnyClass(clazz)
}
}
}
//調(diào)用地方如下
fun execute(classPool: ClassPool) {
val memberMatcher = Uint32MemberMatcher()
val start = System.currentTimeMillis()
classPool.accept(AllClassVisitor(FilterClassVisitor(Uint32ClassMatcher(this))))
val end = System.currentTimeMillis()
println("scan finish total ${classPool.size()}, match: ${matchClassPool.size()}, cost: ${end-start}")
//matchClassPool.accept(AllClassVisitor(memberMatcher))
//memberMatcher.print()
}
最終掃描了總共四萬(wàn)多個(gè)類伟墙,匹配的類有一千多條翘鸭,整個(gè)掃描過(guò)程也是僅僅花了不到300毫秒的時(shí)間,效率可以說(shuō)是極高的Case two 方法依賴掃描
在研究proguard優(yōu)化時(shí)需要知道項(xiàng)目里哪些地方使用了反射去實(shí)例化類對(duì)象戳葵,反射實(shí)例化類有兩種方式就乓,一種是調(diào)java/lang/reflect/Constructor
的 newInstance
方法,另外一種是調(diào)用 java/lang/Class
的 newInstance
方法拱烁,前面我們已經(jīng)提到過(guò)了生蚁,當(dāng)一個(gè)類引用了外部類的某個(gè)方法是,class常量池里會(huì)有一條與之對(duì)應(yīng)的CONSTANT_Methodref_info
常量戏自,因此我們可以設(shè)計(jì)以下方案:
- 把整個(gè)項(xiàng)目的所有class字節(jié)碼讀取到
ClassPool
里面 - 遍歷
ClassPool
里面的所有class對(duì)象 - 遍歷class常量池里面的
CONSTANT_Methodref_info
常量邦投,名字是newInstance
并且類名是Constructor
或Class
類的就是要查找目標(biāo)類
由于原理跟代碼跟上面的類搜索相似,這里直接給出核心代碼
/**
* Create by nls on 2022/5/29
* description: ReflectionClassMatcher
*/
class ReflectionClassMatcher : ClassMatcher {
override fun match(clazz: Clazz, refConstant: MethodrefConstant): Boolean {
val className = refConstant.getClassName(clazz)
val methodName = refConstant.getName(clazz)
if (className == "java/lang/reflect/Constructor" || className == "java/lang/Class") {
return methodName == "newInstance"
}
return false
}
}
方法引用常量里面會(huì)有方法名跟類名的索引擅笔,我們只需要判斷下類名跟方法名便能找到自己想要的志衣,最終執(zhí)行效果如下屯援,掃描了四萬(wàn)多個(gè)類,四十多萬(wàn)個(gè)方法念脯,耗時(shí)才300多毫秒狞洋,效率是相當(dāng)?shù)捏@人的Case three 依賴鏈掃描
前面介紹的兩種掃描方式都比較簡(jiǎn)單,都是通過(guò)直接掃描常量池就可以達(dá)到效果了绿店,但掃描常量池只能得到有依賴某個(gè)外部類吉懊,某個(gè)外部方法等信息,卻并不能知道外部類或外部方法是被本類的哪些方法引入進(jìn)來(lái)的假勿,下面我們來(lái)分析下這種場(chǎng)景該如何進(jìn)行掃描借嗽。
上面的Demo類test1
方法依賴了println
方法废登,我們反編譯看下test1
方法的內(nèi)部指令
jvm
的指令集里淹魄,方法調(diào)用會(huì)用到invoke
系列指令(invokestatic invokespecial invokeinterface invokevirtual invokedynamic) 指令后面的操作數(shù)便是需要調(diào)用的方法在常量池里的索引甲锡。前面介紹class文件結(jié)構(gòu)時(shí)我們已經(jīng)提到過(guò)了,方法體編譯后的代碼會(huì)以二進(jìn)制流的形式被保存到Code
屬性里羽戒,因此我們可以設(shè)計(jì)以下方案:
- 掃描class常量池找到調(diào)用方法并且記錄下它的索引id
- 掃描class的方法表找到類的所有方法
- 掃描方法表里每個(gè)方法的Code屬性
- 遍歷Code屬性里面的所有指令集缤沦,找出invoke指令跟指令操作數(shù)
- 指令操作數(shù)為第一步掃描出來(lái)的索引id,那么就建立一條調(diào)用關(guān)系并且記錄
- 一直的遞歸繼續(xù)掃
手y的頻道模版入口是LiveTemplateView::onCreate
易稠,但是在調(diào)用到onCreate
前面有一套很復(fù)雜的上下滑框架缸废,假如我們并不熟悉那套框架的代碼邏輯,又想快速的找到進(jìn)頻道的調(diào)用邏輯是怎么樣的驶社,這時(shí)候我們就可以通過(guò)class掃描的方法把調(diào)用鏈給掃描出來(lái)
第一步我們先遍歷常量池企量,找到引用方法的索引id
override fun visitProgramClass(programClass: ProgramClass) {
kotlin.run {
//1.遍歷常量池,找到目標(biāo)調(diào)用方法在常量池里的索引id.
programClass.constantPool.forEachIndexed { index, constant ->
constant?.accept(programClass, this)
if (methodFind) {
methodRefIndex = index
return@run
}
}
}
//省略部分代碼
}
第二步遍歷方法表以及每個(gè)方法的Code屬性
override fun visitProgramClass(programClass: ProgramClass) {
//省略部分代碼
//2.遍歷方法表,事實(shí)上類里面可能有多個(gè)方法都調(diào)用了外部引入的方法,
//這里為了方便演示,找到一處調(diào)用就return. 只檢測(cè)一條的調(diào)用鏈。
kotlin.run {
programClass.methods.forEach {
//跳過(guò)橋接方法,免得引起死循環(huán).
if (it.accessFlags and AccessConstants.BRIDGE == 0) {
it.accept(programClass, this)
if (methodFind) {
listener.onFindMethod(programClass.name, it.getName(programClass))
return@run
}
}
}
}
}
override fun visitProgramMethod(programClass: ProgramClass, programMethod: ProgramMethod) {
//3.遍歷方法屬性表
programMethod.attributesAccept(programClass, this)
}
override fun visitCodeAttribute(clazz: Clazz, method: Method, codeAttribute: CodeAttribute) {
//4.我們只管Code屬性,其他屬性不管它.
codeAttribute.instructionsAccept(clazz, method, this)
}
第三步遍歷Code屬性里面的所有指令亡电,我們只關(guān)心常量指令届巩,如果常量指令的操作數(shù)為第一步索引到的id,那么此方法就是調(diào)用方法
override fun visitConstantInstruction(
clazz: Clazz,
method: Method,
codeAttribute: CodeAttribute,
offset: Int,
constantInstruction: ConstantInstruction
) {
//5. 我們只管常量指令,其他指令不管它.
if (constantInstruction.constantIndex == methodRefIndex) {
methodFind = true
}
}
最后把找出來(lái)的類名方法名作為新的參數(shù)一直遞歸掃描就可以把整個(gè)調(diào)用鏈給檢索出來(lái)了份乒,效果如下:總結(jié)
雖然某些場(chǎng)景下掃描項(xiàng)目代碼可以通過(guò)寫腳本的方式去實(shí)現(xiàn)恕汇,但基于class字節(jié)碼的掃描方式會(huì)更加快,效率更加高或辖,而且能做的事情更加廣瘾英,如敏感方法使用,無(wú)用代碼掃描等等颂暇,這些都是腳本方式無(wú)法實(shí)現(xiàn)的缺谴。
附錄
-
常量池常量結(jié)構(gòu)
CONSTANT_Class_info常量
CONSTANT_Class_info {
u1 tag;//7
u2 name_index;
}
CONSTANT_Fieldref_info常量
CONSTANT_Fieldref_info {
u1 tag; //9
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info常量
CONSTANT_Methodref_info {
u1 tag; //10
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info常量
CONSTANT_InterfaceMethodref_info {
u1 tag; //11
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_String_info常量
CONSTANT_String_info {
u1 tag; //8
u2 string_index;
}
CONSTANT_Integer_info常量
CONSTANT_Integer_info {
u1 tag; //3
u4 bytes;
}
CONSTANT_Float_info常量
CONSTANT_Float_info {
u1 tag; //4
u4 bytes;
}
CONSTANT_Long_info常量
CONSTANT_Long_info {
u1 tag; //5
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Double_info常量
CONSTANT_Double_info {
u1 tag; //6
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_NameAndType_info常量
CONSTANT_NameAndType_info {
u1 tag; //12
u2 name_index;
u2 descriptor_index;
}
CONSTANT_Utf8_info常量
CONSTANT_Utf8_info {
u1 tag; //1
u2 length;
u1 bytes[length];
}
CONSTANT_MethodHandle_info常量
CONSTANT_MethodHandle_info {
u1 tag; //15
u1 reference_kind;
u2 reference_index;
}
CONSTANT_MethodType_info常量
CONSTANT_MethodType_info {
u1 tag; //16
u2 descriptor_index;
}
CONSTANT_InvokeDynamic_info常量
CONSTANT_InvokeDynamic_info {
u1 tag; //18
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}