在Android中使用反射到底有多慢?

(到目前为止,我们分析了大量的APP并发现了一些严重拖慢很多APP速度的问题。从这篇文章开始,我们将逐个对这些问题进行详细的分析。)

反射(Reflection)在Java和安卓开发过程中非常有用,但是反射的使用往往是APP严重性能问题的根本原因。下面我们通过分析几个真实的案例来帮助我们更直观的理解这个问题。

两个真实的案例

第一个案例是纽约时报安卓客户端。在NimbleDroid的帮助下,纽约时报的开发者发现Gson中的type adapter使用了反射,增加了APP700毫秒的启动时间,他们通过自行实现自定义的type adapter解决了这一问题。

第二个案例是大型图片分享平台Photobucket,反射的使用也是它们的一个巨大性能瓶颈:

com.photobucket.android Icicle Graph
构造 com.photobucket.api.client.jersey.UserClient constructor 花费了660毫秒

我们可以看到构造 com.photobucket.api.client.jersey.UserClient 花费了660毫秒。进一步分析冰柱图,我们可以看到延迟的来源是对反射的使用:

com.photobucket.android Iricle Graph
存在大量反射的调用,例如:java.lang.Class.getGenericInterfaces

getGenericInterfaces() 函数会返回一个类型直接实现的接口类型,这里对它进行了5次调用,每次耗时大约81毫秒。当然,如果只看单次调用花费的时间可能影响不大,但是所有调用的总时间就很长了,仅仅是对这个方法的调用就造成了大约600毫秒的启动延迟。现在让我们更深入的分析一下为什么这个方法的调用会耗费如此长的时间。

我们发现,这个库支持开发者通过注解(annotation)来定义REST API,但问题是这个库并没有在编译期间去处理这些注解,而是在运行时进行通过反射进行处理,从性能的角度来看,这一点是灾难性的。

微基准测试(Micro-benchmarks)

我们创建了一个简单地测试代码来测试反射究竟有多慢。

我们在 android.app.Activity 类中反复进行了10,000次操作,代码如下:

Class<?> clazz = android.app.Activity.class;
for (int i = 0; i < 10000; i++) {
	clazz.getFields();
}

我们还创建了两个测试用例,通过创建一个空的类型的实例,来测试反射造成的开销,代码如下:

try {
    for (int i = 0; i < 1_000_000; i++) {
        DummyItem.class.newInstance();
    }
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

下面是我们的测试结果(时间单位均为毫秒,用的是有真正使用的手机,所以结果更接近真实用户体验):

NEXUS 5 (6.0) ART GALAXY S5 (5.0) ART GALAXY S3 mini (4.1.2) Dalvik
getFields 1108 1626 27083
getDeclaredFields 347 951 7687
getGenericInterfaces 16 23 2927
getGenericSuperclass 247 298 665
makeAccessible 14 147 449
getObject 21 167 127
setObject 21 201 161
createDummyItems 312 358 774
createDummyItemsWithReflection 1332 6384 2891

显然,在Android中使用反射非常慢,使用反射的测试执行分别需要1332毫秒、6384毫秒、2891毫秒,而不使用反射则只需要312毫秒、358毫秒、774毫秒。一个有趣的现象是,安卓5.0 ART环境下,高端设备使用反射耗费的时间,比安卓4.1 Dalvik环境下的低端设备更长,只有安卓6.0 ART环境才提升了反射的性能,但是反射仍然非常慢。

更多的真实案例

ActiveAndroid是另一个使用了反射技术的库,让我们通过测试一些Google Play商店的APP来分析反射对APP启动速度的影响。

下面是Scribd的测试结果:

Scribd Iricle Graph
调用 com.activeandroid.ActiveAndroid.initialize 耗费1093毫秒

Myntra也存在同样的问题:

Myntra Iricle Graph
调用 com.activeandroid.ActiveAndroid.initialize 耗费1421毫秒

我们可以看到,ActiveAndroid的初始化耗时超过1秒。1秒其实已经很长了,尤其是考虑到用户对APP启动时间的期望是2秒以内

总结一下,在Android中使用反射非常之慢。为了向用户提供最流畅的用户体验,我们强烈建议:

尽可能避免反射的使用(以及使用了反射的第三方库),尤其是使用类型反射来对Java对象进行序列化操作。