Android 中的编译时依赖注入权衡

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

作为后端软件开发人员,我习惯将 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 依赖项。有两个步骤:

  1. 在上下文中获取对 eventBus 实例的引用
  2. 使用相关参数调用构造函数

接线配置本身是在所谓的模块中完成的:


 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 的限制,这可能是可以达到的最佳效果。