最佳实践 | 基于注解处理器实现视图绑定

上篇文章我们详细介绍了注解处理器的相关功能,不过即使看完也完全不知道注解处理器到底要怎么写。

注解处理器本身是个实践性十分强的概念,本篇文章将会从0到1编写一个Kotlin注解处理器(KAPT)实现Android中的视图绑定功能,在这之前,先来介绍一些能够协助我们快速开发的开源库。

注:源码链接放置在文末。

开源库推荐

先推荐几个开源库:AutoServiceJavaPoetKotlinPoet

AutoService

在上篇文章中讲到自定义的注解处理器要正常运作的话,需要对其进行注册,具体是需要在resources/META-INF/services下新建javax.annotation.processing.Processor文件并将注解处理器的全限定名写入文件中。

注册注解处理器

这是Java中非常常见的ServiceLoader使用方式,需要我们创建的文件称为ServiceLoader的配置文件(configuration),这种方式在很多框架的源码中都有使用。

不过这种方式略微繁琐,考虑一个问题,既然是生成文件的话,我们的APT刚好就可以实现,那么是否可以由注解处理器来自动生成这个文件呢?

答案是可以的,而且已经有很多人这样在做了。谷歌就写了一个开源库叫auto,其中的AutoService就可以帮助我们生成这个文件,源码非常精简,有兴趣可以看看。

有了AutoService之后,只需要一个注解就可以解决注解处理器的注册问题了:

1
2
3
4
5
6
7
8
9
package com.sukaidev.compiler

import com.google.auto.service.AutoService
import javax.annotation.processing.Processor

@AutoService(Processor::class)
class ViewBindingProcessor : AbstractProcessor() {
// …
}

这里有一点需要注意,就是在引入AutoService依赖时,如果我们编写的是KAPT的话,需要以同时以implementation和kapt的方式引入AutoService:

1
2
implementation 'com.google.auto.service:auto-service:1.0-rc6'
kapt 'com.google.auto.service:auto-service:1.0-rc6'

JavaPoet

APT中我们可以使用Filer来创建生成Java源文件:

1
env.filer.createClassFile("...").openWriter().write("xx.java")

然而这种方式我们只能通过字符串硬编码的方式来写入代码,对于程序员来说非常不友好,如果代码比较多很容易出现错误而找不到问题的情况。

因此Square开发了JavaPoet,JavaPoet提供了一套用于生成.java文件的API,其使用方法可以参照其Github页面的Readme,使用它我们可以按照程序员的方式去编写源代码并输出到文件中。

比较有意思的是,AutoService正是使用了JavaPoet来生成System Loader的配置文件。

KotlinPoet

与JavaPoet类似,KotlinPoet用于生成.kt源码文件。

详细的使用方法参照官方文档:https://square.github.io/kotlinpoet/。

项目结构

首先来搭建整个Demo的项目结构,一般情况下我们会创建两个Java/Kotlin Module,命名为annotations和processor,其中annotations存放所有的注解,processor则为注解处理器模块。

另外添加了一个名为runtime的Android Module,存放框架的所有对外开放类,整个源码目录如下。

Demo基础结构

下面分别来介绍这些模块。

annotations

annotations模块中定义了我们所需要处理的注解,此Demo中我们需要定义两个注解:

  • BindClass:使用此注解标注的类我们才会去收集需要注入的View并声称对应的管理类。

    1
    2
    3
    @Target(AnnotationTarget.CLASS)
    @Retention(AnnotationRetention.BINARY)
    annotation class BindClass
  • BindView:使用此注解标注的View才会进行依赖注入。

    1
    2
    3
    @Target(AnnotationTarget.FIELD)
    @Retention(AnnotationRetention.BINARY)
    annotation class BindView(val redId: Int)

processor

processor属于核心模块,注解处理器在这里定义。模块目录结构如下所示:

processor模块

主要的逻辑在VIewBindingProcessor中,其它类都用于协助注解处理器。

