目录

写一个依赖注入

写一个依赖注入!

会学到:

  1. 几种依赖注入的方式
  2. 如何自己做一个 Gradle Plugin
  3. 如何操作 Java 字节码

为什么要依赖注入

来看下面这个例子:

https://cdn.jsdelivr.net/gh/zsqw123/cdn@master/picCDN/202108301603013.png

我们需要在 class A 中的 method a 中调用 class B 中的 method b:

1
2
3
4
5
6
class A{
  	fun a(){
      val classB = B()
      classB.b()
    }
}

这样的调用非常自然,但是当有一天我们需要让所有调用 b 方法的地方全部改成 c 方法,那么意味着所有用到 b 方法的地方都需要替换成 c,如果方法套的很深,或者很多地方都用到了,经常就会出现改了这个类,导致一整条链路上的调用都需要修改,项目耦合非常严重。

原因实际上是 class A 执行了 class B 的初始化操作,但 class A 事实上不需要关注 class B 是怎么产生的,它只需要去得到指定的类去调用它想要的方法,这时候我们可以让外部来初始化这个类,像这样:

https://cdn.jsdelivr.net/gh/zsqw123/cdn@master/picCDN/202108301634622.png

我们希望做到的是 A 定义它需要什么方法,B 声明它可以实现什么方法,然后这个工具负责去实现这两个类之间的交互,这个就是依赖注入,这个注入意思就是

  1. A 不需要知道如何获取 B 实例,只需要告诉外部它需要什么,工具会为他找到适合它的实例。
  2. B 不需要知道他在哪里被用到了,只需要告诉外部它可以提供什么。

我们接下来就看看如何实现这个工具

现有方案

静态保存所有实现类

1
2
3
4
5
6
7
8
class Tool{
  val map = hashMapOf<Class<*>,List<*>>()
	init{
    map[I0::class] = listOf(Impl00(),Impl01(),Impl02());
    map[I1::class] = listOf(Impl10(),Impl11());
    ...
  }
}

我们要做的就是在尽可能早的时候去初始化这个类,之后我们便可以从这个类中获取我们想要的内容,当有新的实现的时候,就把所有的实现都加到这里面,这样的方案看似简单清晰,但是所有类都没有懒加载,而且这个类还需要尽早实例化,因此会导致启动速度严重变慢。

反射

前面提到了静态保存这些类会导致没有懒加载,解决的办法其实也是很简单的,只要保存 class name,然后反射构造就好了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Tool{
  val map = hashMapOf<Class<*>,List<String>>()
	init{
    map[I0::class] = listOf("Impl00","Impl01","Impl02");
    map[I1::class] = listOf("Impl10","Impl11");
    ...
  }
  fun getAllInstance(clazz:Class<*>):List<*>{
    return map[clazz].map{ Class.forName(it).getDeclaredInstance().newInstance() }
  }
}

这算是实现了我们想要的目标了吧?当然还可以做一些优化,比如 class name 不使用硬编码,获取全部实例用 Sequence 实现懒加载… 但是!他有这些缺点:

  1. 牺牲了 IDE 静态检查的特性,获取到的是 Any 类型,需要强转,即便没有实现这个接口,你依旧可以把这个实现类放到 map 中
    有些人可能会说:不能实现我干嘛要放过去?其实放过去你可能会记得,但是当你觉得这个方法没有地方用到,然后调试了一下程序也顺利编译,于是删除无用的接口的时候,你可能不会知道这个方法会在不久的将来埋下隐患:一旦在某个地方想查找这个接口的实现了,然后用到了这个实现类,然而它并没有实现,直接爆出 NoSuchMethod
  2. 用了反射,性能会差。
  3. 需要手动注册和删除,这是不可靠的,人还是很容易忘事的。

apt

注解处理生成代码,我们可以通过 javapoet 或者 kotlinpoet 去处理注解和生成代码,但是这个过程是发生在编译前的,对于编译隔离的环境下,处理起来就非常棘手了

目前现有的比较完善的库有 dragger,以及 Jetpack 组件中的 hilt,这里如果感兴趣可以去参照 Google 官方的文档:使用 Hilt 实现依赖项注入,这个库是基于 dragger 来做的,因为 dragger 用起来是比较复杂的,hilt 对它进行了针对 Android 的场景化处理。

当然如果针对动态模块,其实 hilt 也有解决方案,但是看起来并不优雅:在多模块应用中使用 Hilt,使用会稍微麻烦。

对于 Kotlin 的注解处理,我们一般使用 kapt,但是 kapt 事实上是非常耗时的,这里我介绍一下 kapt 的工作流程:

