Kahn's blogs

Dagger 2 迁移方案和生成代码基本原理

2017/09/14

参考资料和相关知识

  1. https://github.com/google/dagger
  2. https://google.github.io/dagger/
  3. 控制反转(IOC)和依赖注入(DI)的区别
  4. 使用Dagger 2进行依赖注入
  5. Dagger 2 使用,详细解读和从Dagger1迁移的方法介绍
  6. [译]使用 Dagger 2,Mockito 和 自定义 JUnit 规则执行 Android 测试
  7. 依赖注入库Dagger2的使用方法
  8. 这就是Dagger2
  9. http://leoray.leanote.com/post/dagger2

踩过的坑

第一大坑

Error:(18, 10) 错误: com.chriszou.auttutorial.groupshare.CheckoutModel cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.
com.chriszou.auttutorial.groupshare.CheckoutActivity.mCheckoutModel
[injected field of type: com.chriszou.auttutorial.groupshare.CheckoutModel mCheckoutModel]

意思是说注入一个,没有在构造函数上加@Inject,或者没有提供@Provides方法的类

注入到目标类的时候,有两种提供依赖的方式
1, 提供dependencies的module
2, 在dependencies的构造函数上写入@inject标签

注意事项:1,第二种注入方式,构造函数有参数的话,参数需要是一个module.2,…

除了这两种提供dependencies的方法外暂时没发现别的方法.当然了component是必须的.

第二大坑:

guava库版本冲突。

第三大坑:

不rebuild,代码不生成,sync没用

前言

DI的概念就不过多叙述网上有很多,也是使用了很长时间的设计理念。好处多多。例如我现在正在做的记录数据变化监控。如果记录model对象都由DI容器提供,可以直接切换model的实现,使用带有数据追踪功能的子类。会比现在好做很多。

为什么从dagger1迁移到dagger2。
dagger1为半预编译半反射的方法。dagger2被google接手后改为全部使用预编译生成代码的方式。注入时间大大缩短(因为跟手写的代码效率是一样的)。而且库的升级是大势所趋,在合适的时间(不能有了新版本就使用)升级旧有库,可以保证整体工程质量。dagger1已经被废弃,无人维护,越早升级越好。

知识点

  1. AbstractProcessor 预编译处理注解
  2. guava lib 谷人稀的java工具类库 dagger2使用的版本是21,注意冲突问题。类库简介guava(故人稀果然喜欢用自己的工具库)
  3. https://github.com/google/auto Dagger2依赖auto库中的common,auto-value和auto-service,也是生成代码的工具类,谷人希的。使用方法自行百度
  4. javapoet 。square的库,用来通过注解生成代码。使用版本1.7。dagger1就是square公司开发,dagger2fork的dagger1,由google接手
  5. Java 依赖注入标准(JSR-330)简介;

阅读dagger生成依赖注入代码源码的过程

  1. 首先我们知道dagger2是生成的源代码,而不是生成的class文件,并且是使用预编译扫描注解生成的源代码。由此得知肯定使用了AbstractProcessor技术
  2. clone dagger2源码,全局搜索发现并没有找到集成AbstractProcessor的子类,一脸懵逼。好吧,一定是使用了第三方工具。
  3. 首先google dagger2源码分析,后,查看build配置文件,查找依赖库发现auto库。
  4. 搜索auto库使用方法,发现这个东西,BasicAnnotationProcessor,发现了ComponentProcessor集成了它,并使用了auto库的注解@AutoService(Processor.class)。继续google。各种google。
  5. 值得关注的方法,initSteps返回了这个
    xx
  6. 查看ComponentProcessingStep源码,发现它实现了ProcessingStep接口,里面有我们在使用dagger时使用的注解
    xx
  7. 差不多知道了dagger生成源码使用了auto分步step处理注解的思想。关于auto的用法,自行google。

dagger2生成源码的基本过程大概已经了解了。就到这里吧。

迁移步骤

首先要知道dagger2怎么用,文章开头附有各种链接,可以自行搜索。官网上也有迁移指导,不过,对于我们项目,基本无用。

接下来要看我们的项目是怎么使用dagger1的,要达到使用最小的改动平滑过渡到dagger2.

我们的项目dagger1是怎么使用的?

  1. 在主工程的application初始化时,初始化了一个全局对象图ObjectGraph,把所有业务模块的BeanModule全部加载进去,dagger1是把所有需要注入的对象全部配置在了module类的类级别注解上。所以此时,对于dagger1,初始化工作基本已经完成了。xx
  2. 把全局对象图对象(ObjectGraph)赋值给每个子模块。
  3. 在想要注入对象的类中,调用ObjectGraph的inject方法就可以了。
  4. 坑爹的地方来了,dagger1因为是动态注入,是可以在父类里面写inject,子类全部受用的(子类还是要在BeanModule配置)。

