为什么要用依赖注入?
让每个类保持职责单一。
好处:
- 方便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存在什么问题?
- 学习和科普成本极高。
- APT生成的辅助代码增加编译时间。
- APT生成的辅助代码增加了Apk体积。
dagger2中各种基本情况组合起来非常复杂,光注解就非常繁多,比如@Inject、@Component、@Subcomponent、@MultiBinding、@Binds、@ContributesAndroidInjector,要在大型团队中推广,必须要保证至少有一个人能完全hold住所有Dagger2相关的问题,否则就是灾难。
这位负责人还要尽量文档化,负责科普宣讲,减少认知和沟通成本。