字节码层面看switch

支持的类型

switch支持char、byte、short、int、Character、Byte、Short、Integer、String、Enum十种类型,其中对Emun与String的支持是在JDK1.5和JDK1.7的时候增加的,同时switch并不支持long、double、float。

基础类型的Switch

switch最终会被编译为tableswitchlookupswitch两个指令,且tableswitchlookupswitch仅支持int数据,这也就是为什么switch不支持long、double、float的原因,因为这三种类型无法转换为int。

tableswitch

switch内的case序列能被表示为一个表中的索引值时,则使用tableswitch。即case的值是连续的,那么就可以使用tableswitch。底层实现是维护一个int数组,下标是case值,数组的值则是转跳的偏移量,这样做效率非常高,时间复杂度是O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void intSwitch() {
int a = 1;
switch (a) {
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
case 3:
System.out.println("3");
break;
}
}

// access flags 0x2
private intSwitch()V
L0
ICONST_1
ISTORE 1
L1
ILOAD 1
TABLESWITCH
1: L2 // L2-L5为具体的执行逻辑
2: L3
3: L4
default: L5

还有一种情况,当case序列并不是连续分布,但是也并不是很稀疏,编译器会通过补上中间空缺的值将它变成一个连续的序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void intSwitch() {
int a = 1;
switch (a) {
case 101:
System.out.println("1");
break;
case 102:
System.out.println("2");
break;
break;
case 104:
System.out.println("4");
break;
}
}

private intSwitch()V
L0
ICONST_1
ISTORE 1
L1
ILOAD 1
TABLESWITCH
101: L2
102: L3
103: L5 // 编译器补上了case = 10,跳转逻辑和default相同
104: L4
default: L5

lookupswitch

