- 方法鏈
- 位元組碼與 Smali 下的編譯結果
- 總結
方法鏈
方法鏈(Method Chaining),也被稱為命名引數法,是在物件導向的程式語言中呼叫的呼叫多個方法的通用語法。每一個方法返回一個物件,在一個單一的宣告裡,方法鏈省去了中間變數的需要。
當需要構建一個物件或者設定其初始屬性時,往往透過構造引數傳入或者 setter 方法。比如:
User user = new User("張三", 34);
// 或者
User user = new User();
user.setName("張三");
user.setAge(34);
但這樣存在一些缺點:
- 引數數量:很多情況下我們可能只需要一兩個引數,其餘引數保持預設。使用構造方法的話需要為各種情況進行宣告;
- 中間變數:如果建立後的物件直接被作為其他方法的引數,如果不使用構造方法設定引數,而是隻設定其中以一兩個個引數,那麼必須使用中間變數並透過 setter 實現。
方法鏈透過返回物件自身,便可以在設定一個引數後直接呼叫另一個引數而不必使用中間變數。方法鏈常與建造者模式(Builder)結合,比如 StringBuilder:
String str = new StringBuilder("a")
.append(1)
.append(3.14f)
.toString();
位元組碼與 Smali 下的編譯結果
Java 原始碼透過編譯為 JVM 可解釋的位元組碼(Byte Code)以便在 JVM 上執行。為了適應移動裝置的特點,Android 使用 DalvikVM 而不是 JVM,相應的位元組碼也需要轉變。DalvikVM 位元組碼碼反編譯過後就是 Smali 程式碼。
JVM 是基於棧的虛擬機器,而 DalvikVM 是基於暫存器的虛擬機器。相比之下,基於暫存器的虛擬機器效率更高,更適合在移動裝置上執行。另外,JVM 通常使用 JIT(Just In Time, 即時編譯)進行加速,而 DalvikVM 的繼承者 ART 則採用 AOT(Ahead Of Time, 提前編譯)加速。
對於以上 StringBuilder 的例子,使用 javac
編譯成位元組碼後,透過 javap -c
檢視,結果如下:
0: new #37 // class java/lang/StringBuilder
3: dup
4: ldc #50 // String a
6: invokespecial #52 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: iconst_1
10: invokevirtual #54 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
13: ldc2_w #57 // float 3.14f
16: invokevirtual #59 // Method java/lang/StringBuilder.append:(F)Ljava/lang/StringBuilder;
19: invokevirtual #46 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
可以看到,在第一條 invokevirtual
呼叫 append
方法之後會把其結果存到棧內,下一條 ldc2_w
則把常數 3.14 再存入棧內,此時棧頂有一個 StringBuilder 物件和一個 double。下一句 invokevirtual
則直接取棧頂兩個作為引數進行呼叫,並再把結果放入棧內。可以看到鏈式呼叫不僅在 Java 原始碼上可以簡化寫法,甚至在位元組碼上沒有額外增加指令。
透過 d8
將該位元組碼編譯為 .dex 檔案,再透過安卓逆向工具得到 Smali 程式碼:
new-instance v0, Ljava/lang/StringBuilder;
const-string v1, "a"
invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
.line 28
const/4 v1, 0x1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
.line 29
const v1, 0x4048f5c3 # 3.14f
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(F)Ljava/lang/StringBuilder;
move-result-object v0
.line 30
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
Smali 程式碼中可以看到很多暫存器,有一種看 MIPS 或者 ARM 彙編的感覺。可以看到每次透過 invoke-virtual
呼叫 append
方法後需要把返回值移動到暫存器 v0
,之後再新增引數,呼叫下一個 append
方法。注意,每次呼叫 invoke-virtual
時,第一個引數都是暫存器 v0
,而 v0
都來自於上次呼叫 append
後的返回值。而實際上在第一句 new-instance
之後,v0
始終為同一個 StringBuilder 物件。也就是說,此處的 move-result-object v0
是多餘的(除了最後一個呼叫 toString
之後的)。每一次呼叫 append
時,需要指定除 v0
外的另一個引數 v1
(透過 const
等語句),而呼叫之後則透過 move-result-object
將結果返回給 v0
,當 append
呼叫很多時,冗餘率達 1/3。
所以對 DalvikVM 好一點兒的寫法是:
StringBuilder bld = new StringBuilder("a");
bld.append(1);
bld.append(3.14f);
String str = bld.toString();
另外,對於字串拼接,原始碼中使的用加號(+
)實際上是語法糖,會被編譯成 StringBuilder
或者 StringBuffer
,而每次至少呼叫兩次 append
(因為脫糖之後不會在構造方法內傳入第一個字串,而是不帶參構造,兩次呼叫 append
進行拼接)和一次 toString
。透過 StringBuilder
在處理大量的拼接操作時固然有效,但是對於僅兩個字串的拼接,建議直接用 String.concat
方法以減少引數呼叫次數,同時效率也不打折扣。
總結
方法鏈固然帶來了很大的方便,但是對於安卓來說會在一定程度上帶來冗餘的 Dalvik 位元組碼,因此在做安卓開發時需要慎用。
原文地址:https://www.cnblogs.com/RainbowC0/p/18636744 ,未經作者許可禁止轉載