VIewBindingProcessor要做的事情很简单,分为三步:

  1. 收集所有标注了@BindClass的Activity/Fragment:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    val activityType = elements.getTypeElement(ACTIVITY_JVM_CLASS_NAME).asType()
    val fragmentType = elements.getTypeElement(FRAGMENT_JVM_CLASS_NAME).asType()
    roundEnv.getElementsAnnotatedWith(BindClass::class.java)
    .filter { it.kind.isClass }
    .forEach {
    try {
    when {
    it.asType().isSubType(activityType, types) -> {
    bindingActivities[it] = BindingActivity(it as TypeElement)
    }
    it.asType().isSubType(fragmentType, types) -> {
    bindingFragments[it] = BindingFragment(it as TypeElement)
    }
    else -> {
    Logger.error(it, "Unsupported typeElement:${it.simpleName}")
    }
    }
    } catch (e: Exception) {
    Logger.logParsingError(it, BindClass::class.java, e)
    }
    }
  2. 收集所有标注了@BindView的View:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    val viewType = elements.getTypeElement(VIEW_JVM_CLASS_NAME).asType()
    roundEnv.getElementsAnnotatedWith(BindView::class.java)
    .filter { it.kind.isField }
    .forEach {
    try {
    // 必须是View的子类
    if (it.isSubType(viewType, types)) {
    if (it.enclosingElement.isSubType(activityType, types)) {
    bindingActivities[it.enclosingElement]?.bindingViews?.add(BindingView(it as Symbol.VarSymbol))
    ?: Logger.error(it, "BindingView $it annotated as @BindView while ${it.enclosingElement} not annotated.")
    }
    if (it.enclosingElement.isSubType(fragmentType, types)) {
    bindingFragments[it.enclosingElement]?.bindingViews?.add(BindingView(it as Symbol.VarSymbol))
    ?: Logger.error(it, "BindingView $it annotated as @BindView while ${it.enclosingElement} not annotated.")
    }
    }
    } catch (e: Exception) {
    Logger.logParsingError(it, BindView::class.java, e)
    }
    }
  3. 为Activity/Fragment生成对应ViewBinding类:

    1
    2
    3
    4
    5
    6
    bindingActivities.values.forEach {
    it.builder.build()
    }
    bindingFragments.values.forEach {
    it.builder.build()
    }

BindingActivityBuilder和BindingFragmentBuilder分别用于构造生成类的源码以及输出生成类,主要就是使用KotlinPoet构建整类的变量和方法,具体的代码参考文章底部的源码路径。

生成类演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final class MainActivity_ViewBinding(
private var activity: MainActivity?
) : UnBinder {
private var decorView: View? = activity!!.window.decorView

private var button: AppCompatButton? = Utils.findRequiredViewAsType(decorView!!, 2131230808)

public override fun bind(): UnBinder? {
if (activity is MainActivity) {
activity?.button = button!!
return this
}
return null
}

public override fun unBind(): Unit {
this.activity = null
this.decorView = null
this.button = null
}
}

在编写注解处理器时,最好是先把要生成的代码写出来,然后对照这个代码使用KotlinPoet进行代码生成。

runtime

runtime模块主要是提供一个门面类:ViewBinding。此类负责注册Activity和Fragment的LifecycleCallback,方便在Activity和Fragment销毁时自动进行视图解绑,防止出现内存泄露问题。

ViewBinding类的代码如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class ViewBinding {

private val BINDINGS = LinkedHashMap<String, UnBinder>()

private val activityLifecycleCallback = BindingActivityLifecycleCallback()
private val fragmentLifecycleCallback = BindingFragmentLifecycleCallback()

fun init(app: Application) {
app.registerActivityLifecycleCallbacks(activityLifecycleCallback)
}

/**
* 进行视图绑定
* 需要在Activity setContentView之后
* 以及Fragment onCreatedView之后调用
*/
fun bind(source: Any) {
if (source !is Activity && source !is Fragment) return

val sourceClass = source::class.java
val bindingClassName = source.javaClass.name + BINDING_CLASS_POSTFIX
var unBinder = BINDINGS[bindingClassName]
try {
if (unBinder == null) {
val bindingClass = Class.forName(sourceClass.name)?.classLoader?.loadClass(bindingClassName) ?: return
unBinder = bindingClass.getConstructor(sourceClass).newInstance(source) as UnBinder
BINDINGS[bindingClassName] = unBinder
}
unBinder.bind()
} catch (e: ClassNotFoundException) {
e.printStackTrace()
return
}

if (source is Fragment) {
source.parentFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallback, false)
}
}


/**
* 解绑
* 由LifecycleCallback自动调用
*/
fun unBind(source: Any) {
if (source !is Activity && source !is Fragment) return

val bindingClassName = source.javaClass.name + BINDING_CLASS_POSTFIX

val unBinder = BINDINGS[bindingClassName] ?: return
unBinder.unBind()

BINDINGS.remove(bindingClassName)

if (source is Fragment) {
source.parentFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallback)
}
}

companion object {

const val BINDING_CLASS_POSTFIX = "_ViewBinding"

val INSTANCE: ViewBinding by lazy { ViewBinding() }
}
}

其中init方法需要在Application创建时调用,因此这里使用了ContentProvider来初始化ViewBinding类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Created by sukaidev on 2021/07/03.
*
* 用于初始化Runtime.
*
* @author sukaidev
*/
class RuntimeProvider : ContentProvider() {

override fun onCreate(): Boolean {
ViewBinding.INSTANCE.init(context.applicationContext as Application)
return true
}

override fun query(uri: Uri?, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?) = null

override fun getType(uri: Uri?) = null

override fun insert(uri: Uri?, values: ContentValues?) = null

override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?) = 0

override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
}

源码地址

Github地址,需科学:源码地址