switch内的case序列分布很稀疏,强行使用tableswitch会导致序列过长占用大量空间,这时候会使用lookupswitchlookupswitch会将case的值和转跳的偏移量保存在一个Map里,当lookupswitch被执行的时候,将目标值和这个Map里的keys逐一对比找到符合的偏移量跳转,没有找到则使用default。当然编译器会对keys进行排序,所以搜索方式应该是通过二分(猜的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void intSwitch() {
int a = 1;
switch (a) {
case 101:
System.out.println("1");
break;
case 1:
System.out.println("2");
break;
case 1000:
System.out.println("3");
break;
}
}
private intSwitch()V
L0
ICONST_1
ISTORE 1
L1
ILOAD 1
LOOKUPSWITCH
1: L2
101: L3
1000: L4
default: L5

如何选择

这部分的逻辑可以看javac的源码Gen.java

1
2
3
4
5
6
7
8
9
10
11
12
// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
lookup_space_cost + 3 * lookup_time_cost
?
tableswitch : lookupswitch;

大概就是空间和时间上的权衡

String的Switch

由于tableswitchlookupswitch只支持int,所以String的switch逻辑本质还是int的switch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
private void stringSwitch() {
String a = "";
switch (a) {
case "AA":
System.out.println("Test1");
break;
case "BB":
System.out.println("Test2");
break;
case "Test3":
System.out.println("Test3");
break;
}
}

// access flags 0x2
private stringSwitch()V
L0
LDC ""
ASTORE 1
L1
ALOAD 1
ASTORE 2
ICONST_M1
ISTORE 3
ALOAD 2
INVOKEVIRTUAL java/lang/String.hashCode ()I
LOOKUPSWITCH
2080: L2
2112: L3
80698817: L4
default: L5
L2
ALOAD 2
LDC "AA"
INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
IFEQ L5
ICONST_0
ISTORE 3
GOTO L5
L3
ALOAD 2
LDC "BB"
INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
IFEQ L5
ICONST_1
ISTORE 3
GOTO L5
L4
ALOAD 2
LDC "Test3"
INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
IFEQ L5
ICONST_2
ISTORE 3
L5
ILOAD 3
TABLESWITCH
0: L6
1: L7
2: L8
default: L9
L6
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Test1"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L10
GOTO L9
L7
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Test2"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L11
GOTO L9
L8
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Test3"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L9
RETURN

String的逻辑并不只是简单的替换为int的switch,接下来讲一下这里的逻辑

  1. 获取目标String的hashCode作为switch的目标值,取所有case对应String的hashCode作为case序列,可以理解为如下伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void stringSwitch() {
String a = "";
switch (a.hashCode()) {
case "AA".hashCode():
System.out.println("Test1");
break;
case "BB".hashCode():
System.out.println("Test2");
break;
case "Test3".hashCode:
System.out.println("Test3");
break;
}
}
  1. 使用以上生成的目标值与case序列生成tableswitch或者lookupswitch指令
  2. case对应的执行逻辑中通过String#equals判断目标String与当前case分支对应的String是否相等,如果相等则会在本地变量表中存储一个下标,下标值为当前分支在switch中位置,默认该下标为-1。
  3. 执行一个tableswitch,获取到之前在本地变量表中存储的下标作为目标值,case序列则为[0,n-1],其中n为switch的分支数量,这个tableswitch的作用是跳转到真正的执行逻辑。

目标值的hashCode冲突

由于String的hashCode是存在重复的,所以可能存在某个字符串与匹配的case对应字符串并不相同,但是hashCode相同导致成功匹配,这样会造成执行预期之外的逻辑。所以在第一个switch里hashCode匹配之后还需要对字符串进行比较,如此完成匹配后才会通过后续的一个tableswtch执行真正逻辑。

case的hashCode冲突

case本身也存在hashCode的冲突,如”Aa”与”BB”的hashCode都是2112,生成的字节码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    private void stringSwitch() {
String a = "";
switch (a) {
case "Aa":
System.out.println("Test1");
break;
case "BB":
System.out.println("Test2");
break;
case "Test3":
System.out.println("Test3");
break;
}
}

// access flags 0x2
private stringSwitch()V
L0
LDC ""
ASTORE 1
L1
ALOAD 1
ASTORE 2
ICONST_M1
ISTORE 3
ALOAD 2
INVOKEVIRTUAL java/lang/String.hashCode ()I
LOOKUPSWITCH
2112: L2
80698817: L3
default: L4
L2
ALOAD 2
LDC "BB"
INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
IFEQ L5
ICONST_1
ISTORE 3
GOTO L4
L5
ALOAD 2
LDC "Aa"
INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
IFEQ L4
ICONST_0
ISTORE 3
GOTO L4
L3
ALOAD 2
LDC "Test3"
INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
IFEQ L4
ICONST_2
ISTORE 3
L4
ILOAD 3
TABLESWITCH
0: L6
1: L7
2: L8
default: L9
L6
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Test1"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L10
GOTO L9
L7
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Test2"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L11
GOTO L9
L8
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Test3"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L9
RETURN

可以看出生成的lookupswitch只包含两个case,”Aa”与”BB”执行同一个逻辑,而在逻辑内部会将目标字符串分别与”Aa”、”BB”进行比较以计算后续tableswitch中的目标值,其他逻辑不变。

枚举的Switch

枚举并不是单纯的使用ordinal()获取int然后生成switch,因为这样并不能保证生成tableswitch,所以做了一些额外操作保证生成tableswitch
编译器会生成一个int[]数据,长度为枚举的长度,然后将switch中的case按顺序取ordinal()值作为下标加到数组中,value为在switch中的顺序。通过目标枚举的ordinal()从数组中获取index,该index则为后续tableswitch的目标值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private enum E {
a, b, c
}

private void enumSwitch() {
E a = E.a;
switch (a) {
case a:
System.out.println("");
break;
case c:
System.out.println("");
break;
}
}

static final /* synthetic */ int[] $SwitchMap$M$E = new int[E.values().length];

static {
try {
$SwitchMap$M$E[E.a.ordinal()] = 1;
} catch (NoSuchFieldError e) {
}
try {
$SwitchMap$M$E[E.c.ordinal()] = 2;
} catch (NoSuchFieldError e3) {
}
}

// access flags 0x2
private enumSwitch()V
L0
GETSTATIC M$E.a : LM$E;
ASTORE 1
L1
GETSTATIC M$1.$SwitchMap$M$E : [I
ALOAD 1
INVOKEVIRTUAL M$E.ordinal ()I
IALOAD
LOOKUPSWITCH
1: L2
2: L3
default: L4

Dex对Switch的优化

开发过程中解包看生成的字节码是否正确的时候,发现生成的switch语句和预期的不太一样,但是逻辑上是一致的,而且看起来更加高效,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 原始代码
public void test() {
int a = 0;
switch (a) {
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
case 3:
System.out.println("3");
break;
case 100:
System.out.println("100");
break;
}
}

// apk反编译代码
public void test() {
if (0 != 100) {
switch (0) {
case 1:
System.out.println("1");
return;
case 2:
System.out.println("2");
return;
case 3:
System.out.println("3");
return;
default:
return;
}
} else {
System.out.println("100");
}
}

可以看出四个case中连续的三个独自组成了一个switch,而另一个case使用if语句来进行判断,这样的好处就是原本的lookupswitch变成了tableswitch。最开始怀疑是proguard做的优化,后来发现debug下也是如此,就开始找生成dex的相关代码,最后发现相关逻辑在CodeRewriter中,其中的rewriteSwitch方法负责优化switch逻辑,大概逻辑如下

  1. 如果switch中只有一个分支,则转换为if语句
  2. 如果存在多个分支,通过遍历排序后的case序列,生成两个变量sequencesoutliers
1
2
List<IntList> sequences = new ArrayList();
IntList outliers = new IntArrayList();

sequences为case中连续序列的合集,outliers则是零散case的合集,举个例子

1
2
3
4
5
6
7
8
switch (a) {
case 1: break;
case 20: break;
case 21: break;
case 50: break;
case 101: break;
case 102: break;
}

以上switch语句将生成如下的sequences和outliers

1
2
sequences = [{20, 21}, {101, 102}]
outliers = {1, 50}
  1. 收集到连续序列与零散点后,可能存在以下两种优化
  2. 为sequences中每一个sequence创建tableSwitch,为outliers中每一个case创建if语句
  3. 为sequences中每一个sequence创建tableSwitch,为outliers所有点创建一个lookupswitch
    是否采取优化与具体采用那种优化方式主要依赖于优化后预计生成指令的个数和大小,具体的策略可以参考具体的代码逻辑。

生成Switch

了解以上内容后生成switch语句问题就不大了,但是自己写太麻烦,一番搜索后发现AGP提供了一个StringSwitch.java类可以让我们快捷的生成String的switch逻辑,不过它只处理了case的冲突情况并没有处理目标值的冲突,需要我们自己处理。

参考链接