反射与动态代理:性能开销、替代方案与内存模型
反射和动态代理,这两个词在Android圈子里经常被提起。说实话,我刚入行那会儿也觉得它们很“高级”,好像用了反射代码就显得很牛。但后来在线上遇到过几次性能问题,追查下来发现反射调用占了很大比重。嗯,从那以后我对反射就谨慎多了。
这一章,我想跟你聊聊反射到底慢在哪,有没有更好的替代方案,以及动态代理在内存里是怎么玩的。你想想看,搞懂这些,你写框架或者做性能优化时心里就有底了。
反射的性能开销到底有多大?
先看一个简单的例子。假设我们有一个普通的对象调用,和一个通过反射的调用:
// 普通调用
User user = new User();
user.getName();
// 反射调用
Class<?> clazz = Class.forName("com.example.User");
Method method = clazz.getMethod("getName");
Object result = method.invoke(user);
这两段代码,执行时间能差多少?我在项目中做过压测,普通调用大概在纳秒级,反射调用在微秒级。差了上百倍。为什么会这样?
反射的慢,主要来自三个环节:
- 类型检查:每次调用都要校验方法签名、参数类型,这些在编译期就确定的东西,反射要在运行时重新查一遍。
- 装箱拆箱:反射方法返回的是Object,基本类型要装箱,引用类型要强转。这些操作都有开销。
- JIT优化失效:JIT编译器对普通调用可以做内联优化,但反射调用是动态的,JIT很难优化。说白了,JIT看到反射调用,基本就放弃了。
核心结论:反射调用比普通调用慢 100-1000 倍。如果是在循环里频繁调用,这个差距会被放大到不可接受。
替代方案:编译时注解与代码生成
既然反射慢,那有没有办法既保留动态调用的灵活性,又避免运行时开销?当然有。我个人习惯的做法是:把运行时的工作挪到编译期去做。
编译时注解(APT)
APT(Annotation Processing Tool)可以在编译时扫描注解,生成辅助代码。比如ButterKnife、Dagger、Room这些框架,都是这么干的。
举个例子,你写一个自定义注解:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
然后在编译时,APT会生成一个类,里面直接写好了findViewById的代码。运行时调用这个生成的类,就是普通的方法调用,没有反射开销。
我的经验:在项目中,能用APT解决的问题,尽量不要用反射。APT生成的代码虽然多了几个类,但APK体积增加微乎其微,性能收益却很明显。
代码生成(Code Generation)
除了APT,还可以用代码生成框架,比如JavaPoet。你可以写一个模板,在编译时生成需要的类。这种方式在写框架时特别有用。
我记得有一次要做一个事件总线,最初用反射实现,每次发送事件都要遍历所有订阅方法,性能很差。后来改成编译时生成订阅表,性能提升了近10倍。
动态代理的内存模型
动态代理是反射的另一个典型应用。Android里常见的场景有:AOP切面编程、Retrofit的接口代理、Hook框架等。
动态代理在内存里是怎么工作的?我画了一张图,你可以看看:
从这张图可以看出,动态代理在内存里创建了一个代理对象($Proxy0),它实现了目标接口。所有方法调用都会先经过InvocationHandler的invoke方法,再由它转发给真正的目标对象。
这里有一个关键点:代理对象是运行时生成的。每次调用Proxy.newProxyInstance,都会在堆内存里生成一个新的代理类。如果频繁创建代理对象,会导致元空间(Metaspace)占用增加。
我曾经踩过的坑:在一个列表页里,每个item都创建了一个动态代理。结果列表滑动时频繁GC,卡顿明显。后来改成复用代理对象,问题就解决了。所以,动态代理适合创建一次、多次调用的场景,不适合高频创建。
反射与动态代理的适用场景
说了这么多,到底什么时候用反射,什么时候用动态代理?我整理了一个表格:
| 技术 | 适用场景 | 性能要求 | 推荐程度 |
|---|---|---|---|
| 反射 | 框架初始化、调试工具、兼容性适配 | 低(非频繁调用) | 谨慎使用 |
| 编译时注解 | 依赖注入、数据绑定、路由 | 高(编译期完成) | 强烈推荐 |
| 代码生成 | 模板代码、序列化、ORM | 高(编译期完成) | 推荐 |
| 动态代理 | AOP、接口封装、Hook | 中(单次调用) | 按需使用 |
你想想看,如果只是框架启动时做一次反射,那点开销可以忽略。但如果是在onDraw或者列表的getView里用反射,那就要小心了。
性能优化的核心思路
最后,我想分享一个核心思路:能编译期做的事,不要拖到运行时。反射和动态代理本质上是在运行时做决策,这必然带来开销。而编译时注解和代码生成,把决策提前到了编译期,运行时就是普通的代码执行。
我记得有一次优化一个图片加载框架,里面用了反射来解析图片的Exif信息。每次加载图片都要反射调用,导致列表滑动时帧率下降。后来改成编译时生成Exif解析器,帧率从40fps提升到了55fps。这个优化其实不复杂,但效果很明显。
嗯,这一章的内容就到这里。反射和动态代理不是不能用,而是要清楚它们的代价,在合适的地方用合适的技术。