0%

依赖注入 - Dagger2基本机制

为什么要用依赖注入?

让每个类保持职责单一。

好处:

  • 方便mock测试,减少排错和调试成本。
  • 类职责单一后,代码复用性非常强,减少改动成本。
  • 方便单元测试、自动化测试,促进项目健壮性。

Dagger2的优势?

  • 无反射,编译时生成注入代码,无运行时性能损耗。
  • 符合jsr-330依赖注入标准,切换成本低。

其他Java下的依赖注入框架,比如Dagger1、Guice,是通过反射在运行时对要注入的对象进行赋值,反射在运行时是比较耗时的,Dagger2没有反射,在编译时进行生成辅助代码,利用辅助代码做对象赋值,运行时性能没有消耗。

Dagger2按照jsr-330依赖注入标准要求来设计,比如支持@Inject、@Qualifier、@Scope等jsr-330中规定的注解,以后替换为其他符合jsr-330标准的依赖注入框架时切换成本小。

Dagger2基本运行流程

  • 开发者把要注入的对象写在Module对象中,Module对象挂载在各种Component下,都用注解来配置。
  • 要注入的对象的生命周期受Component对象的生命周期控制,Component对象什么时候创建、什么时候丢弃是完全自行控制的。
  • 编译时通过解析注解处理器,生成注入相关的代码。
  • 在Activity、Fragment中通过调用Component的inject()方法,给所有标注为@Inject的属性赋值。
    • 也可以通过Component直接get到对象。
    • inject()后,所有依赖路径上的对象都会被创建。

作用域Scope

Scope可以在三个地方使用:

  • Component类上标记。
  • Module中providerXXX方法。
  • Dagger2要创建的类上标记。

作用域的唯一的实际作用,是让Component里的对象创建过后就缓存在Component对象里。

所有作用域是完全跟Component对象的生命周期走的。

比如,有一个UserScope,表示用户登陆到用户注销期间的作用域范围。

  • 在Dagger2中,UserScope是要标记到Component上去的,可以把所有用户登陆期间需要的对象都挂载到一个UserComponent上去。
  • 用户登陆时创建一个UserComponent对象并保存,用户注销时丢弃保存的UserComponent对象。
  • 在Module中标记为UserScope的对象,通过UserComponent去获取的时候都是同一个。

作用域如何嵌套?

比如用户作用域内,可能通过蓝牙绑定智能设备到App,会存在设备相关的状态,就定义一个DeviceComponent代表设备相关的状态对象。

  • 在设备绑定时创建DeviceComponent对象并保存,在设备解绑时丢弃DeviceComponent对象。
  • 再次绑定设备时,创建新的DeviceComponent对象示例,这样挂载在DeviceComponent的对象就都重新生成了,设备的状态对象都会更新。
  • 设备绑定和解绑的动作一定是发生在用户登陆期间的,所以DeviceComponent和UserComponent并无代码上的直接关联,就可以实现概念上的作用域嵌套。

SubComponent解决了什么?

有了Component,怎么还搞一个SubComponent?

顾名思义,就是建立Component的层级关系,而建立层级关系是为了读取共享的对象。

比如:

  • 用户登陆生命周期对应UserComponent。
  • 用户绑定手环手表的生命周期对应DeviceComponent。
  • 用户在App开启设备的跑步到停止跑步的生命周期对应RunningComponent。
  • 用户在App开启设备的心率检测到结束心率检测的生命周期对应HeartRateComponent。

这里的层级关系从功能上看比较明显:

  • 用户绑定了设备才能启用跑步和心率检测的功能,所以:
    • HeartRateComponent和RunningComponent平级。
    • DeviceComponent是RunningComponent和HeartRateComponent的父Component。
    • RunningComponent和HeartRateComponent中的对象想要读取设备信息,应该都是读取DeviceComponent中同一份设备信息,这就催生了共享的需求。
  • 不同用户之间设备信息是不一样的,所以:
    • UserComponent是DeviceComponent的父Component。
    • DeviceComponent中的对象需要读取用户信息,读取的都是UserComponent下的对象状态。
    • RunningComponent和HeartRateComponent中的对象要读取用户相关的状态,也要读取UserComponent挂载的对象。
    • 退出登录换一个账号登录后,DeviceComponent里的对象内容应该也要跟着变,因为不同的用户绑定的设备不一样。

如果没有Component的层级关系,DeviceComponent和RunningComponent中的对象要读取设备和用户相关的状态都要写重复的代码,Component层级关系消除了冗余的重复。

如何异步注入?

官方提供Producers机制,理解起来比较困难,其实更方便的是,通过Dagger2获取一个创建对象的函数,自己在代码里异步控制什么时候创建对象。

多绑定解决了什么?

多绑定的意思是把要注入的对象信息都存储到一个集合里,比如Set或Map。

把要注入的对象存储到集合有什么好处?

可以实现一种插件化的架构。
把对象收集到一个中心,再做对象注入的时候,不需要依赖某个具体的Component或者Module对象,而是直接去中心里的集合中查表就行了。

比如dagger.android中:

  • 每个Activity和Fragment都是单独的生命周期,都是单独的SubComponent,因为它们可能会读取共享的父Component的里的状态值。
  • 可以把这些SubComponent挂载的对象通过多绑定的注解标记,存入一个Map中,Key就是Activity或者Fragment类名。
  • 在Activity和Fragment的onCreate中,就可以只调用AndroidInjection.inject(this)
  • Dagger2内部就可以到Map中通过类名的Key查询到对应的Activity或者Fragment对应的SubComponent。
  • 然后创建Component对象,做实际的对象创建和注入。

这样就简化了注入处的代码。

参考:

Dagger2存在什么问题?

  1. 学习和科普成本极高。
  2. APT生成的辅助代码增加编译时间。
  3. APT生成的辅助代码增加了Apk体积。

dagger2中各种基本情况组合起来非常复杂,光注解就非常繁多,比如@Inject、@Component、@Subcomponent、@MultiBinding、@Binds、@ContributesAndroidInjector,要在大型团队中推广,必须要保证至少有一个人能完全hold住所有Dagger2相关的问题,否则就是灾难。

这位负责人还要尽量文档化,负责科普宣讲,减少认知和沟通成本。