Dagger2 in Android(三)Scope与生命周期

1,865 阅读8分钟

系列文章

前言

之前我们已经学习了 Dagger 的基础知识、模块化管理,本章将是 Dagger 基础使用的最后一章。

Scope 被误称 Dagger 的黑科技,但实际上它非常简单,但错误理地解它的人却前仆后继。希望小伙伴们认真阅读这一章,第一次学习时一定要正确理解,不然后边再纠正会感觉世界观都被颠覆了。

@Scope

终于来了。Scope 正如字面意思,它可以管理所创建对象的“生命周期”。Scope 的定义方式类似 Qualifier,都需要利用这个注解来定义新的注解,而不是直接使用。

重点!!!这里所谓的「生命周期」是与 Component 相关联的。与 Activity 等任何 Android 组件没有任何关系!

下面是典型的错误案例

定义一个 @PerActivity 的 Scope, 于是认为凡是被这个 PerActivity 注解的 Provides 所创建的实例, 就会自动与当前 Activity 的生命周期同步。

上述想法非常可爱,非常天真,所以很多很多程序猿们都是可爱的 (o´・ェ・`o) 要是仅仅靠一个注解就能全自动同步生命周期,那也太智能了。


下面开始好好学习啦。先来说说正常的注入流程:目标类首先需要创建一个 Component 的实例,然后调用它定义的注入方法,传入自身。Component 就会查找需要注入的变量,然后去 Module 中查找对应的函数,一旦找到就调用它来获取对象并注入。

这里我们可以发现一个关键,也就是对象最终是 Module 里的函数提供的。这个函数当然也是我们自己编写的,大部分情况下在这里会直接 new 一个出来。因此,如果多次注入同一类型的对象,那么这些对象将分别创建,各不相同。看下面的例子:

class MainAty : AppCompatActivity() {

    @Inject
    lateinit var chef1: Chef
	@Inject
	lateinit var chef2: Chef

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DaggerMainComponent.builder().mainModule(MainModule()).build().inject(this)
}

执行这段代码会进行两次注入,最终 chef1 与 chef2 将是两个完全不同的对象。

那如果我们想获得一个「局部单例」呢?这时候就需要 Scope 了。首先我们要定义一个 Scope:

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

再次强调!ActivityScope 只是一个名字而已,不代表它会自动与 Activity 的生命周期绑定!

然后在 Module 对应的方法上加上 @ActivityScope:

@Module
class MainModule {

    @Provides
    @ActivityScope
    fun provideChef() = Chef()

    @Provides
    @ActivityScope
    fun provideStove() = Stove()
}

最后根据要求,如果 Module 有 Scope,那么对应的 Component 也必须有,所以给 Component 也加上:

@Component(modules = [MainModule::class])
@ActivityScope
interface MainComponent {
    fun inject(activity: MainAty)
}

[注] Component 可以关联多个 Scope。

此时我们再执行上述代码,会发现 chef1 与 chef2 是同一个对象。这就实现了局部单例,也就是 Scope 的作用。神奇吧!虽然我们只简单地 new 了一个对象,却能实现单例。其实也不奇怪,看看源码就可以发现,加上了 Scope 后 Dagger 内部会自动把创建的对象缓存起来。

何为局部单例

局部单例意思是,在同一个 Component 下是单例的,也呼应了前面所说 这里所谓的「生命周期」是与 Component 相关联的。因为我们在这个 Activity 中只创建了一个 Component 因此注入的对象是单例的。但若换一个 Activity 那么还是会生成新的对象,其本质原因是 Component 实例变了。

为什么能实现 Activity 生命周期同步

这个是真的能实现的,但和 Dagger 没关系。一起思考下:我们在 Activity 的 onCreate() 方法中进行了注入,此时对象被创建,也就是创建周期同步了√。创建后有两个对象会持有它的引用:① Activity ② Component(为了实现局部单例会缓存),而 Component 实际上并没有被我们保存引用,它在注入完成后随时会被回收掉。因此最终注入的对象只有 Activity 在引用,那自然当 Activity 被销毁时就会被同步销毁√。进而实现了所谓的「生命周期同步」。

结论很明显了,Scope 不能管理生成对象的真正生命周期,只能控制对于同一个 Component 是否是局部单例的,请各位务必准确理解这一点。

@Singleton

理解了前面的 @Scope,那么这个 @Singleton 就没有任何难度了。

上面为了实现局部单例,我们自定义了一个 Scope 名为 @ActivityScope。这很麻烦对不对?因为几乎所有程序有会用到单例对象,为了方便,Dagger 帮我们预定义了一个 Scope ,这就是 @Singleton。

所以 @Singleton 没有任何特殊之处(其实有一点点点点的特殊,最后讲),它仅仅是为了方便而已。你可以把 @Singleton 直接替换成任何一个自定义的 Scope 代码逻辑不会发生任何改变!

任何 Provides 都不会因为被 Scope 而自动地变成「全局单例」,@Singleton 亦然。

@Reusable

它的作用类似 Scope 但不完全相同。Scope 目的是在 Component 的生命周期内保证对象的单例,其实它缓存了生成的对象,并使用 DoubleCheck 来检查保证单例。因此被 Scope 标注的 Provides 是绑定到 Component 的。

而 Reusable 只是为了尽可能地重用对象。它没有进行多线程检查,因此无法保证单例。最关键的是 Reusable 并不绑定 Component。因此一个被 Reusable 注解的 Provides 所提供的对象,会尽可能地在全局范围内重用,这将拥有比 Scope 更好的性能。

因为 Reusable 不与 Component 绑定,因此需要在 Component 也标记注解,只需在 Module 标记即可。现在我们把上面的例子改成 Reusable:

@Module
class MainModule {

    @Provides
    @Reusable // 替换 Scope
    fun provideChef() = Chef()

    @Provides
    @Reusable
    fun provideStove() = Stove()
}
@Component(modules = [MainModule::class])
//@ActivityScope 不再需要额外的注解
interface MainComponent {
    fun inject(activity: MainAty)
}

OK~ 就这么简单。现在 Chef 已经可以全局重用了,但不保证是单例的。

全局单例

既然 Scope 只能保证局部单例,但我们如何实现全局单例呢。

我们已经知道了,局部单例是与 Component 绑定的,因此只要 Component 是全局单例的,那么它对应的 Module 下生成的所有对象都会变成全局单例,举个例子:已知 a < b,那如何实现 a < 100?答:只需令 b = 100 即可。

那如何保证 Component 全局单例?因为 Component 是 Dagger 自动生成的,我们不可能直接把他改为传统的单例模式,那就只能从应用生命周期入手。我们只需规定:只在 Application 类的 onCreate() 函数中实例化 Component,那个这个 Component 一定是单例的。其他地方如果需要用到,完全可以 (getApplication() as MyApp).component 这样获取。

下面是一个例子:

@Module
class AppModule(val context: Context) {

    @Provides
    @Singleton
    fun provideContext() = context

}

@Component(modules = [AppModule::class])
@Singleton
interface AppComponent {
	fun context(): Context
}
class MyApplication: Application {

    lateinit var component: AppComponent

    override fun onCreate() {
        super.onCreate();
        component = DaggerAppComponent.builder().appModule(AppModule(this)).build();
    }
}

如此一来,我们就实现了单例的 Component,其他 Component 可以依赖这个,进而能够在任何地方拿到 Context 来用。根据业务需要,我们可以在 AppModule 里定义更多的 Provides 来注入全局单例的对象,例如数据库等。

@BindsInstance

BindsInstance 用于简化编写含参构造函数的 Module。 遇到这种情况我们应该首选 BindsInstance 方式,而不是在 Module 的构造函数中增加参数。上面的 AppModule 是一个典型的例子。下面我们将改写它:

@Component()
@Singleton
interface AppComponent {
	fun context(): Context
	
    @Component.Builder // 自定义Builder
    interface Builder {
        @BindsInstance
        fun context(context: Context): Builder

        fun build(): AppComponent
    }
}

看到没,这下连 Module 都免了。

之前我们注入时是这样写的:

DaggerAppComponent.builder().appModule(AppModule(this)).build();

现在只需这样写:

DaggerAppComponent.builder().context(this).build();

注意: 在调用 build() 之前,必须先调用所有 BindsInstance 的函数来传入所需参数。

Scope 的要求

多个 Scope 和多个 Component 使用时有一些要求需要遵守:

  • Component 和他所依赖的 Component 不能用相同的 Scope。编译时会报错,因为这有可能破坏 Scope 的范围,详见 issues
  • @Singleton 的 Component 不能依赖其他 Component。这个好理解,毕竟 Singleton 设计及就是用来做全局的。如果有需求请自定义 Scope。(这算是 Singleton 的一点点特殊)
  • 无 Scope 的 Component 不能依赖有 Scope 的 Component,这也会导致 Scope 被破坏。
  • Module 以及通过构造函数注入依赖的类以及其 Component 必须有相同 Scope。

总结

写了一晚上一夜,终于写完就 Dagger 基础了。下面会继续写 Android 方面 Dagger 的特殊功能。

回想起自己学 Dagger 的历程,真的是非常头疼。各种概念越看越晕。网上还有很多不负责任的教程自己都没搞懂就开始误导别人。希望这个系列文章能给 Dagger 的初学者带来一点清新的感觉吧。