写在最前面

上一篇我们分析了,反射来实现 ButterKnife。那么这一篇,我们就来正式分析 ButterKnife 的相关库,先从 Gradle Plugin 这个库开始。

项目结构

.
├── build.gradle
├── butterknife-gradle-plugin.iml
├── gradle.properties
└── src
    ├── main
    │   ├── java
    │   │   └── butterknife
    │   │       └── plugin
    │   │           ├── ButterKnifePlugin.kt
    │   │           ├── FinalRClassBuilder.kt
    │   │           ├── R2Generator.kt
    │   │           └── ResourceSymbolListReader.kt
    │   └── resources
    │       └── META-INF
    │           └── gradle-plugins
    │               └── com.jakewharton.butterknife.properties
    └── test

整个项目的结构非常清晰,4个文件。这里就直接说明了,这个库的作用就是用于生成library库的R2文件

好的 我们简单来分析下,对于 library 是怎样的一个操作。

源码分析

ButterKnifePlugin

.properties 文件定义了相应的 Plugin 类ButterKnifePlugin

class ButterKnifePlugin : Plugin<Project> {
  override fun apply(project: Project) {
    project.plugins.all {
      when (it) {
        is FeaturePlugin -> {
          project.extensions[FeatureExtension::class].run {
            configureR2Generation(project, featureVariants)
            configureR2Generation(project, libraryVariants)
          }
        }
        is LibraryPlugin -> {
          project.extensions[LibraryExtension::class].run {
            configureR2Generation(project, libraryVariants)
          }
        }
        is AppPlugin -> {
          project.extensions[AppExtension::class].run {
            configureR2Generation(project, applicationVariants)
          }
        }
      }
    }
  }


  private fun getPackageName(variant : BaseVariant) : String {...}

  private fun configureR2Generation(project: Project, variants: DomainObjectSet<out BaseVariant>) {...}

  private operator fun <T : Any> ExtensionContainer.get(type: KClass<T>): T {
    return getByType(type.java)
  }
}

文件结构非常简单,拿到 project 之后,查找每个 plugin,当是 com.android.library 时,查找里面的 AndroidExtension,这里用到了 kotlin 操作符重载。

找到之后,则开始配置 R2 文件必须要的内容。

configureR2Generation

 private fun configureR2Generation(project: Project, variants: DomainObjectSet<out BaseVariant>) {
    variants.all { variant ->
      val outputDir = project.buildDir.resolve(
          "generated/source/r2/${variant.dirName}")

      val rPackage = getPackageName(variant)
      val once = AtomicBoolean()
      variant.outputs.all { output ->
        // Though there might be multiple outputs, their R files are all the same. Thus, we only
        // need to configure the task once with the R.java input and action.
        if (once.compareAndSet(false, true)) {
          val processResources = output.processResourcesProvider.get() // TODO lazy

          // TODO: switch to better API once exists in AGP (https://issuetracker.google.com/118668005)
          val rFile =
              project.files(
                  when (processResources) {
                    is GenerateLibraryRFileTask -> processResources.textSymbolOutputFile
                    is LinkApplicationAndroidResourcesTask -> processResources.textSymbolOutputFile
                    else -> throw RuntimeException(
                        "Minimum supported Android Gradle Plugin is 3.3.0")
                  })
                  .builtBy(processResources)
          val generate = project.tasks.create("generate${variant.name.capitalize()}R2", R2Generator::class.java) {
            it.outputDir = outputDir
            it.rFile = rFile
            it.packageName = rPackage
            it.className = "R2"
          }
          variant.registerJavaGeneratingTask(generate, outputDir)
        }
      }
    }
  }

获取,每个 extension 的 variants,这里面通常就是指的是 release 和 debug 两种,然后对这两种情况分别操作。 获取文件输出目录,获取包名。然后获取ProcessAndroidResources这个task,获取其生成的 R.txt文件,并将其封装到 FileCollection 中。最后创建一个新的 task: generateReleaseR2或者debug的。

并将这个 task,注册到 JavaGeneratingTask 中,根据说明所有注册到这个task中的task,都会使 generateReleaseSources 来依赖他,所以我们不用显示的来进行依赖了。

至此,这个R2生成的任务已经创建成功同时注册到了任务栈中了。

R2Generator

