1、背景
话说jdk1.5以前字符串拼接如果使用“+”号时会消耗性能和内存,因为每次使用“+”号编译的时候都会转换出多个String对象(String对象是不可变的),所以都推荐使用StringBuilder。而jdk1.5之后,编译器对“+”号做了改进,会自动转换成StringBuilder再构造String对象,使得字符串拼接变得更方便,然而就代表着程序员可以随便使用"+"号而不考虑性能问题么?答案是:做梦吧。
2、“+”号照妖镜
既然我们所见的“+”号并不是“+”号,那我们通过反编译apk文件来看看“+”号的本质是什么?
首先先看简单的字符串拼接:
String result = "1111"+"2222"+"3333"+"4444";
编译后变成如下smali代码
const-string v0, "1111222233334444"
.local v0, "result":Ljava/lang/String;
这种直接拼接字符串常量的用法,编译器已经帮你合成好了,"1111"+"2222"+"3333"+"4444"跟"1111222233334444"编译后是一模一样的。
如果他们中出了个叛徒呢?把"2222"改成数字 2222
String result = "1111"+2222+"3333"+"4444";
编译后如下:
const-string v0, "1111222233334444"
.local v0, "result":Ljava/lang/String;
发现结果也是一样,说明编译器会把数字2222一起编译到同一个字符串中
StringBuilder正在热身.... ....
如果我们不在一行拼接完字符串,而是分几次拼接呢?
String result = "1111";
result = result+"2222";
result = result+"3333";
result = result+"4444";
编译后:
.line 26
const-string v0, "1111"
.line 27
.local v0, "result":Ljava/lang/String;
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v2, "2222"
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
.line 28
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v2, "3333"
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
.line 29
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v2, "4444"
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
额,“+”号现在是转换成了StringBuilder了,可是每个“+”号都做了 new StringBuilder、append、toString,三倍的操作,这种情况下会导致性能和内存倍增
如果我们把字符串变量在一行拼接起来呢?
String str2 = "2222";
String str3 = "3333";
String str4 = "4444";
String result = "1111"+str2+str3+str4;
编译后:
.line 48
const-string v0, "2222"
.line 49
.local v0, "str2":Ljava/lang/String;
const-string v1, "3333"
.line 50
.local v1, "str3":Ljava/lang/String;
const-string v2, "4444"
.line 51
.local v2, "str4":Ljava/lang/String;
new-instance v3, Ljava/lang/StringBuilder;
invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V
const-string v4, "1111"
invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
.line 52
.local v3, "result":Ljava/lang/String;
这种情况下编译后,只new 一个 StringBuilder,append 4次,toString一次
3、结论和建议
通过上面的源码和编译后smali比对后我们发现字符串相加的编译器处理有以下规律:
1、如果在一个表达式中连续“+”了多个字符串常量,编译器会把这些连续的字符串常量自动拼接成一个("1"+"2"+"3"+"4"==>"1234")
2、如果出现了数字常量,编译器会自动转换成字符串常量
3、如果在一个表达式中出现了字符串变量,这个时候编译器会new StringBuilder、append、toString,每个表达式至少都会走一遍这三个操作
4、如果在一个表达式码连续“+”多个字符串,只会new 一个 StringBuilder,多次append,且只有一次toString
建议如下:
1、如果可以提高代码可读性,一个表达式中的字符串常量相加并不会增加性能损耗,这个时候推荐用“+”
2、尽量将所有的字符串变量能在一个表达式中“+”完
3、如果在一个表达式“+”不完的情况下,推荐使用StringBuilder