Java1.5中引入了枚举的语法,包括Enum,EnumSet,EnumMap等。其中Enum就是我们在C或C++中见过的枚举类型,但是Java中的枚举又比C或C++中的枚举更成熟和复杂。在Java中,枚举算是一种特殊的类,也就是class,因此它可以做很多类相关的事情,而不仅仅是定义几个枚举值。
我们在很多经典的Java书已经看到推荐使用枚举来代替int常量了,但是在Android开发中我不建议使用枚举,特别是大型的App中,能不用则不用。我们先了解下枚举的原理和优势。
在Java1.5以前,我们要定义常量的话一般都是使用类常量或者接口常量,接口常量可能用的更少一些。比如我们要定义4个颜色的常量。用类常量和枚举分别定义的话,就如下的实现方式:
public class TestEnum {
public static final int COLOR_RED = 1;
public static final int COLOR_GREEN = 2;
public static final int COLOR_YELLOW = 3;
public static final int COLOR_Blue = 4;
//----------------------------------------------------------
public enum ColorEnum {
RED, GREEN, YELLOW, BLUE;
}
}
上面的代码中,我们分别定义了类常量和枚举常量。从定义上看,枚举常量显然更简单,只要定义各个枚举项,而不需要定义值等。
枚举常量属于稳定态型,编译阶段就能保证类型的安全,使用的时候也可以不用再次校验。例如,对于以下函数的代码:
public static void useColor(ColorEnum e) {
switch (e) {
case RED:
Log.e("TestEnum", "RED");
break;
case GREEN:
Log.e("TestEnum", "GREEN");
break;
case YELLOW:
Log.e("TestEnum", "YELLOW");
break;
case BLUE:
Log.e("TestEnum", "BLUE");
break;
default:
Log.e("TestEnum", "Error Color");
break;
}
}
如上面的函数,如果我们需要调用该函数的话,已经限定了参数只能是ColorEnum的枚举类型,不能是其他类型。在代码的逻辑判断上,也不需要做额外的越界等其他判断,使用起来会更安全。不过这里有个点需要注意,例如我们调用以下代码,结果会输出什么呢?
TestEnum.useColor(null);
输出的结果并不是Error Color,而是java.lang.NullPointerException,对于枚举类型,我们是需要进行NULL值判断的,否则容易引起空指针异常。至于为什么没有走到defalut的代码,后面会分析。
前面提到了,枚举在Java中其实也是类,除了少许限制,和普通的类差别不大,所以它有内置的方法,也可以定义一些属于自己的方法。每个枚举都是java.lang.Enum的子类。Enum是一个abstract的抽象类,而且实现了序列化等接口,我们可以看部分的代码:
public abstract class Enum<E extends Enum<E>> implements Serializable, Comparable<E> {
private static final long serialVersionUID = -4300926546619394005L;
private final String name;
private final int ordinal;
private static final BasicLruCacheextends Enum>, Object[]> sharedConstantsCache
= new BasicLruCacheextends Enum>, Object[]>(64) {
@Override protected Object[] create(Class extends Enum> enumType) {
Method method = (Method) Class.getDeclaredConstructorOrMethod(
enumType, "values", EmptyArray.CLASS);
try {
return (Object[]) method.invoke((Object[]) null);
} catch (IllegalAccessException impossible) {
throw new AssertionError();
} catch (InvocationTargetException impossible) {
throw new AssertionError();
}
}
};
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}
@Override
public String toString() {
return name;
}
public final int compareTo(E o) {
return ordinal - o.ordinal;
}
public static extends Enum> T valueOf(Class enumType, String name) {
if (enumType == null || name == null) {
throw new NullPointerException("enumType == null || name == null");
}
if (!enumType.isEnum()) {
throw new IllegalArgumentException(enumType + " is not an enum type");
}
for (T value : getSharedConstants(enumType)) {
if (name.equals(value.name())) {
return value;
}
}
throw new IllegalArgumentException(name + " is not a constant in " + enumType.getName());
}
}
以上是枚举抽象类的部分代码,里面我们可以看到一些成员函数和常用方法。因为枚举已经继承了Enum,所以我们不能再继承其他的类,但是还能够实现其他的接口,并且我们也可以添加自己的构造函数等。例如:
public enum ColorEnum {
RED("red color"), GREEN("green color"), YELLOW("yellow color"), BLUE("blue color");
private final String mDescription;
private ColorEnum(String s) {
mDescription = s;
}
public String getDescription() {
return mDescription;
}
}
上面的代码中,自己添加了成员变量、方法和构造函数。这样看起来就和一般的类很相似了。除了上面看到的一些方法,以及Object类的一些方法外,我们还能看到其他的方法,在写代码的时候,自动提示中就可以看到,如下图所示:
这里还能看到values()和valueOf(String arg0)方法,而在抽象类中并不存在这两个方法,这两个方法都是由编译器自动添加的static方法。在使用valueOf()的时候需要注意校验,例如我们使用valueOf("Red")的时候,就会遇到崩溃,提示异常。
01-07 15:36:08.191: E/AndroidRuntime(15599): Caused by: java.lang.IllegalArgumentException: Red is not a constant in com.example.viewtest.TestEnum$ColorEnum
01-07 15:36:08.191: E/AndroidRuntime(15599): at java.lang.Enum.valueOf(Enum.java:200)
EnumSet的作用主要用以代替普通的int或者long类型的“位标志”,这些“位标志”一般用来表示“开/关”信息,我们自己去做位运算的话会比较麻烦,其实EnumSet已经为我们实现好了这个逻辑。其内部也是使用位运算,效率非常的高。我们看下EnumSet的实现代码,如下:
public static extends Enum> EnumSet noneOf(Class elementType) {
if (!elementType.isEnum()) {
throw new ClassCastException(elementType.getClass().getName() + " is not an Enum");
}
E[] enums = Enum.getSharedConstants(elementType);
if (enums.length <= 64) {
return new MiniEnumSet(elementType, enums);
}
return new HugeEnumSet(elementType, enums);
}
通过源码我们可以看到,当枚举个数不超过64个的时候采用MiniEnumSet来实现,而当个数超过64个的时候采用HugeEnumSet来实现。MiniEnumSet中采用一个long类型的每一位来映射对应到枚举的序号,而HugeEnumSet则采用的是long[] bits的long数组来处理,原理上类似。当个数小于64个的时候,性能会更好。
前面谈到了很多使用枚举的优势,确实在代码上使用枚举会带来很多好处。但是在移动端的开发中有一些限制,导致使用枚举会带来一些不利因素。下面来就来谈下几个不利点。
前面说到枚举也是一个类,那么对于枚举的定义,看似简单,我们可以看下在Android工程的bin/classes/包名目录下,查看相关的编译时候的class类的文件,我们可以发现和内部类一样,枚举也会生成一个类文件。例如:
public enum ColorEnum {
RED, GREEN, YELLOW, BLUE;
}
对于上述的枚举定义,我们在该目录下可以发现一个1246字节大小的TestEnum$ColorEnum.class文件。如下图所示:
下面我们看下没有定义枚举时候的dex文件大小,在该测试工程中,classes.dex的大小为764468字节,如果加上以上枚举的定义,那么大小增加到:765320字节,增加了852字节(这里没有使用混淆)。
下面我们将颜色定义成20个枚举,如下所示:
public enum ColorEnum {
RED, GREEN, YELLOW, BLUE, ORANGE, PURPLE, GRAY, BROWN, TAN, SYAN, BLACK, WHITE, PINK, PLUM, POWDERBLUE, SAPPHIRE, SILVER, ROSYBROWN, ROYALBLUE, RUBINE;
}
上面的枚举定义,使得classes.dex文件的大小变为766396,比4个枚举类型的时候增加了1920字节。可见枚举个数的增加会导致枚举类文件增大,最后导致dex文件的增大,因为每个枚举项最后都会生成一个实例,数量越多,编译器自动生成实例的代码也会越多,相应的字节码也就会越多。我们接着定义多个枚举,只是名称不同,内容一致。
public enum ColorEnum {
RED, GREEN, YELLOW, BLUE, ORANGE, PURPLE, GRAY, BROWN, TAN, SYAN, BLACK, WHITE, PINK, PLUM, POWDERBLUE, SAPPHIRE, SILVER, ROSYBROWN, ROYALBLUE, RUBINE;
}
public enum ColorEnum1 {
RED, GREEN, YELLOW, BLUE, ORANGE, PURPLE, GRAY, BROWN, TAN, SYAN, BLACK, WHITE, PINK, PLUM, POWDERBLUE, SAPPHIRE, SILVER, ROSYBROWN, ROYALBLUE, RUBINE;
}
public enum ColorEnum2 {
RED, GREEN, YELLOW, BLUE, ORANGE, PURPLE, GRAY, BROWN, TAN, SYAN, BLACK, WHITE, PINK, PLUM, POWDERBLUE, SAPPHIRE, SILVER, ROSYBROWN, ROYALBLUE, RUBINE;
}
public enum ColorEnum3 {
RED, GREEN, YELLOW, BLUE, ORANGE, PURPLE, GRAY, BROWN, TAN, SYAN, BLACK, WHITE, PINK, PLUM, POWDERBLUE, SAPPHIRE, SILVER, ROSYBROWN, ROYALBLUE, RUBINE;
}
public enum ColorEnum4 {
RED, GREEN, YELLOW, BLUE, ORANGE, PURPLE, GRAY, BROWN, TAN, SYAN, BLACK, WHITE, PINK, PLUM, POWDERBLUE, SAPPHIRE, SILVER, ROSYBROWN, ROYALBLUE, RUBINE;
}
如上代码,我们会得到5个枚举定义的class文件,classes.dex文件的大小变为772952,比单个枚举定义的时候增加了6556字节,通过上面我们可以发现枚举对安装包大小带来的影响,虽然混淆可能会去掉部分方法,但仍然不可避免的增加dex的大小。
通过前面的介绍后,我们已经很容易想到这个问题。枚举作为一个类,除了本身的一些方法外,编译器还会额外增加几个方法。如前面介绍的values()、valueOf(),另外还有构造函数等等以及在使用switch的时候,额外增加的函数。这样枚举的数量越多,必然导致方法数量的增加。对于大型的App,dex文件为了规避早期版本的65535方法数限制,一直在努力的减少方法数量,手淘也会经常因为方法数的超限而导致无法打包。而代码中枚举这样的隐晦的类及其包括的方法,如果能够减少也是减少方法数量的一个新途径,可谓踏遍铁鞋无觅处,得来全不费工夫。
我们知道,int等类型的常量,在编译的时候,编译器会做优化,在生成class字节码的过程中就已经直接替换掉了,这样可以提升性能。而枚举不同,虽然本质上和int值类似,但是它会为每个枚举项导出static和final域的实例。前面关于单例的文章中已经提到,枚举其实也是一个单例。这样一旦引用该枚举的时候,就会触发虚拟机加载该枚举类,并且实例化所有的枚举项,并且这些枚举实例的内存无法回收,而且枚举是单例,如果自定义的枚举类中包含了大块内存的引用,也可能会带来内存泄露。
这里需要注意的是,枚举和内部类相似,在加载枚举的时候,不会引起外围类被加载,只会引起该枚举本身的加载和实例化。加载外部类的时候,也不会引起内部类的自动加载,也不会引起类中枚举的自动加载,只有在使用到该枚举的时候才会被加载。
通过Mat工具,我们可以查看ColorEnum被实例化后的信息,如下图所示,包括一个枚举数组的实例和20个枚举项的实例:
再进一步查看各个实例的内存情况,我们可以看到,每个实例包含了一个枚举的定义:
所有枚举实例都是static的,所以这部分实例的内存一旦被加载后,就会一直存在,会占用内存,同时也会产生内存碎片,导致更大的问题,因为Android5.0以前的虚拟机没有碎片压缩的机制。
另外在switch语句中使用枚举,还会额外产生一个静态的数组,这个内存也会长期占有,在下面的分析中会看到该数组。
枚举的每一项,编译器都会自动生成对应的字符串常量,以便后续的函数中方便获得枚举的名字。例如对于上面的枚举,我们就通过字节码查看,可以看到“RED”,“GREEN”等字符串常量:
0 new com.example.viewtest.TestUtil$ColorEnum1 [1]
3 dup
4 ldc "RED"> [31]
6 iconst_0
7 invokespecial com.example.viewtest.TestUtil$ColorEnum1(java.lang.String, int) [32]
10 putstatic com.example.viewtest.TestUtil$ColorEnum1.RED : com.example.viewtest.TestUtil.ColorEnum1 [36]
13 new com.example.viewtest.TestUtil$ColorEnum1 [1]
16 dup
17 ldc "GREEN"> [38]
19 iconst_1
20 invokespecial com.example.viewtest.TestUtil$ColorEnum1(java.lang.String, int) [32]
23 putstatic com.example.viewtest.TestUtil$ColorEnum1.GREEN : com.example.viewtest.TestUtil.ColorEnum1 [39]
26 new com.example.viewtest.TestUtil$ColorEnum1 [1]
29 dup
30 ldc "YELLOW"> [41]
32 iconst_2
33 invokespecial com.example.viewtest.TestUtil$ColorEnum1(java.lang.String, int) [32]
36 putstatic com.example.viewtest.TestUtil$ColorEnum1.YELLOW : com.example.viewtest.TestUtil.ColorEnum1 [42]
39 new com.example.viewtest.TestUtil$ColorEnum1 [1]
42 dup
枚举的使用会产生函数调用时间的开销,在高频率情况下就会产生一定的性能问题。我们可以通过代码测试和TraceView来跟踪这个函数的调用。测试代码如下,其中useColor的代码如文章开头处所示:
for (int i = 0; i < 100; i++) {
TestEnum.useColor(ColorEnum.BLUE);
}
在这个函数中我们看到了编译器自动生成的一些方法调用。Enum.ordinal()执行了120次,其中useColor函数调用了100次,该调用发生在switch(e)的判断中,所以前面提到的TestEnum.useColor(null)会发生空指针异常就可以理解了,因为编译器会自动调用e.ordinal()方法。另外还能看到TestEnum.$SWITCH_TABLE$com$example$viewtest$TestEnum$ColorEnum()
函数执行了100次,该函数由编译器自动生成,该函数的返回类型为[I,也就是说是一个int类型的数组。进一步看该函数做了什么:
从上图我们可以看到该函数调用了20次的ordinal()方法,和values()方法,通过这个调用栈,这里可以基本得出,在第一次调用的时候会把所有枚举都遍历一遍,并把取得的序号存放到数组中,通过该函数返回这个数组。下面继续执行一次,再做TraceView,我们发现该函数的执行中,已经没有了Childred,即使将Activity退出后重新执行,如下图所示:
从这里我们可以基本确定,该函数返回的数组应该是一个static类型的数组,只是在第一次调用的时候创建,后面不会再创建了,这也就是上面分析内存的时候讲到的一个静态数组。这个过程也是通过TraceView推断代码逻辑的一个思路。当然如果有代码,通过反编译代码更能看明白其中的原理,我们能看到这个数组的定义,其定义如下:
private static int TestEnum.$SWITCH_TABLE$com$example$viewtest$TestEnum$ColorEnum[];
而TestEnum.$ SWITCH_TABLE$com$ example$ viewtest$ TestEnum$ ColorEnum()
函数在首次执行的时候会创建该数组,一旦创建了,后续就直接返回该数组。
通过上面的分析我们可以看到,对于枚举的使用,编译器会自动调用ordinal()等方法,这些方法的调用会比int常量带来更多的性能损耗和内存消耗。另外如果代码中通过valuesOf来获取枚举值的话,高频率下性能损耗会更明显,因为这个涉及到更多的函数操作和字符串比较,性能损耗会比较大,我们可以看下跟踪到的函数执行情况。
我们可以再看顶部Enum的源码,在文件开头就定义了sharedConstantsCache。
实际的项目中,我们也会遇到因为枚举导致的性能问题,例如在网络层的日志打印等高频率函数中,如果使用枚举就会给性能带来一定的影响,如果使用int常量,就不会有这部分的开销,高频率下对性能的影响还是显而易见的。
上图是网络库中使用枚举判断日志输出带来的性能开销,在高频率下,这部分性能开销能够避免的话也是对性能的一个提升。
通过上面的分析我们了解了枚举的优势和相关的原理,但是在Android开发上,由于设备内存和安装程序大小的限制,不管是从文件大小、内存使用量还是性能等角度考虑,使用枚举都不是一个好的选择,尤其是大型的App。对于主Dex文件的模块代码,以及高频率函数中,我们应该尽可能的不使用枚举。在其他int常量能够满足需求的情况下,也最好选用int常量的方式来实现相关的功能,避免对文件大小,方法数量,内存,性能产生不利的影响。