迁移流程

  1. dagger2添加了Component的概念,使用了Component接口来生成往目标对象注入依赖对象的工具类。所以我们要把在BeanModule里配置的入口类,全部挪到这个里面来。
  2. dagger2没了对象图的概念,使用component,每个Component接口都会生成一个DaggerXXX(XXX是你的component接口类名,需要rebuild自动生成),在需要注入的地方,使用DaggerXXX.build().inject来注入。因为dagger2生成代码时使用的是最高级别的安全检查,又使用了有向无环图,如果有循环依赖,或者不符合语法的地方,不会生成类。
  3. 因为之前我们是一个模块一个beanmodule,该模块的所有入口类都配置在了一个beanmodule上,所以为了平滑升级,我们把BeanModule中配置的入口类,全部挪到新的component接口中,写成void inject(Xxx obj)的形式(违反了一组功能一个component的原则)。
  4. 问题来了,之前使用的是往父类activity oncreate里面调用inject方法,inject方法的参数为泛型,现在不行了,因为component接口是通过接口内的inject方法参数生成的新类,所以必须是具体类型。

看具体实现代码最清楚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** @version 1.0 * @ClassName * @date 2017/5/3 * @Description */
@Singleton
@Component(modules = {BeanModule.class})
public interface XXXMainComponent {
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
void inject(Xxx object);
}

这个是Component接口,里面的inject方法的参数表示了要注入的类
注解Component参数modules指定了这个组件要使用的Module,Module在dagger2里面就是为了提供对象的(dagger1也一样。。。。)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Module
public class BeanModule {
private final Context context;
public BeanModule(Context context) {
this.context = context;
}
@Singleton
@Provides
Context provideContext() {
return context;
}
}

这是Component使用的BeanModule类,它可以帮助生产context对象。如果你想生产别的对象,需要再添加provideXXX这样的方法,并给该方法添加@Provides 注解。@Singleton是什么意思不做详细解释。都懂的。注:提供provide方法来提供的对象一般是第三方库和一些你无法修改源代码的类的对象。如果给一个类的无参构造函数添加inject注解,该类不需要在module中提供provide方法就可以生成。如果是有参的构造方法,参数必须有无参带inject注解的构造参数或有provide提供对象。总之,得让dagger知道怎么获取这个对象。后面会详细讲对象的构造过程。

怎么注入:

dagger1

每个我们之前使用dagger1的时候每个模块都会有一个XXXApp,例如记录的XxxApp。它缓存了dagger1的对象图,在Activity的oncreate中调用inject方法完成注入。

1
2
3
4
5
6
7
public abstract class XxxBaseActivity extends XxxBaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
XxxApp.inject(this);
}
}

XxxApp.inject(this)的方法inject,参数是一个泛型,调用的就是ObjectGraph的inject方法,参数是一个Object,所以可以直接注入。

dagger2

我们之前写过了XxxMainComponent接口,dagger2会给我们生成一个DaggerXxxMainComponent类,它实现了XxxMainComponent接口。所以你在XxxMainComponent接口注入声明inject方法,DaggerXxxMainComponent都会实现,所以:XxxApp.inject(this)方法的参数就不能是泛型了,因为DaggerXxxMainComponent的inject方法都是具体的参数,泛型set不进去,就像这样
xx

参数改为Object类型也是一样的,不行!因为DaggerXxxMainComponent类中并没有参数为Object的inject方法。

解决方案1:

1
2
3
4
5
6
7
8
9
10
11
12
public static <T> void inject(T t){
try{
if(comonent == null) {
return ;
}
Class clazz = t.getClass();
Method method = component.getClass().getMethod("inject", class);
method.invoke(component, t);
...
}
}

使用反射动态选择调用方法,违反了dagger2的设计理念!本来dagger2就是为了去反射,以最大优化的姿态完成注入,这样岂不是又回去了?

解决方案2:

1
2
3
4
5
6
7
...
if(t instanceof XxxFragment) {
component.inject((XxxFragment), t);
} else if(t instanceof aaa) {
component.inject((aaa), t);
}
...

手动选择类型方法,instanceof关键字java虚拟机优化过,效率非常高(也可以使用比对class是否相等)
此方法需要注意的地方!!!父类不能写在里面,因为instanceof关键字会直接匹配父类,不会进入注入子类的inject方法了!!!(class比对应该不会有这个问题)

简单看一下生成的类DaggerXxxMainComponent
它实现了我们写的XxxMainComponent接口,所以有很多的inject的方法,inject方法有两种实现方式

1
2
3
4
5
6
7
8
9
@Override
pulbic void inject(Aaa object) {
MembersInjectors.<Aaa>noOp().injectMembers(object);
}
@Override
pulbic void inject(Bbb object) {
XxxFragmentMembersInjector.injectMembers(object);
}

