android开发之避免使用枚举

Java1.5中引入了枚举的语法,包括Enum,EnumSet,EnumMap等。其中Enum就是我们在C或C++中见过的枚举类型,但是Java中的枚举又比C或C++中的枚举更成熟和复杂。在Java中,枚举算是一种特殊的类,也就是class,因此它可以做很多类相关的事情,而不仅仅是定义几个枚举值。
    我们在很多经典的Java书已经看到推荐使用枚举来代替int常量了,但是在Android开发中我不建议使用枚举,特别是大型的App中,能不用则不用。我们先了解下枚举的原理和优势。

一、枚举常量的优势

1)枚举常量会更简单

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

     上面的代码中,我们分别定义了类常量和枚举常量。从定义上看,枚举常量显然更简单,只要定义各个枚举项,而不需要定义值等。

2)枚举常量会更安全

    枚举常量属于稳定态型,编译阶段就能保证类型的安全,使用的时候也可以不用再次校验。例如,对于以下函数的代码:

	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的代码,后面会分析。

3)枚举可以有内置方法和自己的方法

    前面提到了,枚举在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(Classextends 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类的一些方法外,我们还能看到其他的方法,在写代码的时候,自动提示中就可以看到,如下图所示:

android开发之避免使用枚举_第1张图片

    这里还能看到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)

4)可以用EnumSet代替位标志

    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个的时候,性能会更好。

二、使用枚举带来的问题

    前面谈到了很多使用枚举的优势,确实在代码上使用枚举会带来很多好处。但是在移动端的开发中有一些限制,导致使用枚举会带来一些不利因素。下面来就来谈下几个不利点。

1)枚举会增加dex文件大小

    前面说到枚举也是一个类,那么对于枚举的定义,看似简单,我们可以看下在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的大小。

2)枚举会增加dex文件方法数量

    通过前面的介绍后,我们已经很容易想到这个问题。枚举作为一个类,除了本身的一些方法外,编译器还会额外增加几个方法。如前面介绍的values()、valueOf(),另外还有构造函数等等以及在使用switch的时候,额外增加的函数。这样枚举的数量越多,必然导致方法数量的增加。对于大型的App,dex文件为了规避早期版本的65535方法数限制,一直在努力的减少方法数量,手淘也会经常因为方法数的超限而导致无法打包。而代码中枚举这样的隐晦的类及其包括的方法,如果能够减少也是减少方法数量的一个新途径,可谓踏遍铁鞋无觅处,得来全不费工夫。

3)枚举会增加内存的使用

    我们知道,int等类型的常量,在编译的时候,编译器会做优化,在生成class字节码的过程中就已经直接替换掉了,这样可以提升性能。而枚举不同,虽然本质上和int值类似,但是它会为每个枚举项导出static和final域的实例。前面关于单例的文章中已经提到,枚举其实也是一个单例。这样一旦引用该枚举的时候,就会触发虚拟机加载该枚举类,并且实例化所有的枚举项,并且这些枚举实例的内存无法回收,而且枚举是单例,如果自定义的枚举类中包含了大块内存的引用,也可能会带来内存泄露
    这里需要注意的是,枚举和内部类相似,在加载枚举的时候,不会引起外围类被加载,只会引起该枚举本身的加载和实例化。加载外部类的时候,也不会引起内部类的自动加载,也不会引起类中枚举的自动加载,只有在使用到该枚举的时候才会被加载。
    通过Mat工具,我们可以查看ColorEnum被实例化后的信息,如下图所示,包括一个枚举数组的实例和20个枚举项的实例:

android开发之避免使用枚举_第2张图片
    
    再进一步查看各个实例的内存情况,我们可以看到,每个实例包含了一个枚举的定义:

android开发之避免使用枚举_第3张图片

    所有枚举实例都是static的,所以这部分实例的内存一旦被加载后,就会一直存在,会占用内存,同时也会产生内存碎片,导致更大的问题,因为Android5.0以前的虚拟机没有碎片压缩的机制。
    另外在switch语句中使用枚举,还会额外产生一个静态的数组,这个内存也会长期占有,在下面的分析中会看到该数组。

4)枚举会增加字符串常量

    枚举的每一项,编译器都会自动生成对应的字符串常量,以便后续的函数中方便获得枚举的名字。例如对于上面的枚举,我们就通过字节码查看,可以看到“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

5)枚举会增加函数调用时间

    枚举的使用会产生函数调用时间的开销,在高频率情况下就会产生一定的性能问题。我们可以通过代码测试和TraceView来跟踪这个函数的调用。测试代码如下,其中useColor的代码如文章开头处所示:

	for (int i = 0; i < 100; i++) {
		TestEnum.useColor(ColorEnum.BLUE);
	}

     上面代码的运行后的TraceView结果如下:

    在这个函数中我们看到了编译器自动生成的一些方法调用。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来获取枚举值的话,高频率下性能损耗会更明显,因为这个涉及到更多的函数操作和字符串比较,性能损耗会比较大,我们可以看下跟踪到的函数执行情况。

android开发之避免使用枚举_第4张图片



    我们可以再看顶部Enum的源码,在文件开头就定义了sharedConstantsCache。

    实际的项目中,我们也会遇到因为枚举导致的性能问题,例如在网络层的日志打印等高频率函数中,如果使用枚举就会给性能带来一定的影响,如果使用int常量,就不会有这部分的开销,高频率下对性能的影响还是显而易见的。

android开发之避免使用枚举_第5张图片

    上图是网络库中使用枚举判断日志输出带来的性能开销,在高频率下,这部分性能开销能够避免的话也是对性能的一个提升。

    通过上面的分析我们了解了枚举的优势和相关的原理,但是在Android开发上,由于设备内存和安装程序大小的限制,不管是从文件大小、内存使用量还是性能等角度考虑,使用枚举都不是一个好的选择,尤其是大型的App。对于主Dex文件的模块代码,以及高频率函数中,我们应该尽可能的不使用枚举。在其他int常量能够满足需求的情况下,也最好选用int常量的方式来实现相关的功能,避免对文件大小,方法数量,内存,性能产生不利的影响。

你可能感兴趣的:(性能优化)