-
概述
依赖注入(DependencyInject)是一个存在已久的概念了,起初,这个概念在服务器盛行,我们熟知的服务端的控制反转(IOC)中就包含了依赖注入。
我们知道,传统依赖注入的原理就是利用反射来替代new形式的创建,这可以帮我们省去大量的创建代码,而且整个代码看起来也要优雅很多,但是因为反射是通过动态地查找类、方法或字段的字节码入口来完成同new一样的对象创建和类变量访问,这使得性能上会出现影响。
但是影响大小是相对的,对服务端来说,网络的响应、请求的转换和资源的IO访问等对性能的影响和反射对性能的影响相比要大得多,再加上服务器的内存容量要大得多,而且还有集群,所以通常常用的对象会被提前创建以节省请求时的时间。而对于移动端来说,内存容量相对来说要小的多,而且本地代码执行的速度直接会影响到用户的视觉体验,所以反射对移动端来说是一瓶毒药,要尽量的避免在移动端使用反射。
但是我们又想使用依赖注入的好处怎么办呢?于是,有了一种新的依赖注入的概念。
-
Dagger的实现原理
当前比较热门一个DI库—Dagger,就是移动端应用依赖注入的答案,他用的就是另一种依赖注入的思路,我们通过手动创建依赖注入的过程来了解Dagger的实现原理。
我们拿Android中的一个普通依赖流程来说明。
现在有一个Activity,它依赖于它的ViewModel,ViewModel又依赖于Repository,Repository内部又依赖着LocalDataSource(本地数据库)和RemoteDataSource,LocalDataSource又依赖Room,RemoteDataSource又依赖Retrofit:
代码如下:
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) // Then, satisfy the dependencies of UserRepository val remoteDataSource = UserRemoteDataSource(retrofit) val localDataSource = UserLocalDataSource() // Now you can create an instance of UserRepository that LoginViewModel needs val userRepository = UserRepository(localDataSource, remoteDataSource) // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = LoginViewModel(userRepository) } }
我们可以看到,每次创建一个ViewModel都要事先创建很多依赖对象,这没办法复用代码,会产生很多重复的样板代码,创建的顺序也是固定的。
所以我们能改进的一点就是把这块代码抽出来,至于抽取到哪,我觉得还是看具体的业务,如果是我们举例的这种情况的话,考虑到Retrofit、LocalDataSource、RemoteDataSource等在所有的ViewModel都会用到,所以如果为每一个ViewModel建立一个类去集中这部分代码的话,那站在整个项目的角度来看,Retrofit、LocalDataSource和RemoteDataSource等的逻辑还是会产生重复代码,所以对于这个示例来说,还是抽取到一个App级别的公用容器类中为好。比如像下面这样:
// Container of objects shared across the whole app class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) private val remoteDataSource = UserRemoteDataSource(retrofit) private val localDataSource = UserLocalDataSource() // userRepository is not private; it'll be exposed val userRepository = UserRepository(localDataSource, remoteDataSource) }
// Custom Application class that needs to be specified // in the AndroidManifest.xml file class MyApplication : Application() { // Instance of AppContainer that will be used by all the Activities of the app val appContainer = AppContainer() }
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets userRepository from the instance of AppContainer in Application val appContainer = (application as MyApplication).appContainer loginViewModel = LoginViewModel(appContainer.userRepository) } }
这样一来,我们的代码就清爽多了。还有一点值得理解的是,这里的Repository没有使用单例,所有的场景都使用同一个实例不太好测试,因为业务场景是随时会改变的,必须考虑扩展和其他变化的测试,单例只适用于那些基础性的、不太可能会变动、不需要考虑不同情况的场景,比如Retrofit这些基础框架。
再进一步,我们可以把ViewModel的创建代码也放到容器管理类中:
// Definition of a Factory interface with a function to create objects of a type interface Factory { fun create(): T } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory(private val userRepository: UserRepository) : Factory { override fun create(): LoginViewModel { return LoginViewModel(userRepository) } }
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) val loginViewModelFactory = LoginViewModelFactory(userRepository) } class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance val appContainer = (application as MyApplication).appContainer loginViewModel = appContainer.loginViewModelFactory.create() } }
现在还有一个问题,那就是性能方面的考虑,我们的ViewModel只服务于具体的某个Activity或Fragment,当它销毁的时候我们需要释放这些ViewModel占用的内存空间,所以在不再需要依赖的时候需要手动去释放依赖的内存占用:
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel private lateinit var loginData: LoginUserData private lateinit var appContainer: AppContainer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) appContainer = (application as MyApplication).appContainer // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = LoginContainer(appContainer.userRepository) loginViewModel = appContainer.loginContainer.loginViewModelFactory.create() loginData = appContainer.loginContainer.loginData } override fun onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null super.onDestroy() } }
这样一来,就又多了很多额外的管理代码。
所以,当应用变大时,你会发现编写了大量样板代码(例如工厂),这可能容易出错。还必须自行管理容器的范围和生命周期,优化并舍弃不再需要的容器以释放内存。如果操作不当,可能会导致应用出现微小错误和内存泄露。
有样板代码产生、流程相似就说明一定可以使用“脚本逻辑”来自动话这些代码生成过程,很明显我们需要一个能自动帮我们管理这些依赖的工具,这个工具就是Dagger。
你可以根据上面说的理解出这和反射的区别,这就好像是AOT和JIT的区别,反射是不提前生成代码,等到运行调用时根据类和属性的信息动态加载,这很明显会影响执行速度;而Dagger的原理则是通过编译器在编译期间就生成样板代码,在运行时直接执行无需再动态查找加载,这和我们手动写代码再运行是一样的,只不过Dagger根据我们提供的信息帮我们去“写”好了这部分代码。
-
关于Dagger使用
既然Dagger能帮我们生成上面的代码,那我们需要怎么跟Dagger“沟通”呢。
首先,在构建时,Dagger 会走查代码,并执行以下操作:
- 构建并验证依赖关系图,确保:
- 每个对象的依赖关系都可以得到满足,从而避免出现运行时异常。
- 不存在任何依赖循环,从而避免出现无限循环。
- 生成在运行时用于创建实际对象及其依赖项的类。
生成容器我们需要建立一个接口并给它添加@Component注解:
// @Component makes Dagger create a graph of dependencies @Component interface ApplicationGraph { // The return type of functions inside the component interface is // what can be provided from the container fun repository(): UserRepository }
build时Dagger会自动生成该容器接口的实现类,你可以直接在容器类前面加上“Dagger”前缀后调用它的create方法来获取它的实例:
// Create an instance of the application graph val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create() // Grab an instance of UserRepository from the application graph val userRepository: UserRepository = applicationGraph.repository()
这里我们想创建的依赖是UserRepository,所以我们需要告诉Dagger注入它:
// @Inject lets Dagger know how to create instances of this object class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... }
注意,@Inject注解要放在构造方法前,然后UserRepository的依赖项—UserLocalDataSource和UserRemoteDataSource,同样也需要注入:
// @Inject lets Dagger know how to create instances of these objects class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor() { ... }
如果需要容器内的某个依赖项是单例的话,需要添加作用域注解,这个注解可以是@Singleton,也可以是自定义注解,结合使用,发现使用哪个注解作为作用域修饰并没关系,但是要求修饰容器类的和修饰某个依赖的注解一致,这样在同一个容器实例中获取的依赖项都会是同一实例:
// Scope annotations on a @Component interface informs Dagger that classes annotated // with this annotation (i.e. @Singleton) are bound to the life of the graph and so // the same instance of that type is provided every time the type is requested. @Singleton @Component interface ApplicationGraph { fun repository(): UserRepository } // Scope this class to a component using @Singleton scope (i.e. ApplicationGraph) @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... }
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create() val userRepository: UserRepository = applicationGraph.repository() val userRepository2: UserRepository = applicationGraph.repository() assert(userRepository == userRepository2)
注意,使用作用域注释的模块只能在带有相同作用域注释的组件中使用(容器A中需要依赖项B是单例,则需要A和B都用同一个作用域注解修饰),作用域注解的单例不在于“Singleton”这个名字,可以是其他自定义注解,注解放在类上,只有针对Providers注解时才会放在方法上。
除了用在构造函数上之外,还可以用于属性注入,比如:
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel }
这种方式下由于不是调用容器类的方法来构造的(Activity是系统实例化的,无法进行构造方法注入),而Dagger又不是全域扫描注解解析的,所以我们需要在使用之前调用一下当前实例以告知Dagger添加该实例中相关依赖的注入。
@Component interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is requesting. fun inject(activity: LoginActivity) }
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Make Dagger instantiate @Inject fields in LoginActivity (applicationContext as MyApplication).appComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } } // @Inject tells Dagger how to create instances of LoginViewModel class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
"inject"这个方法名不是限制的,但是必须写在@Component修饰的容器类里,然后调用方法里必须传入想要实例化依赖项的类实例,因为在build的时候Dagger会把@Component修饰的容器类中所有的参数类型、返回类型的类的所有依赖项的创建代码提前写好,比如:
class DemoCC { @Inject lateinit var userRepository: UserRepository } @Component interface Container { fun inject(demo: DemoCC) }
他生成的代码是这样的:
@Override public void inject(DemoCC demo) { injectDemoCC(demo); } private DemoCC injectDemoCC(DemoCC instance) { DemoCC_MembersInjector.injectUserRepository(instance, repository()); return instance; } @Override public UserRepository repository() { return new UserRepository(new LocalDataSource(), new RemoteDataSource()); } //DemoCC_MembersInjector @InjectedFieldSignature("com.mph.review.dependency_inject.dagger.DemoCC.userRepository") public static void injectUserRepository(DemoCC instance, UserRepository userRepository) { instance.userRepository = userRepository; }
所以当你调用inject方法的时候就会调用到这部分提前创建好的代码,自然就给该实例的属性进行了初始化。
- 构建并验证依赖关系图,确保:
-
对于第三方框架等不属于本地项目中的依赖项的实例化
因为Dagger只会扫描本地项目类信息,所以对于第三方框架库中的类的依赖加载怎么完成呢?
Dagger通过@Providers注解来调用生成非本地类的实例代码,@Providers注解必须存在于@Module修饰的类里,否则无效。
class RemoteDataSource @Inject constructor( private val retrofit: Retrofit )
比如上面构造RemoteDataSource需要依赖Retrofit实例,因为Retrofit不属于本地代码,所以Dagger无法自动生成创建代码。
@Module class NetWorkModule { @Provides fun provideRetrofit():Retrofit{ return Retrofit.Builder().build() } }
像这样,Dagger会在build时在需要Retrofit实例的时候去查找@Providers注解修饰的方法看看有没有对应的方法返回类型,如果有就会把Provides修饰的方法放入自动生成的创建逻辑中去,说起来有些绕,直接看生成的代码:
private RemoteDataSource remoteDataSource() { return new RemoteDataSource(NetWorkModule_ProvideRetrofitFactory.provideRetrofit(netWorkModule)); }
可以看到,根据@Module和@Providers注解生成了NetWorkModule_ProvideRetrofitFactory类:
public static Retrofit provideRetrofit(NetWorkModule instance) { return Preconditions.checkNotNullFromProvides(instance.provideRetrofit()); }
可以看到,instance是NetWorkModule,provideRetrofit方法正是@Providers修饰的方法,NetWorkModule是在容器类构造时创建的,所以最终Dagger把我们创建非本地类实例的逻辑嵌入到了Dagger依赖生成流程中。
对于接口实例的依赖项,需要使用@Binds注解完成和实现类的绑定,比如:
interface AnalyticsService { fun analyticsMethods() } // Constructor-injected, because Hilt needs to know how to // provide instances of AnalyticsServiceImpl, too. class AnalyticsServiceImpl @Inject constructor( ... ) : AnalyticsService { ... } @Module @InstallIn(ActivityComponent::class) abstract class AnalyticsModule { @Binds abstract fun bindAnalyticsService( analyticsServiceImpl: AnalyticsServiceImpl ): AnalyticsService }
可以看到,@Binds修饰的方法只需要定义不需要实现,返回参数表示依赖项的接口类型,参数则表示需要注入的该接口的实现类。
当然你也可以使用@Providers实现,只不过需要写方法实现并返回手动创建的对象。
-
Dagger 子组件
对于不同业务,有着不同的生命周期作用域,有的适合全局应用作用域,比如创建成本高、全局都会用到等,而有些是仅针对某个流程的,这些依赖项的生命周期应该是短暂的,比如一个只服务于某个Activity的依赖项,你希望在Activity运行期间多个Fragment可以共用一个实例,但是下次重启Activity的时候你希望它会是一个全新的依赖对象,如果放在应用容器中,那么这个单例会存在于整个应用运行期间(这个原理在于在Application中创建全局容器实例保证该容器实例一直在应用运行期间存在,然后对于和该容器拥有相同作用域的属性,容器第一次创建时会创建一个DoubleCheck实例,这个实例中会维持一个volatile类型的实例来保存这个和该容器具有相同作用域的属性实例,从而达到单例的效果),哪怕我们不再使用它,这显然不太合适。
所以我们需要一个新的作用域容器,但是应用级容器又需要管理子业务容器,所以需要@Subcomponent来区分。
具体流程如下:
-
用Subcomponent注解修饰子组件,并且必须在
LoginComponent
内定义子组件 factory或者builder(这个就是Dagger设计要求的,出于规范设计)。@Subcomponent interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { fun create(): LoginComponent } fun inject(loginActivity: LoginActivity) }
-
创建新的 Dagger 模块(例如
SubcomponentsModule
),并将子组件的类传递给注释的subcomponents
属性。// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent::class) class SubcomponentsModule {}
也是出于代码规范和简洁考虑,Component注解中并没有subcomponent属性,所以必须通过Module作为中介衔接管理。
-
将新模块(即
SubcomponentsModule
)添加到ApplicationComponent
。// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent {}
请注意,
ApplicationComponent
不再需要注入LoginActivity
,因为现在由LoginComponent
负责注入,因此您可以从ApplicationComponent
中移除inject()
方法。 -
ApplicationComponent
的使用者需要知道如何创建LoginComponent
的实例。父组件必须在其接口中添加方法,确保使用者能够根据父组件的实例创建子组件的实例,所以需要提供在接口中创建LoginComponent
实例的 factory.@Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent fun loginComponent(): LoginComponent.Factory }
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Creation of the login graph using the application graph loginComponent = (applicationContext as MyDaggerApplication) .appComponent.loginComponent().create() // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } }
注意看,现在当前Activity依赖的loginComponent只会在当前Activity活动期间保存,如果Activity重建则loginComponent也会是一个新的实例,这就既能保证了loginComponent内部的单例又能保证在Activity销毁后不会存在于内存中。
注意,如果父容器中使用了Singleton或者其他的单例作用域,则子组件中不得使用相同的,否则区别不出属于哪个作用域,所以以父容器使用了Singleton为例,我们子组件中就使用自定义作用域来实现子组件中的单例:
// Definition of a custom scope called ActivityScope @Scope @Retention(value = AnnotationRetention.RUNTIME) annotation class ActivityScope // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
-
-
关于多模块应用
项目中多个Module的情况下,分为静态的app主模块implement所有其他的功能模块和动态的按需下载对应的功能模块两种。
前者容易理解,和父子组件的应用是一样的,关于动态的情况有些特殊。
功能模块的独特优势在于能够自定义应用的不同功能以及何时下载到搭载 Android 5.0(API 级别 21)或更高版本的设备上。例如,为了减小应用的初始下载大小,您可以将某些功能配置为按需下载,或者只能由支持特定功能(比如拍照或增强现实)的设备下载。
这样一来,app模块是不包含这些动态加载的模块的,本身不在它的构建路径中,只能是后面下载的功能块来找app模块,因为app模块是先安装的,那时还没有下载动态模块,所以无法从app模块来寻找动态模块,但是动态模块是后下载的,它知道app模块的构建路径,它可以找到并加载它的代码,这就是Dagger的组件依赖关系机制原理。具体做法就是通过动态模块的Component中添加dependencies属性来依赖app模块的Component。
-
Hilt
Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。
原理都是一样的,列一些不同的特点:
- 使用HildAndroidApp注解生成应用级容器;
- 使用InstallIn注解声明依赖可在什么层面使用,比如ApplicationComponent还是ActivityComponent;
- Hilt 可以为带有
@AndroidEntryPoint
注释的其他 Android 类提供依赖项,Hilt 目前支持以下 Android 类:-
Application
(通过使用@HiltAndroidApp
) Activity
Fragment
View
Service
BroadcastReceiver
-
- 在组件内部,具体的依赖还是需要Inject注解来完成;
这么看好像没啥变化,Hilt背后做的其实很多。我们只需要维护组件内部的依赖项和其创建逻辑即可,生成的依赖会自动归于当前的组件容器中管理,也就是说会和组件的生命周期绑定,所以我们不需要担心内存泄露等问题。
在使用Dagger时,至少我们需要创建一个容器类对象,然后调用它的相关方法获取需要的依赖项,但是在Hilt中我们自始至终没有看到过触发依赖注入的入口逻辑,这是因为Hilt在编译期会通过插件自动生成依赖项所在的目标组件类的Hilt类,然后在运行时会修改底层字节码,会将目标类继承自动生成的类,以Activity为例,会在自动生成类的onCreate方法中调用完自动生成的类的inject方法后再调用super的onCreate方法(即目标类、你自己编写的组件类的onCreate方法)。
比如:
@AndroidEntryPoint class TestHomeActivity : BaseActivity
() { @Inject lateinit var myService:MyService } 在编译后会生成:
public abstract class Hilt_TestHomeActivity
extends BaseActivity implements GeneratedComponentManager inject方法最终会回调到ActivityCImpl(不同组件的是不同的类)的injectTestHomeActivity方法中:
@Override public void injectTestHomeActivity(TestHomeActivity testHomeActivity) { injectTestHomeActivity2(testHomeActivity); } private TestHomeActivity injectTestHomeActivity2(TestHomeActivity instance) { TestHomeActivity_MembersInjector.injectMyService(instance, new MyServiceImpl()); return instance; }
@InjectedFieldSignature("com.mph.review.TestHomeActivity.myService") public static void injectMyService(TestHomeActivity instance, MyService myService) { instance.myService = myService; }
从这个过程可以看到Hilt插件相较于Dagger来说,它的主要工作在于针对Android的组件做了很多自动化的工作。