背景
DI(Dependency Injection),即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
最近业务同学需要接入谷歌推的Hilt
框架。因为哔哩哔哩的业务上很容易出现业务层面的交叉,而因为项目完成了大量的组件化拆分。由于不希望业务之间产生相互引用,所有在技术评估完成之后我们决定由我们部门来对Hilt
进行接入。
方案介绍
接入Hilt
摘自官方文档 使用 Hilt 实现依赖项注入
首先先声明下dagger.hilt.android.plugin
相关的plugin。
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35.1'
}
}
其次由于hilt
一大部分相关的都是基于kapt
的代码生成逻辑,所以我们要在使用到hilt
的模块的build.gradle
中都定义如下相关的。
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.35.1"
kapt "com.google.dagger:hilt-android-compiler:2.35.1"
}
我们需要给我们的Application
增加一个注解。
@HiltAndroidApp
class ExampleApplication : Application() { }
根据官方接入文档哦,我们组只要把Application
只要打上@HiltAndroidApp
的注解,就可以完成这部分接入能力了。
Hilt在组件化
但是但是官方有个声明是这样的。
Hilt 代码生成操作需要访问使用 Hilt 的所有 Gradle 模块。编译 Application 类的 Gradle 模块需要在其传递依赖项中包含所有 Hilt 模块和通过构造函数注入的类。
从这部分说明上来看,这个注解最好是能放在com.android.application
模块中, 这样就能保证依赖到所有的子模块中去了。
但是实际我们在使用过程中,由于com.android.application
模块还是有一些代码量的,而由于kapt
代码生成机制,需要先将kotlin代码转化成java代码,之后才能生成ast语法树。
根据ci
上的实验结果,在com.android.application
模块下kapt
耗时在30s左右,而整体编译时间大概为3分钟左右。这种耗时我个人觉得还是属于不能接受的。
所以我们调整了下这部分代码。这次建立了一个壳module
,这边只有HiltApplication
的注解相关。然后将这个module
依赖了所有业务仓库,按照编译逻辑来说,基于gradle task
的depend
逻辑,他会在application模块编译之前,所有业务模块编译之后,这样能保证hilt生成的代码逻辑正常。
同时由于是一个空工程,我们把空工程定义为bundle-kapt
,所以整体来说对于编译速度影响会变到最小。让各位大佬看下我们后续的优化结果。
上述是我最后截图出来的结果,我们将一个耗时大概30s的任务优化到3s,其实效果上来说已经非常明显,达到我们想要的预期了已经。
当然如果后续hilt
支持了ksp之后,这部分速度应该可以更快,毕竟我么可以直接抛弃java语法树了吗。
出现了点小问题
这两天业务方实际在使用过程中,突然反馈说貌似我们接入的Hilt
貌似不行啊,进入到页面直接崩溃了。
有一说一,一脸懵逼。先看看异常吧。初一开始我以为是kapt没有生成好或者别的什么原因导致的。
代码语言:javascript复制com.xxxxxx.xxxxxx}: java.lang.ClassCastException: xxxxx.DaggerHiltApplication_HiltComponents_SingletonC$ActivityRetainedCImpl$ActivityCImpl cannot be cast to xxxxx_GeneratedInjector
只能一步步调试咯,我打开项目开启编译项目,之后进入蛮长的等待过程中。10分钟过去了终于好了。
由于Hilt
使用了kapt,所以很自然的打开了build/generated/source/kapt
文件路径,之后我看了下DaggerHiltApplication_HiltComponents_SingletonC
这个类的生成。
ActivityRetainedCImpl
从这里我大概猜测出了一小部分Hilt
原理,通过收集不同子Module的抽象接口,然后把这部分能力聚合在HiltApplication
中,举个例子Hilt_BangumiDetailActivityV3
这个就是一个子业务内的DI
注入的一个类的实现。
突然这个时候我想到了一件事哦,也就是说我们的bundle-kapt
模块,其实它的实际编译产物会根据接入业务的不同而产生实际的变更。也就是说虽然这个模块的代码没有发生变更,但是由于子业务增加了注解和代码变更,导致了这个模块的kapt
还是需要重新执行,这样才能保证输出的产物是变化的。
然而我们的项目之前由于工程结构太庞大了,可能有30-40个Module
,所有我们将一部分没有变更的代码产物化,也就是基于commit变成了一个个aar
或者jar
发布到了远端。之后根据commit来重新定向到产物。
而bundle-kapt
这个模块也很不幸,被当做了一个静态模块,变成了一个远端的产物,之后即时业务添加了再多的注入相关的,因为bundle-kapt
没有参与编译,所以注入的能力就出错了。
这个时候因为是周五要下班了,所以不能参与内卷了,只能收拾下工位回家,至于解决方案肯定是要礼拜一了啊,不要让资本家尝到甜头啊。
总结
我们团队算是一个比较好玩的团队了,团队有两位巨佬,一位大佬专门负责编译相关的,基本gradle方面都懂了,而且玩的也很花里胡哨的。另外一位大佬lancet作者,gradle方面懂得多就不提了,上一篇文章最后的问题也是这位大佬帮忙处理的。
而其他的组员其实人也都非常的不错,而且我们团队也做了很多东西。比如之前介绍的网络优化,资源混淆插件优化,图片中间件,动态化,自研的路由框架,还有blkv自研的ky存储框架等等。
同样的我们的kt版本还有agp版本都算是业内比较靠前的了。一方面是大佬能cover住,另一方面我们也会在这部分投入人力,我们也已经开始用viewbinding的代理啦。