一、基本介紹
1.1、java的平臺無關(guān)性

- JAVA源代碼->Class字節(jié)碼->JVM解
釋執(zhí)?(依賴于不同的jvm實現(xiàn)跨平臺) - Java 虛擬機(JVM):負(fù)責(zé)將字節(jié)碼?
件翻譯成特定平臺下的機器碼然后運
?。 - 不同的平臺有不同的JVM實現(xiàn)
1.2、字節(jié)碼的語言無關(guān)性
字節(jié)碼(ByteCode)是構(gòu)成平臺無關(guān)性的基石,這也決定了,只要是能轉(zhuǎn)換成字節(jié)碼,其他語言也可以運行在jvm之上,所以現(xiàn)在不只是java,還有kotlin、groovy等都可以運行在jvm之上。

所以把JVM(Java Virtual Machine Java虛擬機)叫CVM(Class Virtual Machine Class虛擬機)反而更合適一些.
1.3、Class文件格式
Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個數(shù)據(jù)項目嚴(yán)格按照順序緊湊地排列在Class文件之中。
1.3.1、字節(jié)碼的兩種基本數(shù)據(jù)類型
Class文件結(jié)構(gòu)包含兩種數(shù)據(jù)類型:

- 無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1、u2、u4、u8來分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù),無符號數(shù)可以用來描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值。
- 表是由多個無符號數(shù)或者其他表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型,所有表都習(xí)慣性地以“_info”結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),整個Class文件本質(zhì)上就是一張表。
1.3.2、Class文件表構(gòu)成

1.3.3、編譯測試
編譯下面這個類
public class JavaCodeTest {}
然后把得到的.class用16進(jìn)制編輯器直接打開字節(jié)碼顯示是這樣的:

可以看到開頭的四個字節(jié)是
CAFEBABE,后面的字節(jié)可以意思對應(yīng)上方表格進(jìn)行查看,都是嚴(yán)格按照順序一一對應(yīng)的。
1.4、常用屬性介紹
在Class文件、字段表、方法表都可以攜帶自己的屬性表集合,以用于描述某些場景專有的信息。任何人實現(xiàn)編譯器都可以向?qū)傩员碇袑懭胱约鹤远x的屬性信息,但是java虛擬機運行時會忽略掉他不認(rèn)識的屬性。
《Java虛擬機規(guī)范(Java SE 7)》版中,預(yù)定義屬性已經(jīng)增加到21項,如下表所示:

1.4.1、Code屬性
Java程序方法體中的代碼經(jīng)過Javac編譯器處理后,最終變?yōu)樽止?jié)碼指令存儲在Code屬性內(nèi)。Code屬性出現(xiàn)在方法表的屬性集合之中,但并非所有的方法表都必須存在這個屬性,譬如接口或者抽象類中的方法就不存在Code屬性。
Code屬性是Class文件中最重要的一個屬性,如果把一個Java程序中的信息分為代碼(Code,方法體里面的Java代碼)和元數(shù)據(jù)(Metadata,包括類、字段、方法定義及其他信息)兩部分,那么在整個Class文件中,Code屬性用于描述代碼,所有的其他數(shù)據(jù)項目都用于描述元數(shù)據(jù)。了解Code屬性是學(xué)習(xí)關(guān)于字節(jié)碼執(zhí)行引擎內(nèi)容的必要基礎(chǔ),能直接閱讀字節(jié)碼也是工作中分析Java代碼語義問題的必要工具和基本技能。
比如這個類
public class JavaCodeTest {
private int a;
public int testAdd() {
return a + 1;
}
}
編譯后的方法部分如下所示(下面這種為轉(zhuǎn)義過的,未轉(zhuǎn)義的都如上面介紹的那樣,為16進(jìn)制形式)
如下所示,具體每一個字段的釋義可以參考注釋。
{
//---略
public com.canzhang.asmdemo.test.JavaCodeTest();
//descriptor 對方法參數(shù)和返回值進(jìn)行描述
descriptor: ()V//`()`表示無參數(shù),`V`表示Void,無返回值
flags: ACC_PUBLIC//方法修飾符,表示是public的,可以有多個。
Code://方法體
stack=1, locals=1, args_size=1//參數(shù)個數(shù)
//aload_0指令表示:將第0個Slot中為reference類型的本地變量推送到操作數(shù)棧頂。
0: aload_0
//invokespecial指令表示:以棧頂?shù)膔eference類型的數(shù)據(jù)所指向的對象作為方法接收者,調(diào)用此對象的實例構(gòu)造器方法
1: invokespecial #1 // 這里的`#1`是invokespecial指令的參數(shù),表示指向常量池聲明的常量:Method java/lang/Object."<init>":()V
//指令return,含義是返回此方法,并且返回值為void。這條指令執(zhí)行后,當(dāng)前方法結(jié)束。
4: return
//LineNumberTable 是用來描述Java源碼行號與字節(jié)碼行號(字節(jié)碼偏移量)之間的對應(yīng)關(guān)系,可以配置不生成,不生成就無法獲取異常發(fā)生源碼行號,也無法按照源碼的行數(shù)來調(diào)試程序。
LineNumberTable:
line 6: 0
public int testAdd();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 10: 0
}
//---略
疑問點:
- 第一個方法
<init>()是什么?
java在編譯之后會在字節(jié)碼文件中生成<init>方法,稱之為實例構(gòu)造器 - 實例構(gòu)造器<init>()和testAdd(),這兩個方法很明顯都是沒有參數(shù)的,為什么Args_size會為1?而且無論是在參數(shù)列表里還是方法體內(nèi),都沒有定義任何局部變量,那Locals又為什么會等于1?
在任何實例方法里面,都可以通過“this”關(guān)鍵字訪問到此方法所屬的對象。這個訪問機制對Java程序的編寫很重要,而它的實現(xiàn)卻非常簡單,僅僅是通過Javac編譯器編譯的時候把對this關(guān)鍵字的訪問轉(zhuǎn)變?yōu)閷σ粋€普通方法參數(shù)的訪問,然后在虛擬機調(diào)用實例方法時自動傳入此參數(shù)。因此在實例方法的局部變量表中至少會存在一個指向當(dāng)前對象實例的局部變量,局部變量表中也會預(yù)留出第一個Slot位來存放對象實例的引用,方法參數(shù)值從1開始計算。這個處理只對實例方法有效,如果testAdd()方法聲明為static,那Args_size就不會等于1而是等于0了。
附錄:
生成字節(jié)碼樣例參考
java文件
public class JavaCodeTest {
private int a;
public int testAdd() {
return a + 1;
}
}
//生成字節(jié)碼
javac /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.java
//反編譯字節(jié)碼
javap -v /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.class
反編譯結(jié)果
Classfile /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.class
Last modified 2020-11-5; size 311 bytes
MD5 checksum 985828dc144886121bd06e4d19423d65
Compiled from "JavaCodeTest.java"
public class com.canzhang.asmdemo.test.JavaCodeTest
minor version: 0//jdk次版本號
major version: 52//jdk主版本號
flags: ACC_PUBLIC, ACC_SUPER
Constant pool://常量池
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/canzhang/asmdemo/test/JavaCodeTest.a:I
#3 = Class #17 // com/canzhang/asmdemo/test/JavaCodeTest
#4 = Class #18 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code//屬性的名稱 對于每個屬性,它的名稱需要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示
#10 = Utf8 LineNumberTable//屬性的名稱
#11 = Utf8 testAdd
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 JavaCodeTest.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // a:I
#17 = Utf8 com/canzhang/asmdemo/test/JavaCodeTest
#18 = Utf8 java/lang/Object
{
public com.canzhang.asmdemo.test.JavaCodeTest();
descriptor: ()V
flags: ACC_PUBLIC
Code://方法體
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
public int testAdd();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 10: 0
}
常用的幾個指令
- .java--->.class:
javac /xxxx/JavaCodeTest.java - .class 轉(zhuǎn)換字節(jié)碼內(nèi)容:
javap -c /xxxx/JavaCodeTest.class
用法: javap <options> <classes>
其中, 可能的選項包括:
-help --help -? 輸出此用法消息
-version 版本信息
-v -verbose 輸出附加信息
-l 輸出行號和本地變量表
-public 僅顯示公共類和成員
-protected 顯示受保護(hù)的/公共類和成員
-package 顯示程序包/受保護(hù)的/公共類
和成員 (默認(rèn))
-p -private 顯示所有類和成員
-c 對代碼進(jìn)行反匯編
-s 輸出內(nèi)部類型簽名
-sysinfo 顯示正在處理的類的
系統(tǒng)信息 (路徑, 大小, 日期, MD5 散列)
-constants 顯示最終常量
-classpath <path> 指定查找用戶類文件的位置
-cp <path> 指定查找用戶類文件的位置
-bootclasspath <path> 覆蓋引導(dǎo)類文件的位置
對應(yīng)android javac,并不能保證所有都正常編譯,因為有很多android sdk的內(nèi)容是識別不了的,另外android編譯過程中也會有自己一些額外處理(比如脫糖、插樁一類的),所以最好直接使用android工程編譯后的產(chǎn)物也進(jìn)行字節(jié)碼分析,一般工程如下圖目錄已經(jīng)存在了編譯后的.class文件,我們可以直接取用.class 使用javap轉(zhuǎn)換成可以讀懂的文字內(nèi)容就可以分析了。
具體目錄:app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/xxx

android studio 便捷命令配置
為了方便我們使用javap,可以在android studio 配置tool,方便我們使用:

如圖示 依次打開 tools-external tools -點擊左下角的添加按鈕,按照箭頭輸入幾個關(guān)鍵項就可以了:
- Name:隨便填寫
- Program:
$JDKPath$\bin\javap - Arguments:
-c $FileClass$ - Working directory:
$OutputPath$
然后點擊 ok apply 就可以使用了,使用方法,如下圖所示:
image.png
注意事項:
- 使用的時候默認(rèn)獲取的是當(dāng)前打開的類作為輸入入?yún)?,比如你想?code>MainActivity.java對應(yīng)的字節(jié)碼文件,那么就打開
MainActivity.java就可以了 - 需要工程編譯后才能按照配置的路徑,找到對應(yīng)的.class文件。
- 如果編譯了,依然提示文件不存在,上面的配置項可以嘗試清空,使用右側(cè)的 insert 去插入對應(yīng)路徑,上面的幾個路徑,都是有選項可選的,按照名字選擇即可。
字節(jié)碼查看器
Hex Fiend
下載后直接雙擊打開,就可以看到對應(yīng)的字節(jié)碼,選中還可以高亮對應(yīng)。

未完待續(xù).....
參考文章
kotilin中的某些特性 :https://juejin.im/post/6844903588716609543
反射原理
