markzhai's home

从零开始的Android新项目4 - Dagger2篇

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拯救了我)。

我们看个简单的比较图,左边是没有依赖注入的实现方式,右边是手动的依赖注入:
Without DI and with Maunl DI

我们想要一个咖啡机来做一杯咖啡,没有依赖注入的话,我们就需要在咖啡机里自己去new泵(pump)和加热器(heater),而手动依赖注入的实现则将依赖作为参数,然后传入,而不是自己去显示创建。在没有依赖注入的时候,我们丧失了灵活性,因为一切依赖是在内部创建的,所以我们根本没有办法去替换依赖实例,比如想把电加热器换成火炉或者核加热器,看一看下图,是不是更清晰了:
Without DI and with Maunl DI

为什么我们需要DI库

但问题在于,在大型应用中,把这些依赖全都分离,然后自己去创建的话,会是一个很大的工作量——毫无营养的公式化代码,一堆Factory类。不仅仅是工作量的问题,这些依赖可能还有顺序的问题,A依赖B,B依赖C,B依赖D,如此一来C、D就必须在A、B的后面,手动去做这些工作简直是一个噩梦 =。=(哈哈,是不是想到了appliation初始化那些依赖)。Google的工程师碰到的问题就是在Android上有3000行这样的代码,而在服务器上的大型程序则是100000行。

你会想自己维护这样的代码吗?

Why Dagger2

先来看看如果用Spring实现上面提到的咖啡机依赖,我们需要做什么:
DI with 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了:

1
2
3
4
5
6
7
public interface Provider<T> {
T get();
}

// Usage:
Provider<T> coffeeMakerProvider = ...;
CoffeeMaker coffeeMaker = coffeeMakerProvider.get();

Dagger2基于Component注解:

1
2
3
4
5
6
7
8
9
@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开发,我才来。
帅哥设计师说,只有礼拜天我才有空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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个实例的作用域的区别。

依赖图例子

Simple Graph

如上是一个我现在使用的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层。

参考文献

扩展阅读

Mark Zhai (翟一帆) wechat
欢迎您扫一扫上面的微信公众号,订阅我们的公众号!
坚持原创技术分享,您的支持将鼓励我继续创作!

热评文章