前言
在Android
官方文档推出性能优化的时候,从一开始有这样一段说明:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
意思是说在 Android 平台上 avoid
使用枚举,因为枚举类比一般的静态常量多占用两倍的空间。
由于枚举最终的实现原理还是类,在编译完成后,最终为每一种类型生成一个静态对象,而在内存申请方面,对象需要的内存空间远大于普通的静态常量,而且分析枚举对象的成员变量可知,每一个对象中默认都会有一个字符数组空间的申请,计算下来,枚举需要的空间远大于普通的静态变量。
如果只是使用枚举来标记类型,那使用静态常量确实更优,但是现在翻看官方文档发现,这个建议已经被删除了,这是为什么那 ? 具体看 JakeWharton 在 reddit 上的一个评论
The fact that enums are full classes often gets overlooked. They can implement interfaces. They can have methods in the enum class and/or in each constant. And in the cases where you aren't doing that, ProGuard turns them back into ints anyway.
The advice was wrong for application developers then. It's remains wrong now.
最重要的一句是
ProGuard turns them back into ints anyway.
在开启 ProGuard 优化的情况下,枚举会被转为int
类型,所以内存占用问题是可以忽略的。具体可参看 ProGuard 的优化列表页面 Optimizations Page,其中就列举了 enum 被优化的项,如下所示:
class/unboxing/enum
Simplifies enum types to integer constants, whenever possible.
ProGuard
官方出了一篇文章 ProGuard and R8: Comparing Optimizers(大致意思就是自己比R8强 ),既ProGuard
会把枚举优化为整形.但是安卓抛弃了了ProGuard
,而是使用了R8
作为混淆优化工具。我们重点看下R8对枚举优化的效果如何 ?
R8对枚举优化
下面通过以下例子验证一下在真实的开发环境中R8
对枚举优化的支持效果。 代码如下:
- 定义一个简答枚举类
Language
arduino
package com.example.enum_test;
public enum Language {
English("en", "英文"), Chinese("zh", "中文");
String webName;
String zhName;
Language(String webName, String zhName) {
this.webName = webName;
this.zhName = zhName;
}
}
- 在
MainActivity
主要代码
typescript
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Language language = null;
if (Math.random() < 0.5) {
doEnumAction(Language.English);
} else {
doEnumAction(Language.Chinese);
}
// doNumberAction(CHINESE);
}
private void doEnumAction(Language language) {
switch (language) {
case English:
System.out.println("english ");
break;
case Chinese:
System.out.println("chinese");
break;
}
System.out.println(language.name());
}
3.build.gradle.kts
文件内开启混淆
ini
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
- 将编译后的apk反编译结果如下(枚举类被优化):
以上结果可以看出,如果是一个简单的枚举类,那么枚举类将会被优化为一个整形数字。既然ProGuard/R8
会把枚举优化为整形,那是不是在Android
中,就可以继续无所顾忌的使用枚举了呢? 我没有找到官方对R8枚举具体的优化场景说明 ,只找了ProGuard
对枚举的优化有一定的限制条件,如果枚举类存在如下的情况,将不会有优化为整形,如下所示:
- 枚举实现了自定义接口。并且被调用。
- 代码中使用了不同签名来存储枚举。
- 使用
instanceof
指令判断。 - 使用枚举加锁操作。
- 对枚举强转。
- 在代码中调用静态方法
valueOf方法
。 - 定义可以外部访问的方法。
参考自:ProGuard 初探 · dim's blog,另外,上面的这七种情况,我并没有找到官方的说明,如果有哪位读者知道,请在评论区里留下链接,谢谢啦~
下面我们对以上的情况进行追一验证,看下这些条件是否也会对R8
编译优化产生限制 , 如下 :
- 枚举实现了自定义接口,并且被调用。
typescript
public interface ILanguage {
int getIndex();
}
public enum Language implements ILanguage{
English("en", "英文"), Chinese("zh", "中文");
String webName;
String zhName;
Language(String webName, String zhName) {
this.webName = webName;
this.zhName = zhName;
}
@Override
public int getIndex() {
return this.ordinal();
}
}
// 调用如下
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ILanguage iLanguage = Language.Chinese;
System.out.println(iLanguage.getIndex());
}
反编译结果如下(枚举类被优化):
- 代码中使用了不同签名来存储枚举。
在对以下代码调用的时候,使用一个变量保存枚举值,由于currColor
变量声明类型的不同, 导致枚举的优化结果也不同
arduino
// 枚举会被优化
Signal currColor = Signal.RED;
//发生了类型转换,变量签名不一致,枚举不会被优化
Object currColor = Signal.RED;
public void change(Signal color) {
switch (color) {
case RED:
currColor = Signal.GREEN;
break;
case GREEN:
currColor = Signal.YELLOW;
break;
case YELLOW:
currColor = Signal.RED;
break;
}
}
protected void onCreate(Bundle savedInstanceState) {
double random = Math.random();
if (random > 0.5f) {
change(GREEN);
} else if (random > 0.5 && random < 0.7) {
change(RED);
} else {
change(Signal.YELLOW);
}
// 最终也是被优化为if语句
//switch (currColor) {
// case RED:
// System.out.println("红灯");
// break;
// case GREEN:
// System.out.println("绿灯");
// break;
// case YELLOW:
// System.out.println("黄灯");
// break;
//}
if (currColor == RED) {
System.out.println("红灯");
} else if (currColor == GREEN) {
System.out.println("绿灯");
} else if (currColor == YELLOW) {
System.out.println("黄灯");
}
}
Signal currColor = Signal.RED;
时 ,枚举被优化整数
Object currColor = Signal.RED;
时 ,枚举未被优化
- 使用
instanceof
指令判断。 (发生了类型转换)
typescript
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean result = getObj() instanceof Language;
System.out.println(result);
}
Language getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}
反编译结果如下(枚举类未被优化):
- 使用枚举加锁操作。
erlang
synchronized (Language.Chinese) {
System.out.println("synchronized");
}
从反编译结果如下(枚举类未被优化):
可以看到在该场景下枚举类没有被优化。
- 不要作为一个输出或打印对象
csharp
System.out.println(RED);
从反编译结果如下(枚举类未被优化):
- 对枚举强转。 比如下代码不会出现枚举优化
typescript
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean result = (getObj()) != null;
System.out.println(result);
}
// 如果返回值类型和返回的枚举类型不一致时,也不会优化枚举。
@Nullable
Object getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}
反编译结果如下:(枚举类未被优化):
如果把返回值修改为Language
则会发生优化
typescript
@Nullable
Language getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}
反编译结果如下:(枚举类被优化):
以下代码也会出现枚举被优化,把方法的返回值类型修改为 Language ,接收变量类型改为 Object
typescript
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Object language = getObj();
boolean result= language != null;
System.out.println(result);
}
@Nullable
Language getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}
- 定义可以外部访问的方法。 R8对枚举的优化并不受定义外部方法的影响,如下在枚举内定义
getLanguage方法
后,枚举仍被优化
typescript
package com.example.enum_test;
import androidx.annotation.Nullable;
public enum Language {
English("en", "英文"), Chinese("zh", "中文");
String webName;
String zhName;
Language(String webName, String zhName) {
this.webName = webName;
this.zhName = zhName;
}
@Nullable
public Language getLanguage(String name) {
if (English.webName.equals(name)) {
return Language.English;
} else {
return null;
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Language language = Language.getLanguage(Math.random() > 0.5f ? "en" : "zh");
boolean result= language != null;
System.out.println(result);
}
apk反编译结果如下(枚举被优化):
复杂多变的枚举优化
在测试中发现一个问题 ,同样的代码,放在不同的文件内,优化效果竟然也不同。
- 在
MainActivity
定义如下方法
arduino
public void change(Signal color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
break;
}
}
public void test() {
double random = Math.random();
if (random > 0.5f) {
change(GREEN);
} else if (random > 0.5 && random < 0.7) {
change(RED);
} else {
change(YELLOW);
}
if (currColor == RED) {
System.out.println("红灯");
} else if (currColor == GREEN) {
System.out.println("绿灯");
} else {
System.out.println("黄灯");
}
}
并在onCreate
方法内执行, 从下面的反编译结果中可以看到枚举被优化了。
- 相同的代码如果定义在
TrafficLight
类中, 并在MainActivity
的onCreate
方法中运行 ,如下:
arduino
package com.example.enum_test;
import static com.example.enum_test.TrafficLight.Signal.GREEN;
import static com.example.enum_test.TrafficLight.Signal.RED;
import static com.example.enum_test.TrafficLight.Signal.YELLOW;
public class TrafficLight {
enum Signal {
GREEN, YELLOW, RED;
}
private Signal currColor = RED;
public void change(Signal color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
}
}
public void test() {
double random = Math.random();
if (random > 0.5f) {
change(GREEN);
} else if (random > 0.5 && random < 0.7) {
change(RED);
} else {
change(YELLOW);
}
if (currColor == RED) {
System.out.println("红灯");
} else if (currColor == GREEN) {
System.out.println("绿灯");
} else {
System.out.println("黄灯");
}
}
}
// onCreate 内执行
TrafficLight trafficLight = new TrafficLight();
trafficLight.test();
从上面的对比中发现,相同的枚举代码操作放在Activity
和 放在普通类中 ,编译结果是不同的 。 导致这种问题的原因还是因为Activity
默认是被配置了混淆的, 而TrafficLight
默认是没有被配置混淆的,编译器会对TrafficLight
进行一系列的编译优化和函数内敛等处理,导致枚举为优化。如果对TrafficLight
进行配置防混淆配置后,结果如下:
配置 -keep class com.example.enum_test.TrafficLight
效果
配置#-keepclassmembernames class com.example.enum_test.TrafficLight
效果
引用代码
如果对具有引用枚举类型变量的类进行了混淆配置处理TrafficLight
内的枚举引用也全部被优化为了整数类型。
从上面的反编译结果中可以看到, 如果未对TrafficLight
类进行混淆配置,这个类的相关成员可能会被抽取到一个公共类里。 currColor
就是m0
类f1749b
属性, 该属性是一个Object
类型,这也是可能是导致枚举未完全优化为整数的原因, 从 m0 的代码中可以看到编译器将多个实例的构造统一只使用了一个Object
作为引用, 方法也被编译到m0
类内部,可以看到m0类不是一个TrafficLight
,猜测这也是编译器在对枚举进行整型优化和枚举持有类优化一种权衡和选择吧 ,说明类优化要比枚举优化收益更高。
枚举 、常量
从编译结果来看,枚举由于会构建多个静态对象
和ordinal()
、values()
等函数和变量的存在,确实会比普通的静态对象或常量更加占用空间和内存。但是从上面的测试结果中可以看到 ,枚举在最佳情况下可以被优化为整型,达到和常量一样的效果。
总结
以下场景都会阻止枚举优化 :
- 使用
instanceof
指令判断。 - 使用枚举作为锁对象操作
- System.out.println(enum) 输出
- 枚举作为返回值返回时,返回参数的声明类型与枚举不一致,请参考 例6
- 混淆优化配置影响枚举优化, 如果一个类中有变量是一个枚举类型,如果该类未在proguard-rules.pro配置混淆优化处理,该类则可能会被编译器优化掉,其变量和方法会被抽取到一个公共类或者内敛到引用类里, 且枚举类不会被优化,因为枚举变量公共类被一个
Object
类型变量引用持有。 - 常规的枚举使用,R8都会对枚举进行一定程度的优化,最好的情况下会优化成一个整数常量,性能几乎不会有任何影响。
我的理解是如果我们可以通过定义普通常量的方式代替枚举,则优先通过使用定义常量解决。权衡易用性和性能以及使用场景,可以考虑继续使用枚举,因为枚举在有些时候确实让代码更简洁,更容易维护,牺牲点内存也无妨。况且Android官方自己也在许多地方应用了枚举,例如 Lifecycle.State
、 Lifecycle.Event
等 。
小彩蛋
前几天群里在讨论 京东金融Android瘦身探索与实践 文章,内容中一点优化是关于枚举的 。
我感觉他们以这个例子没有很强的说服力,原因如下 :
- 如果对持有枚举变量的类或者变量进行混淆配置后 ,编译器会对枚举进行优化 ,
TrafficLight
内枚举的引用被替换为整数,从反编译结果可以看到优化后的代码就是普通的if语句,并不会出现所谓的占用大量体积的情况。
- 如果枚举相关类未进行完全优化,但是例子中的
change()
方法并不会导致大量增加包体 ,只是增加了4行字节码指令
。但是枚举的定义的确会占用一定的包体积大小,这个毋庸置疑。
使用枚举实现以及编译后字节码如下 :
csharp
public class TrafficLight {
enum Signal {
GREEN, YELLOW, RED;
}
private Signal currColor = RED;
public void change(Signal color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
}
}
}
// 22行字节码指令
.method public change(Lb1/a;)V
.registers 3
invoke-virtual {p1}, Ljava/lang/Enum;->ordinal()I
move-result p1
if-eqz p1, :cond_15
const/4 v0, 0x1
if-eq p1, v0, :cond_12
const/4 v0, 0x2
if-eq p1, v0, :cond_d
goto :goto_18
:cond_d
sget-object p1, Lb1/a;->a:Lb1/a;
:goto_f
iput-object p1, p0, Lcom/example/enum_test/TrafficLight;->currColor:Lb1/a;
goto :goto_18
:cond_12
sget-object p1, Lb1/a;->c:Lb1/a;
goto :goto_f
:cond_15
sget-object p1, Lb1/a;->b:Lb1/a;
goto :goto_f
:goto_18
return-void
.end method
使用常量实现相同功能编译后字节码如下 :
arduino
package com.example.enum_test;
public class TrafficLightConst {
public static final int GREEN = 0;
public static final int YELLOW = 1;
public static final int RED = 2;
private int currColor = RED;
public void change(int color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
}
}
}
// 18行字节码指令
.method public change(I)V
.registers 4
const/4 v0, 0x1
if-eqz p1, :cond_10
const/4 v1, 0x2
if-eq p1, v0, :cond_d
if-eq p1, v1, :cond_9
goto :goto_12
:cond_9
const/4 p1, 0x0
iput p1, p0, Lcom/example/enum_test/TrafficLightConst;->currColor:I
goto :goto_12
:cond_d
iput v1, p0, Lcom/example/enum_test/TrafficLightConst;->currColor:I
goto :goto_12
:cond_10
iput v0, p0, Lcom/example/enum_test/TrafficLightConst;->currColor:I
:goto_12
return-void
.end method