第一种
MembersInjectors.noOp().injectMembers(object);是因为在Aaa类中,并没有任何需要注入的成员变量。

第二种是真的注入了对象
里面使用了XxxFragmentMembersInjector变量的injectMebers方法,来看下这个变量是个什么东西

1
private MembersInjector<XxxFragment> XxxFragmentMembersInjector;

发现,MembersInjector是一个接口,找到这个变量在哪里初始化,就可以知道它具体是个什么类型。

这类中有一个initialize 方法,在里面对所有成员变量进行了赋值,参数是我们的module对象

1
2
this.XxxFragmentMembersInjector =
XxxFragment_MembersInjector.create(BxxControllerProvider);

再来看下XxxFragment_MembersInjector.create

1
2
3
public static MembersInjector<XxxFragment> create(Provide<BxxController> mBxxControllerLazyProvider) {
return new XxxFragment_MembersInjector(mBxxControllerLazyProvider);
};

使用参数,构造了它自己,原来XxxFragment_MembersInjector就是一个实现了MembersInjector接口类。
再来看看这个参数是什么?
它是BxxControllerProvider,DoubleCheck类的对象,持有了BxxController_Factory对象,构建BxxController_Factory需要BxxControllerMembersInjector, provideContextProvider这两个参数。

1
2
3
4
this.BxxControllerProvider =
DoubleCheck.provider(
BxxController_Factory.create(
BxxControllerMembersInjector, provideContextProvider));

先不说BxxController_Factory的构造过程,直接看我们首先关注的XxxFragment_MembersInjector是怎么把对象注入到XxxFragment中的。
直接看XxxFragmentMembersInjector.injectMembers(object);这个Object就是XxxFragment对象。

1
2
3
4
5
6
@Overridepublic void injectMembers(XxxFragment instance) {
if (instance == null) {
throw new NullPointerException("Cannot inject members into a null reference");
}
instance.mBxxControllerLazy = DoubleCheck.lazy(mBxxControllerLazyProvider);
}

看到,直接使用“=”往XxxFragment对象的成员变量mBxxControllerLazy赋值了!使用了DoubleCheck.lazy(mBxxControllerLazyProvider)这个东西!
这就是为什么我们声明需要注入的字段的时候不能声明成私有的了,私有成员变量无法被直接引用到。
回过头看下我们在XxxFragment是怎么声明

1
2
@Inject
Lazy<BxxController> mBxxControllerLazy

这里使用了懒加载。

现在要去看DoubleCheck.lazy(mBxxControllerLazyProvider)了。
xx

参数就是BxxControllerProvider,上面说过了,它是一个DoubleCheck,DoubleCheck实现了Provider和Lazy接口,直接返回了,很明显,它是一个单例!

现在我们知道了
@Inject
Lazy mBxxControllerLazy;其实被赋值成了一个DoubleCheck对象,DoubleCheck类实现了Lazy接口。程序捏使用它的时候需要先调用Lazy接口的get,所以去看下DoubleCheck的get是怎么实现的。

xx

这个UNINITIALIZED其实是一个new出来的object,instance成员变量初始化也是赋值了UNINITIALIZED,所以第一次进来肯定是相等的。

内部还是用BxxController_Factory的get方法获取的实际需要注入的对象,再来看看BxxController_Factory的get方法吧

1
2
3
4
@Overridepublic BxxController get() {
return MembersInjectors.injectMembers(
BxxControllerMembersInjector, new BxxController(contextProvider.get()));
}

MembersInjectors.injectMembers返回的实际是第二个参数new BxxController(contextProvider.get()),大功告成,对象在这里被创建了。BxxController的构造方法需要一个context也由contextProvider.get()提供出来了,MembersInjectors.injectMembers方法内部也调用了BxxControllerMembersInjector的injectMembers,为BxxController对象进行了注入。整个流程就结束了!

这个是单例+懒加载的加载过程,它们跟随了DaggerXxxMainComponent对象的生命周期,所以我们如果保证DaggerXxxMainComponent是跟随Application生命周期,就可以保证dagger2对象里设置的单例是有效的!
具体@scope相关注解,自行google!

普通的注入流程就不分析了。都是差不多的样子。

总结:

dagger2会分析依赖条件而生成类。生成的类的形式跟手写的一样。所以没有影响效率的问题。
总的来说生成的类分为,Component,MembersInjector,Provider,Lazy接口的实现类。
每个需要注入的类都会派生出MembersInjector子类负责注入,Provider子类负责提供对象。Provider还有子接口Factory接口。同时实现Provider,Lazy接口的DoubleCheck类,可以实现懒加载。