注册

Android 开发中是否应该使用枚举?

前言

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 int0 ints anyway.
The advice was wrong for application developers then. It's remains wrong now.

最重要的一句是

ProGuard turns them back int0 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对枚举优化的支持效果。 代码如下:

  1. 定义一个简答枚举类Language
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;
}
}
  1. MainActivity主要代码
@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文件内开启混淆

buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
  1. 将编译后的apk反编译结果如下(枚举类被优化):

截屏2023-11-07 20.55.14.png

以上结果可以看出,如果是一个简单的枚举类,那么枚举类将会被优化为一个整形数字。既然ProGuard/R8会把枚举优化为整形,那是不是在Android中,就可以继续无所顾忌的使用枚举了呢? 我没有找到官方对R8枚举具体的优化场景说明 ,只找了ProGuard对枚举的优化有一定的限制条件,如果枚举类存在如下的情况,将不会有优化为整形,如下所示:

  1. 枚举实现了自定义接口。并且被调用。
  2. 代码中使用了不同签名来存储枚举。
  3. 使用instanceof指令判断。
  4. 使用枚举加锁操作。
  5. 对枚举强转。
  6. 在代码中调用静态方法valueOf方法
  7. 定义可以外部访问的方法。

参考自:ProGuard 初探 · dim's blog,另外,上面的这七种情况,我并没有找到官方的说明,如果有哪位读者知道,请在评论区里留下链接,谢谢啦~

下面我们对以上的情况进行追一验证,看下这些条件是否也会对R8编译优化产生限制 , 如下 :

  1. 枚举实现了自定义接口,并且被调用。
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());
}

反编译结果如下(枚举类被优化):

截屏2023-11-07 18.37.01.png

  1. 代码中使用了不同签名来存储枚举。

在对以下代码调用的时候,使用一个变量保存枚举值,由于currColor变量声明类型的不同, 导致枚举的优化结果也不同

// 枚举会被优化
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; 时 ,枚举被优化整数

截屏2023-11-10 11.41.41.png

Object currColor = Signal.RED;时 ,枚举未被优化

截屏2023-11-10 11.34.39.png

  1. 使用instanceof指令判断。 (发生了类型转换)
@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;
}

反编译结果如下(枚举类未被优化):

截屏2023-11-10 12.13.48.png

  1. 使用枚举加锁操作。
synchronized (Language.Chinese) {
System.out.println("synchronized");
}

从反编译结果如下(枚举类未被优化):

截屏2023-11-07 18.23.22.png 可以看到在该场景下枚举类没有被优化。

  1. 不要作为一个输出或打印对象
System.out.println(RED);

从反编译结果如下(枚举类未被优化):

截屏2023-11-10 12.17.11.png

  1. 对枚举强转。 比如下代码不会出现枚举优化
  @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;
}

反编译结果如下:(枚举类未被优化): 截屏2023-11-07 20.07.43.png

如果把返回值修改为Language则会发生优化

@Nullable
Language getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}

反编译结果如下:(枚举类被优化):

截屏2023-11-07 20.15.21.png

以下代码也会出现枚举被优化,把方法的返回值类型修改为 Language ,接收变量类型改为 Object

 @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;
}

截屏2023-11-07 20.34.54.png

  1. 定义可以外部访问的方法。 R8对枚举的优化并不受定义外部方法的影响,如下在枚举内定义getLanguage方法后,枚举仍被优化
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反编译结果如下(枚举被优化):

截屏2023-11-07 20.45.17.png

复杂多变的枚举优化

在测试中发现一个问题 ,同样的代码,放在不同的文件内,优化效果竟然也不同。

  1. 在 MainActivity定义如下方法
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方法内执行, 从下面的反编译结果中可以看到枚举被优化了。

截屏2023-11-10 14.29.32.png

  1. 相同的代码如果定义在 TrafficLight类中, 并在MainActivityonCreate方法中运行 ,如下:
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();

截屏2023-11-10 14.43.23.png

