Dagger - 匕首,顾名思义,比ButterKnife这把黄油刀锋利得多。Square为什么这么有自信地给它取了这个名字,Google又为什么会拿去做了Dagger2呢(不都有Guice和基于其做的RoboGuice了么)?希望本文能讲清楚为什么要用Dagger2,又如何用好Dagger2。
本文会从Dagger2的起源开始,途径其初衷、使用场景、依赖图,最后介绍一下我在项目中的具体应用和心得体会。
Origin
Dagger2,起源于Square的Dagger,是一个完全在编译期间进行的依赖注入框架,完全去除了反射。
关于Dagger2的最初想法,来自于2013年12月的Proposal: Dagger 2.0,Jake大神在issue里面也有回复哦,而idea的来源者Gregory Kick的GitHub个人主页也没多少follower,自己也没几个项目,主要都在贡献其他的repository,可见海外重复造轮子的风气比我们这儿好多了。
扯远了,Dagger2的诞生就是源于开发者们对Dagger1半静态化半运行时的不满(尤其是在服务端的大型应用上),想要改造成完整的静态依赖图生成,完全的代码生成式依赖注入解决方案。在权衡了什么对Android更适合,以及对大型应用来说什么更有意义(往往有可怕数量的注入)两者后,Dagger2诞生了。
初衷
Dagger2的初衷就是装逼,啊,不对,是通过依赖注入让你少些很多公式化代码,更容易测试,降低耦合,创建可复用可互换的模块。你可以在Debug包,测试运行包以及release包优雅注入三种不同的实现。
依赖注入
说到依赖注入,或许很多以前做过JavaEE的朋友会想到Spring(SSH在我本科期间折磨得我欲生欲死,最后Spring MVC拯救了我)。
我们看个简单的比较图,左边是没有依赖注入的实现方式,右边是手动的依赖注入:
我们想要一个咖啡机来做一杯咖啡,没有依赖注入的话,我们就需要在咖啡机里自己去new泵(pump)和加热器(heater),而手动依赖注入的实现则将依赖作为参数,然后传入,而不是自己去显示创建。在没有依赖注入的时候,我们丧失了灵活性,因为一切依赖是在内部创建的,所以我们根本没有办法去替换依赖实例,比如想把电加热器换成火炉或者核加热器,看一看下图,是不是更清晰了:
为什么我们需要DI库
但问题在于,在大型应用中,把这些依赖全都分离,然后自己去创建的话,会是一个很大的工作量——毫无营养的公式化代码,一堆Factory类。不仅仅是工作量的问题,这些依赖可能还有顺序的问题,A依赖B,B依赖C,B依赖D,如此一来C、D就必须在A、B的后面,手动去做这些工作简直是一个噩梦 =。=(哈哈,是不是想到了appliation初始化那些依赖)。Google的工程师碰到的问题就是在Android上有3000行这样的代码,而在服务器上的大型程序则是100000行。
你会想自己维护这样的代码吗?
Why Dagger2
先来看看如果用Spring实现上面提到的咖啡机依赖,我们需要做什么:
不错,就是xml,当然,我们也不需要去关心顺序了,Spring会帮我们解决前后顺序的依赖问题。
但仔细想想,你会想去自己写这样的xml代码吗?layout.xml已经写得我很烦了。而且Spring是在运行时验证配置和依赖图的,你不会想在外网运行的app里让用户发现你的依赖注入出了问题的(比如bean名字打错了)。再加上xml和Java代码分离,很难追踪应用流。
Guice虽然较Spring进了一步,干掉了xml,通过Java声明依赖注入比起Spring好找多了,但其跟踪和报错(运行时的图验证)实在令人抓狂,而且在不同环境注入不同实例的配置也挺恶心的(if else各种判断),感兴趣的可以去看看,项目就在GitHub上,Android版本的叫RoboGuice。
而Dagger2和Dagger1的差别在上节已经提到了,更专注于开发者的体验,从半静态变为完全静态,从Map式的API变成申明式API(@Module),生成的代码更优雅,更高的性能(跟手写一样),更简单的debug跟踪,所有的报错也都是在编译时发生的。
Dagger2使用了JSR 330的依赖注入API,其实就是Provider了:
public interface Provider<T> {
T get();
}
// Usage:
Provider<T> coffeeMakerProvider = ...;
CoffeeMaker coffeeMaker = coffeeMakerProvider.get();
Dagger2基于Component注解:
@Component(modules = DripCoffeModule.class)
interface CoffeeMakerComponet {
CoffeeMaker getCoffeeMaker();
}
// 会生成这样的代码,Dagger_CoffeeMakerComponent里面就是一堆Provider,
// 或者是单例,或者是通过DripCoffeeModule申明new的方式,开发者不必关心依赖顺序
CoffeeMakerComponent component = Dagger_CoffeeMakerComponent.create();
CoffeeMaker coffeeMaker = component.getCoffeeMaker();
除了上面提到的各种好处,不得不提的是也有对应问题:丧失了动态性,在之后的实践中我会举个例子描述一下,但相对于那些好处来说,我觉得是可接受的。Everything has a Price to Pay。啊,对了,还有另一点,没法自动升级,从Dagger1到Dagger2,当然如果你的app是没有历史负担的(本系列的前提),那这不算问题。
如果对性能感兴趣的话,可以去看看Comparing the Performance of Dependency Injection Libraries,RoboGuice:Dagger1:Dagger2差不多是50:2:1的一个性能差距。
如果你用了Dagger2,而你的服务端还在用Spring,你可以自豪地说,我们比你们领先5年。而Google的服务端确实已经用了Dagger2。
使用场景
上面也曾经提到了,因为手动去维护那些依赖关系、范围很麻烦,就连单例我都懒得写,何况是各种Factory类,老在那synchroized烦不烦。而如果不去写那些Factory,直接new,则会导致后期维护困难,比如增加了一个参数,为了保证兼容性,就只能留着原来的构造函数(习惯好一点的标一下deprecated),再新增一个构造函数。
Dagger2解决了这些问题,帮助我们管理实例,并进行解耦。new只需要写在一个地方,getInstance也再也不用写了。而需要使用实例的地方,只需要简简单单地来一个@inject,而不需要关心是如何注入的。Dagger2会在编译时通过apt生成代码进行注入。
想想你所有可能在多个地方使用的类实例依赖,比如lbs服务,比如你的cache,比如用户设置,比起getInstance,比起new,比起自己用注释去注明必须维持这种先后关系(说到此处,想到上个东家的android app初始化时候,必须保持正确顺序不然立马crash,singleton还必须只能init一次的糟糕代码),为什么不用dagger来做管理?Without any performance overhead。
Dagger2基于编译时的静态依赖图构建还能避免运行时再出现一些坑,比如循环依赖,编译的时候就会报错,而不会在运行时死循环。
生动点来说的话。有一场派对:
Android开发A说,有妹子我才来。
美女前端B说,有帅哥设计师,我才来。
iOS开发C说,有Android开发,我才来。
帅哥设计师说,只有礼拜天我才有空。
class AndroidDeveloper extends PartyMember {
public AndroidDeveloper(PartyMember female) throws NotMeizhiSayBB;
}
public class FrontEndDeveloper extends PartyMember {
public FrontEndDeveloper(Designer designer) throws NotHandsomeBoySayBB;
}
class IOSDeveloper extends PartyMember {
public IOSDeveloper(AndroidDeveloper dev);
}
class Designer extends PartyMember {
public Designer(Date date) throw CannotComeException;
}
class PartyMember {
private int mSex = 0; // 1 for male, 2 for female.
public void setSex(int sex);
}
// 手动DI,要自己想怎么设计顺序,还不能轻易改动
Designer designer = new Designer("礼拜天");
FrontEndDeveloper dev1 = new FrontEndDeveloper(designer);
dev1.setSex(2);
AndroidDeveloper dev2 = new AndroidDeveloper(dev1);
IOSDeveloper dev3 = new IOSDeveloper(dev2);
// With Dagger2
@Inject
Designer designer;
@Inject
FrontEndDeveloper dev1;
@Inject
AndroidDeveloper dev2;
@Inject
IOSDeveloper dev3;
// 不使用DI太可怕了...自己想象一下会是什么样吧
...我懒
Scope
Dagger2的Scope,除了Singleton(root),其他都是自定义的,无论你给它命名PerActivity、PerFragment,其实都只是一个命名而已,真正起作用的是inject的位置,以及dependency。
Scope起的更多是一个限制作用,比如不同层级的Component需要有不同的scope,注入PerActivity scope的component后activity就不能通过@Inject去获得SingleTon的实例,需要从application去暴露接口获得(getAppliationComponent获得component实例然后访问,比如全局的navigator)。
当然,另一方面则是可读性和方便理解,通过scope的不同很容易能辨明2个实例的作用域的区别。
依赖图例子
如上是一个我现在使用的Dagger2的依赖图的简化版子集。
ApplicationComponent作为root,拆分出了3个module
- ApplicationModule(application context,lbs服务,全局设置等)
- ApiModule(Retrofit那堆Api在这里)
- RepositoryModule(各种repository)。
这里为了妥协内聚和简洁所以保持了这三个module。你不会想看到自己的di package下有一大堆module类,或者某个module里面掺杂着上百个实例注入的。
UserComponent用在用户主页、登录注册,以及好友列表页。所以你能看到UserModule(用户系统以及那些UseCase)以及需要的赞Module、相册Module。
TagComponent是标签系统,有自己的标签Module以及赞Module(module重用),用在了标签搜索、热门标签等页面。
是不是很好理解?位于上层的component是看不到下层的,而下层则可以使用上层的,但不能引用同一层相邻component内的实例。
如果你的应用是强登录态的,则更可以只把UserComponent放在第二层,Module构造函数传入uid(PerUser scope,没有uid则为游客态,供deeplink之类使用),而所有需要登录态的则都放在第三层。
一个简单的应用就是这样了,而Component继承,SubComponent(共享的放在上层父类),不同component的module复用(一样可以生成实例绑定,只是没法共享component中暴露的接口罢了)这些则是不同场景下的策略,如果有必要我会再开一篇讲讲这些深入的使用。
具体应用和心得体会
No Proguard rules need。因为0反射,所以完全不需要去配置proguard规则。
因为需要静态地去inject,如果一些参数需要运行时通过用户行为去获得,就只能使用set去设置注入实例的参数(因为我们的injection通常在最早,比如onCreate就需要执行)。这就是上文提到过的,因为完全静态而丧失了一定的动态性。
Singleton是线程安全的,请放心,如果实在怀疑,可以去检查生成的源码,笔者已经检查过了...
粒度的问题,如果基于页面去划分的话,老实说笔者觉得实在太细太麻烦,建议稍微粗一点,按照大功能去分,完全可以通过拆分module或者SubComponent的形式去解决复用的问题,而不用拆分出一大堆component,module只要足够内聚就可以,而不需要拆分到某个页面使用的那些。
fragment的问题,因为其诡异的生命周期,所以建议在实在需要fragment的时候,让activity去创建component,fragment通过接口(比如HasComponent)去获得component(一个activity只能inject一个component哦)。
举一个我遇到的例子来说说方便的地方,有一个UseCase叫做SearchTag,原先只需要TagRepository,ThreadExecutor,PostThreadExecutor三个参数。现在需求改变了,需要在发起请求前先进行定位,然后把位置信息也作为请求的参数。我们只需要简单地在构造函数增加一个LbsRepository,然后在buildUseCaseObservable通过RxJava组合一下,这样既避免了底层repository的耦合,又对上层屏蔽了复杂性。
再讲讲之前提到的依赖吧,我们有很多同级的实例,以Singleton为例,比如有一个要提供给第三方sdk的Provider依赖了某个Repository,直接在构造函数里加上那个Repository,然后加上@Inject,完全不需要关心前后顺序了,省不省心?还可以随时在单元测试的包注入一个不需要物理环境的模拟repository。想想以前你怎么做,或者在调用这个的初始化前init依赖的实例,或者在初始化里去使用依赖类的getInstance(),是不是太土鳖?
强烈推荐你在自己的项目里使用上,初期可能怀着装逼的心情觉得有点麻烦,熟练后你会发现简直太方便了,根本离不开(其实是我的亲身经历 哈哈)。
总结
本篇讲了讲Dagger2,主要还是在安利为什么要用Dagger2,以及一些正确的使用姿势,因为时间原因来不及写个demo来说说具体实现,欢迎大家提出意见和建议。
有空的话我最近会在GitHub上写一下demo,你如果有兴趣可以follow一下等等更新: markzhai(希望在4月能完成,哈哈...)。
下集预告
怎么用Retrofit、Realm和RxJava搭建data层。
参考文献
- DAGGER 2 - A New Type of dependency injection: https://youtu.be/oK_XtfXPkqw
- Dagger 2 Official Site: http://google.github.io/dagger/
- Dagger 2 Design Doc: http://goo.gl/mW474Z
原文链接:http://blog.zhaiyifan.cn/2016/03/27/android-new-project-from-0-p4/