Android 中的编译时依赖注入权衡
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2800+ 小伙伴加入学习 ,欢迎点击围观
作为后端软件开发人员,我习惯将 Spring 作为我最喜欢的依赖注入引擎。替代方案包括 Java EE 的 CDI,它以不同的方式实现了相同的结果。然而,两者都是在运行时注入:这意味着在应用程序启动时需要付出一定的性能成本,即满足所有依赖项所需的时间。在应用程序服务器上,应用程序的生命周期以天(如果不是几周)为单位,启动时间开销是可以接受的。如果服务器只是大型集群中的一个节点,它甚至是完全透明的。
作为 Android 用户,当我启动一个应用程序并且它在打开之前滞后几秒钟时,我并不高兴。如果我们在那个时间上再增加几秒钟,就用户友好性而言将是非常糟糕的。更糟糕的是,DI 引擎的内存消耗将是一场灾难。这就是 Square 开发称为 Dagger 的编译时依赖注入机制的原因。请注意,Google 目前正在开发
Dagger 2
。在继续之前,我必须承认 Dagger 2 的文档是简洁的——充其量。但这是另一篇博文的绝佳机会
Dagger 2 与注释处理器一起工作:编译时,它将分析您的注释代码并生成组件之间的连接代码。好消息是这段代码与您手动编写的代码非常相似,没有秘密的黑魔法(与运行时 DI 及其代理相反)。以下代码显示要注入的类:
public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {
private final EventBus eventBus;
public TimeSetListener(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
eventBus.post(new TimeSetEvent(hourOfDay, minute));
}
}
请注意,代码在各个方面都完全独立于 Dagger。无法推断最终将如何注入。有趣的部分是如何使用 Dagger 注入所需的
eventBus
依赖项。有两个步骤:
-
在上下文中获取对
eventBus
实例的引用 - 使用相关参数调用构造函数
接线配置本身是在所谓的模块中完成的:
public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {
private final EventBus eventBus;
public TimeSetListener(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
eventBus.post(new TimeSetEvent(hourOfDay, minute));
}
}
请注意,
EventBus
作为参数传递给方法,由上下文提供。此外,范围明确为
@Singleton
。
到工厂的绑定发生在 组件 中,它引用所需的模块(或更多):
public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {
private final EventBus eventBus;
public TimeSetListener(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
eventBus.post(new TimeSetEvent(hourOfDay, minute));
}
}
这非常简单……直到有人注意到 Android 中的一些(如果不是大多数的话)对象具有由 Android 本身管理的生命周期,而无需调用我们的
注入友好构造函数
。活动就是这样的对象:它们由框架实例化和启动。只有通过诸如
onCreate()
之类的专用生命周期方法,我们才能将代码挂接到对象中。这个用例看起来更糟,因为字段注入是强制性的。更糟糕的是,它还需要调用 Dagger:在这种情况下,它充当普通工厂。
public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {
private final EventBus eventBus;
public TimeSetListener(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
eventBus.post(new TimeSetEvent(hourOfDay, minute));
}
}
我们第一次看到与 Dagger 的耦合,但它是一个很大的耦合。什么是
DaggerApplicationComponent
?前一个
ApplicationComponent
的实现,以及一个提供它们实例的工厂。由于它不提供
inject()
方法,我们必须在我们的接口中声明它:
public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {
private final EventBus eventBus;
public TimeSetListener(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
eventBus.post(new TimeSetEvent(hourOfDay, minute));
}
}
作为记录,生成的类如下所示:
public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {
private final EventBus eventBus;
public TimeSetListener(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
eventBus.post(new TimeSetEvent(hourOfDay, minute));
}
}
天下没有免费的午餐。尽管编译时 DI 乍一看非常有吸引力,但当用于我们的代码不管理生命周期之外的对象时,它就变得不那么吸引人了。缺点变得很明显:耦合到 DI 框架,更重要的是增加了对类进行单元测试的难度。但是,考虑到 Android 的限制,这可能是可以达到的最佳效果。