从上面的对比中发现,相同的枚举代码操作放在Activity和 放在普通类中 ,编译结果是不同的。导致这种问题的原因还是因为Activity默认是配置了防混淆的,如果对一个类成员添加了防混淆配置,编译会尽可能的对类里面相关的枚举使用优化为一个常量,但这不是一定的,枚举的优化会受到其他因素影响,例如 锁对象、类型转换等其它条件限制。而TrafficLight默认是没有被配置防混淆的,如果类内定义了枚举变量,编译器会对类进行一系列的编译优化和函数内联等处理,枚举变量被抽取到一个工厂公共类里内部,枚举变量对象被指向一个Object类型引用,编译器不会对枚举进行优化。如果对类进行防混淆配置后,该类内部枚举代码会被优化为一个整数常量,结果如下:

配置 -keep class com.example.enum_test.TrafficLight效果

截屏2023-11-10 15.16.59.png

配置#-keepclassmembernames class com.example.enum_test.TrafficLight效果

截屏2023-11-10 15.31.18.png

引用代码

截屏2023-11-10 15.17.25.png

如果对具有引用枚举类型变量的类进行了防混淆配置处理TrafficLight内的枚举引用也全部被优化为了整数类型。

如果未对TrafficLight类进行防混淆配置,这个类的相关成员可能会被抽取到一个公共类里。 currColor 就是m0f1749b属性, 该属性是一个Object类型,这也是可能是导致枚举未完全优化为整数的原因, 从 m0 的代码中可以看到编译器将多个实例的构造统一只使用了一个Object作为引用, 方法也被编译到m0类内部,可以看到m0类不是一个TrafficLight,猜测这也是编译器在对枚举进行整型优化和枚举持有类优化一种权衡和选择吧 。

截屏2023-11-10 16.05.23.png

截屏2023-11-10 16.05.54.png

枚举 、常量

从编译结果来看,枚举由于会构建多个静态对象ordinal()values()等函数和变量的存在,确实会比普通的静态对象或常量更加占用空间和内存。但是从上面的测试结果中可以看到 ,枚举在最佳情况下可以被优化为整型,达到和常量一样的效果。

截屏2023-11-10 16.19.04.png

总结

以下场景都会阻止枚举优化 :

  1. 使用instanceof指令判断。
  2. 使用枚举作为锁对象操作
  3. System.out.println(enum) 输出
  4. 枚举作为返回值返回时,返回参数的声明类型与枚举不一致,请参考 例6
  5. 混淆优化配置影响枚举优化, 如果一个类中有变量是一个枚举类型,如果该类未在proguard-rules.pro配置混淆优化处理,该类则可能会被编译器优化掉,其变量和方法会被抽取到一个公共类或者内敛到引用类里, 且枚举类不会被优化,因为枚举变量公共类被一个Object类型变量引用持有。
  6. 常规的枚举使用,R8都会对枚举进行一定程度的优化,最好的情况下会优化成一个整数常量,性能几乎不会有任何影响。

我的结论是如果我们可以通过定义普通常量的方式代替枚举,则优先通过使用定义常量解决。因为枚举本身确实会带来导致包体积和内存的增长, 而枚举被优化的环境和条件实在是过于苛刻,例如可能在输出语造成打印了一下枚举System.out.println(enum),一不小心可能就会造成举优化失败。也不是不能使用枚举,权衡易用性和性能以及使用场景,可以考虑继续使用枚举,因为枚举在有些时候确实让代码更简洁,更容易维护,牺牲点内存也无妨。况且Android官方自己也在许多地方应用了枚举,例如Lifecycle.StateLifecycle.Event等 。

小彩蛋

前几天群里在讨论 京东金融Android瘦身探索与实践 文章,内容中一点优化是关于枚举的 。

截屏2023-11-07 21.14.13.png

我感觉他们以这个例子没有很强的说服力,原因如下 :

  1. 如果对持有枚举变量的类或者变量进行混淆配置后 ,编译器会对枚举进行优化 ,TrafficLight 内枚举的引用被替换为整数,从反编译结果可以看到优化后的代码就是普通的if语句,并不会出现所谓的占用大量体积的情况。

image.png

  1. 如果枚举相关类未进行完全优化,但是例子中的change()方法并不会导致大量增加包体 ,只是增加了4行字节码指令。但是枚举的定义的确会占用一定的包体积大小,这个毋庸置疑。

使用枚举实现以及编译后字节码如下 :

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

使用常量实现相同功能编译后字节码如下 :

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
参考

zhuanlan.zhihu.com/p/91459700

jakewharton.com/r8-optimiza…


作者:Lstone
来源:juejin.cn/post/7299666003364249650

0 个评论

要回复文章请先登录注册