https://cdn.jsdelivr.net/gh/zsqw123/cdn@master/picCDN/202108310823623.png

生成JavaStub的时候,我们其实需要担心一个问题,kotlin有一些它专属的关键字啊,比如说inline fun,比如说data class 还有什么 reified… 这些东西 Java 其实都是没有的,那么 kotlin 如何去处理这些类信息呢?如果你反编译过 kotlin 的类的字节码的时候,你会发现它有个metadata 注解,这就是这些信息存放的地方,如果要解析这些信息,其实在翻阅 kotlin 的源码之后发现,我们可以使用 kotlinx-metadata.

在 kapt 解析完这些类信息之后,才会进行真正的代码生成,解析这一步其实是非常耗时的,不信我们可以去看看我们的线上项目,生成JavaStub消耗了非常长的一段时间

当然,官方也考虑到了这些,做了一个东西叫做 KSP:google/ksp: Kotlin Symbol Processing API (github.com),目前也有一些库在慢慢转变为使用 ksp,但目前尚处于 beta 阶段,还没有正式发布,ksp 的底层是 kcp(Kotlin Complier Plugin),我们也可以翻 Kotlin 源代码就可以发现什么东西用到了 KCP

总结一下,apt 有这些缺点:

  1. 处理编译隔离比较麻烦
  2. 生成代码并参与编译,对编译速度有较大影响(尤其是 Kotlin kapt,会比 Java APT 要慢许多)

Transform

同样是生成代码。不过这个生成的是字节码,字节码生成的之后不需要额外处理源文件,性能会比较高, 首先我们需要首先了解 Android 的构建流程

https://cdn.jsdelivr.net/gh/zsqw123/cdn@master/picCDN/202108301808539.png

其中 *.java, *kt 这些文件会编译成 *.class 文件,然后 Transform 就是在生成 class/jar 之后,编译为 dex 文件之前,这个过程是由 Gradle 来接管的,因此我们需要来处理 Gradle 的构建流程:即自定义 Gradle Plugin

写一个 Gradle Plugin

写一个 HelloWorld

  1. 创建一个 module,module 名字无所谓,插件的话,要确定一个名字,比如我创建的这个叫做 cat-inject

  2. 在 resources/META-INF/gradle-plugins 中创建配置文件一个叫 cat-inject.properties

  3. 里面写入你的实现类:

    1
    
    implementation-class=com.zsqw123.inject.plugin.InjectPlugin
    

    ``

  4. 实现 Plugin:

    需要依赖 Plugin<Project>, 之后 apply 方法会在依赖导入成功以及 gradle build 的时候执行,然后 registerTransform 即可使指定的类型(AppExtension,LibraryExtension)使用此 Transform

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    class InjectPlugin : Plugin<Project> {
        override fun apply(target: Project) {
            val androidAppExtension = target.extensions.findByType(AppExtension::class.java)
            val androidLibExtension = target.extensions.findByType(LibraryExtension::class.java)
            if (androidAppExtension != null || androidLibExtension != null) {
                val injectTransform = InjectTransform()
                androidAppExtension?.registerTransform(injectTransform)
                androidLibExtension?.registerTransform(injectTransform)
            }
            println("CatInject Plugin Loaded!")
        }
    }
    

    ``

  5. Transform 我单独抽了一个 BaseTransform,这样暴露出真正的 transform 逻辑给子类

     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
    
    override fun getName(): String = TRANSFORM_NAME
    // 这里可以选择输入的类型:Class,Resource,Dex
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS
    // 作用范围:当前 Project,子模块,第三方依赖
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT
    // 是否支持增量处理
    override fun isIncremental(): Boolean = true
    override fun transform(transformInvocation: TransformInvocation) {
        val outputProvider = transformInvocation.outputProvider
        if (!isIncremental) {
            outputProvider.deleteAll()
        }
        transformInvocation.inputs.forEach { input ->
            input.jarInputs.forEach { jarInput ->
                // ...
                processJar(file)
            }
            input.directoryInputs.forEach { dirInput ->
                // ...
                processDirectory(file)
            }
        }
        onTransformed()
    }
    // 处理 jar 包
    protected open fun processJar(outputJarFile: File) = Unit
    // 处理源码文件
    protected open fun processDirectory(outputDirFile: File) = Unit
    // 处理完之后进行的操作
    protected open fun onTransformed() = Unit
    

    ``

  6. 处理 jar 包和源码文件会扫描两遍,第一遍会扫描所有被 CatInject 注解的接口,第二遍会扫描所有实现了需要被注入的接口的类,最后将两次结果进行 map,并进行写入字节码