@CacheableTask
open class R2Generator : DefaultTask() {
  @get:OutputDirectory
  var outputDir: File? = null

  @get:InputFiles
  @get:PathSensitive(PathSensitivity.NONE)
  var rFile: FileCollection? = null

  @get:Input
  var packageName: String? = null

  @get:Input
  var className: String? = null

  @Suppress("unused") // Invoked by Gradle.
  @TaskAction
  fun brewJava() {
    brewJava(rFile!!.singleFile, outputDir!!, packageName!!, className!!)
  }
}

fun brewJava(
  rFile: File,
  outputDir: File,
  packageName: String,
  className: String
) {
  FinalRClassBuilder(packageName, className)
      .also { ResourceSymbolListReader(it).readSymbolTable(rFile) }
      .build()
      .writeTo(outputDir)
}

这里创建了一个task,在调用的时候,执行 Action 中的内容。其核心就是生成一个java文件,到输出目录中。

ResourceSymbolListReader

internal class ResourceSymbolListReader(private val builder: FinalRClassBuilder) {
  private var idValue = 0

  fun readSymbolTable(symbolTable: File) {
    symbolTable.forEachLine { processLine(it) }
  }

  private fun processLine(line: String) {
    val values = line.split(' ')
    if (values.size < 4) {
      return
    }
    val javaType = values[0]
    if (javaType != "int") {
      return
    }
    val symbolType = values[1]
    if (symbolType !in SUPPORTED_TYPES) {
      return
    }
    val name = values[2]
    val value = CodeBlock.of("\$L", ++idValue)
    builder.addResourceField(symbolType, name, value)
  }
}

因为读取的 R.txt 文件是如下结构:

int attr alpha 0x7f040001
int attr font 0x7f040002

所以要一行行的读取,并且加到 FinalRClassBuilder 中,最后来生成相应的文件。

FinalRClassBuilder

class FinalRClassBuilder(
    private val packageName: String, 
    private val className: String) {

  private var resourceTypes = mutableMapOf<String, TypeSpec.Builder>()

  fun build(): JavaFile {
    val result = TypeSpec.classBuilder(className)
        .addModifiers(PUBLIC, FINAL)
    for (type in SUPPORTED_TYPES) {
      resourceTypes.get(type)?.let {
        result.addType(it.build())
      }
    }
    return JavaFile.builder(packageName, result.build())
        .addFileComment("Generated code from Butter Knife gradle plugin. Do not modify!")
        .build()
  }

  fun addResourceField(type: String, fieldName: String, fieldInitializer: CodeBlock) {
    if (type !in SUPPORTED_TYPES) {
      return
    }
    val fieldSpecBuilder = FieldSpec.builder(Int::class.javaPrimitiveType, fieldName)
        .addModifiers(PUBLIC, STATIC, FINAL)
        .initializer(fieldInitializer)

    fieldSpecBuilder.addAnnotation(getSupportAnnotationClass(type))

    val resourceType =
        resourceTypes.getOrPut(type) {
          TypeSpec.classBuilder(type).addModifiers(PUBLIC, STATIC, FINAL)
        }
    resourceType.addField(fieldSpecBuilder.build())
  }

  private fun getSupportAnnotationClass(type: String): ClassName {
    return ClassName.get(ANNOTATION_PACKAGE, type.capitalize(Locale.US) + "Res")
  }

  // TODO https://youtrack.jetbrains.com/issue/KT-28933
  private fun String.capitalize(locale: Locale) = substring(0, 1).toUpperCase(locale) + substring(1)
}

由于生成的 R2.java 文件结构就是:

每次来了一个新属性,使用fieldSpecBuilder,来生成一个新field,并增加一个类型注解, 从 resourceTypes 尝试获取一个对象(有就获取,没有就创建),这个对象的类名就是 field 的 type,将新的 field 放进去。

最后在build的时候,返回 resourceTypes 的每一个成员,放到 result 中,最后生成相应的 java 文件。

小结

至此,r2 文件已经完成。这里我们有两个疑问

  • r2 和 r 文件按理说其结构 和k,v是一模一样的,为啥还要单独重新生成一个文件。
  • 这里面每一个每一个成员变量都用注解进行修饰,为什么要这么干。

这些问题,我们在接下来的文章中进行解答。

results matching ""

    No results matching ""