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

  1. 定义一个简答枚举类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;
    }
}
  1. 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")
    }
}
  1. 将编译后的apk反编译结果如下(枚举类被优化):

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

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

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

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

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

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

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

在对以下代码调用的时候,使用一个变量保存枚举值,由于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;时 ,枚举未被优化

  1. 使用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;
    }

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

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

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

可以看到在该场景下枚举类没有被优化。

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

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

  1. 对枚举强转。 比如下代码不会出现枚举优化
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;
}
  1. 定义可以外部访问的方法。 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反编译结果如下(枚举被优化):

复杂多变的枚举优化

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

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

  1. 相同的代码如果定义在 TrafficLight类中, 并在MainActivityonCreate方法中运行 ,如下:
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 就是m0f1749b属性, 该属性是一个Object类型,这也是可能是导致枚举未完全优化为整数的原因, 从 m0 的代码中可以看到编译器将多个实例的构造统一只使用了一个Object作为引用, 方法也被编译到m0类内部,可以看到m0类不是一个TrafficLight,猜测这也是编译器在对枚举进行整型优化和枚举持有类优化一种权衡和选择吧 ,说明类优化要比枚举优化收益更高。

枚举 、常量

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

总结

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

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

我的理解是如果我们可以通过定义普通常量的方式代替枚举,则优先通过使用定义常量解决。权衡易用性和性能以及使用场景,可以考虑继续使用枚举,因为枚举在有些时候确实让代码更简洁,更容易维护,牺牲点内存也无妨。况且Android官方自己也在许多地方应用了枚举,例如 Lifecycle.StateLifecycle.Event 等 。

小彩蛋

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

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

  1. 如果对持有枚举变量的类或者变量进行混淆配置后 ,编译器会对枚举进行优化 ,TrafficLight 内枚举的引用被替换为整数,从反编译结果可以看到优化后的代码就是普通的if语句,并不会出现所谓的占用大量体积的情况。
  1. 如果枚举相关类未进行完全优化,但是例子中的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
参考

zhuanlan.zhihu.com/p/91459700

jakewharton.com/r8-optimiza...

相关推荐
苹果醋333 分钟前
Vue3响应式数据: 深入分析Ref与Reactive
java·运维·spring boot·mysql·nginx
缘友一世1 小时前
JAVA代理模式和适配器模式
java·代理模式·适配器模式
轻浮j1 小时前
Sentinel底层原理以及使用算法
java·算法·sentinel
it噩梦1 小时前
springboot 工程使用proguard混淆
java·spring boot·后端
潜意识起点1 小时前
Java数组:静态初始化与动态初始化详解
java·开发语言·python
竹影卿心1 小时前
Java连接HANA数据库
java·数据库·windows
Abelard_1 小时前
LeetCode--347.前k个高频元素(使用优先队列解决)
java·算法·leetcode
海海不掉头发1 小时前
软件工程-【软件项目管理】--期末复习题汇总
java·学习·产品运营·软件工程·团队开发·需求分析·期末复习
缘友一世1 小时前
java实现网络IO高并发编程java AIO
java·网络·python
洞见不一样的自己1 小时前
android 常用方法
android