enum、static final 与 IntDef:Android 中实现枚举的方案选择

前述

曾经有一段时间,许多网上的 Android 性能调优的文章都提到,要尽量避免在 Android 中使用 enum,因为使用 enum 会引入较大的性能损失。

然而,最新的 Android 文档已经改变了这一说法。根据 Android VM 的开发者Elliot Hugues 的博客所述,过去的 Android 官网的性能优化指南并不准确,混杂了许多臆断。因此如今他们严格地依据事实,重写了Android 性能优化指南,开发者也应当以最新的文档为准。当然比较窘迫的是,Android 文档的更新并不是同时改口,事实上就在同个 training 目录下的 管理应用内存一文中,就仍然保留了过时的避免使用 enum 的说法。

最新的解释

之所以重新鼓励使用 enum ,其解释是:

  1. Android 2.2 及以下系统上,使用 enum 的确会引发较大的性能损耗。主要是内存上的消耗,enum 远大于使用 static final int。
  2. 在 Android 2.3 及以后的系统中,之前的一些 enum 的性能问题已被 JIT 所优化。此时,虽然 enum 相比于 static final int,内存仍然有所增加,但已经是可以接受的了。加之 Android 2.2 到如今的 Android 7.0,Android 手机的内存配置突飞猛进,从256MB跃升至6GB,enum 所带来的内存增加已经可以忽略。

强内存依赖的应用的枚举实现

尽管如此,在实际开发中仍然有可能遇到内存消耗较大的应用开发问题,那么此时,该如何优化枚举值的实现,以节约内存消耗呢?方案如下:

直接使用 static final int

然而,其问题在于,直接使用无法实现枚举变量赋值的类型安全。且无法把多个枚举值归纳到同一个枚举类型下。比如:

显然,此时 int 就未能实现赋值的类型检查,也未能把多个枚举值归纳到同一个枚举类型下。

Android Proguard 优化

在 Android Proguard 中,可以在 proguard.cfg 中加入参数 -Doptimization class/unboxing/enum,从而自动将 enum 替换为 static final int。这样,也就无需担心多余的内存问题了。

使用 IntDef 注解替代 int

IntDef 可以用于替代 int,其价值在于用@IntDef int var限定赋值范围,实现类型安全。还用@IntDef({SUNDAY, MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY})归集了散乱的 static final int 变量,如下代码所示:

然而,IntDef 的缺点在于无法优雅地把 int 转为 IntDef,尤其在一个枚举值是服务端下发的时候。强行的实现会变的极为尴尬:

此时,在枚举值较少的时候还能忍,较多的时候代码就会变得非常丑陋。本质是因为,@IntDef 缺少像 Enum.values() Enum.ordinal() 等等 int 与 enum 与 String 互转的方法,因此在想换转换较多的场景下,不如采取第二种优化方法。

非全屏 Weex 页面开发中的 Android 适配

weex代码中的高度和宽度的单位均为px,然而,在手机屏幕上显示的高宽却不一定与代码指定的相同。原因是weex框架在底层做了针对不同屏幕的适配工作,具体计算公式为 实际高宽 = 代码高宽 * (屏幕宽度 / 750)。

举个例子,假设代码中是这么写的:

那么,在一款屏幕分辨率为1920*1280的Android手机上,此时的计算过程为:
height: 100 * (1080 / 750) = 144;
width: 200 * (1080 / 750) = 288。

如果我们开发的weex页面是全屏幕的,那么这个高宽的转换过程对我们而言是透明的,无需做额外的工作。然而一旦有一个业务场景,weex容器并非是全屏幕的,而是需要从外部传入weex容器的高度,那么,就不得不考虑这个转换的过程。

举一个我在开发weex弹窗时的例子。该weex弹窗的样式如下:

weex-blog

可以看到,如果不考虑多屏幕适配,顶栏和底栏都是一个固定值,那么只需要用总容器高度 – 两个定高组件就可以了。那么需要解决的第一个问题,就是如何获取外部容器的高度。由于weex可以通过$getConfig().env.deviceHeight$getConfig().env.deviceWidth的形式来获取手机屏幕的高度,因而,很自然地就想到,是否能在安卓中以屏幕的3/5的比例,约定容器高度,然后在weex代码中,同样通过3/5来计算容器高度。这样就避免了去写 Native Module 和 Method。

然而,这样的思路是不可行的。因为Android Native的总高度,事实上是可供显示的全屏高度,而不一定是物理屏幕的高度,因为有状态栏,虚拟按键栏,Smartbar等等安卓碎片化引入的额外显示元素,实际全屏高度很有可能小于物理屏幕高度。所以,仍然需要开发和注册Native Module,以获取外部容器高度。

再来看上文的计算公式:总容器高度 – 两个定高 = scroller高度。因为多屏幕适配的原因,上面的公式是不可行的,需要改为:

外部传入的总容器高度 – 两个定高组件的高度字面量 * 转换比例 = scroller实际高度

也就是说:外部传入的总容器高度 / 转换比例 – 两个定高组件的高度字面量 = scroller实际高度 / 转换比例 = scroller的字面量高度。

所以,最终的业务代码如下所示:

这个坑非常的隐蔽,本质是因为:weex 默默做了A参考系转换到B参考系的过程,然而一旦我们自力更生,试图从B参考系获得一个测量得到的高度,用在A参考系,而没意识到这个隐蔽的转换过程的时候,就会陷入到一台机子上调好了,另一台又跪了的尴尬局面。而且,这种情况在Android上远较iOS要来的严重。因为iOS上,除了4S以外,5,5s,6,6p,6s,6sp,屏幕尺寸均为同一长宽比。因此,在一台上调整好后,可无缝等比例放大到其他机型上。然而在Android上,毋论碎片化的屏幕尺寸,光status bar,navigation bar,smartbar等等虚拟的占用实际显示区域的各类bar,就足够让weex的默认适配喝一壶的。因此,weex这种隐蔽适配的处理方式,在Android生态上是否真的合理方便,尚待商榷。