ButterKnife在之前的Android开发中还是比较热门的工具,帮助Android开发者减少代码编写,而且看起来更加的舒适,于是简单实现一下ButterKnife,相信把下面的代码都搞懂,看ButterKnife的难度就小很多。

今天实现的是编译时注解,其实运行时注解也一样能实现ButterKnife的效果,但是相对于编译时注解,运行时注解会更耗性能一些,主要是由于运行时注解大量使用反射。

一、创建java library(lib_annotations)

我这里创建3个annotation放在3个文件中

//绑定layout
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class BindLayout(val value: Int = -1)

//绑定view
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class BindView (val value:Int = -1)

//点击注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
annotation class OnClick (vararg val values:Int)

Retention SOURCE:注解只存在于源代码,编译后不可见,BINARY:注解编译后可见,运行时不可见,RUNTIME:编译后可见,运行时可见

二、创建java library(lib_processor)

@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class BindProcessor : AbstractProcessor() {
    companion object {
        private const val PICK_END = "_BindTest"
    }

    private lateinit var mLogger: Logger
    //存储类文件数据
    private val mInjectMaps = hashMapOf<String, InjectInfo>()

    //必须实现方法
    override fun process(
        annotations: MutableSet<out TypeElement>?,
        roundEnv: RoundEnvironment
    ): Boolean {
        //里面就要生成我们需要的文件

        roundEnv.getElementsAnnotatedWith(BindLayout::class.java).forEach {
            bindLayout(it)
        }

        roundEnv.getElementsAnnotatedWith(BindView::class.java).forEach {
            bindView(it)
        }

        roundEnv.getElementsAnnotatedWith(OnClick::class.java).forEach {
            bindClickListener(it)
        }

        mInjectMaps.forEach { (name, info) ->
           //这里生成文件
           val file= FileSpec.builder(info.packageName, info.className.simpleName + PICK_END)
                .addType(
                    TypeSpec.classBuilder(info.className.simpleName + PICK_END)
                        .primaryConstructor(info.generateConstructor()).build()
                ).build()

            file.writeFile()
        }

        return true
    }

    private fun FileSpec.writeFile() {
        //文件编译后位置
        val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]
        val outputFile = File(kaptKotlinGeneratedDir).apply {
            mkdirs()
        }
        writeTo(outputFile.toPath())
    }

    private fun bindLayout(element: Element) {
        //BindLayout注解的是Class,本身就是TypeElement
        val typeElement = element as TypeElement
        //一个类一个injectInfo
        val className = typeElement.qualifiedName.toString()
        var injectInfo = mInjectMaps[className]
        if (injectInfo == null) {
            injectInfo = InjectInfo(typeElement)
        }

        typeElement.getAnnotation(BindLayout::class.java).run {
            injectInfo.layoutId = value
        }

        mInjectMaps[className] = injectInfo
    }

    private fun bindView(element: Element) {
        //BindView注解的是变量,element就是VariableElement
        val variableElement = element as VariableElement
        val typeElement = element.enclosingElement as TypeElement
        //一个类一个injectInfo
        val className = typeElement.qualifiedName.toString()
        var injectInfo = mInjectMaps[className]
        if (injectInfo == null) {
            injectInfo = InjectInfo(typeElement)
        }

        variableElement.getAnnotation(BindView::class.java).run {
            injectInfo.viewMap[value] = variableElement
        }

        mInjectMaps[className] = injectInfo
    }

    private fun bindClickListener(element: Element) {
        //OnClick注解的是方法,element就是VariableElement
        val variableElement = element as ExecutableElement
        val typeElement = element.enclosingElement as TypeElement
        //一个类一个injectInfo
        val className = typeElement.qualifiedName.toString()
        var injectInfo = mInjectMaps[className]
        if (injectInfo == null) {
            injectInfo = InjectInfo(typeElement)
        }

        variableElement.getAnnotation(OnClick::class.java).run {
            values.forEach {
                injectInfo.clickListenerMap[it] = variableElement
            }
        }

        mInjectMaps[className] = injectInfo
    }

    //把注解类都添加进行,这个方法一看方法名就应该知道干啥的
    override fun getSupportedAnnotationTypes(): Set<String> {
        return setOf(
            BindLayout::class.java.canonicalName,
            BindView::class.java.canonicalName,
            OnClick::class.java.canonicalName
        )
    }

    override fun init(processingEnv: ProcessingEnvironment) {
        super.init(processingEnv)
        mLogger = Logger(processingEnv.messager)
        mLogger.info("processor init")
    }
}
//存储一个Activity文件所有注解数据,并有相应方法生成编译后的文件
class InjectInfo(val element: TypeElement) {

    var mLogger: Logger? = null
    //类名
    val className: ClassName = element.asClassName()
    val viewClass: ClassName = ClassName("android.view", "View")
    //包名
    val packageName: String = getPackageName(element).qualifiedName.toString()

    //布局只有一个id
    var layoutId: Int = -1
    //View 注解数据可能有多个 注意是VariableElement
    val viewMap = hashMapOf<Int, VariableElement>()
    //点击事件 注解数据可能有多个 注意是ExecutableElement
    val clickListenerMap = hashMapOf<Int, ExecutableElement>()


    private fun getPackageName(element: Element): PackageElement {
        var e = element
        while (e.kind != ElementKind.PACKAGE) {
            e = e.enclosingElement
        }
        return e as PackageElement
    }

    fun getClassName(element: Element): ClassName {
        var elementType = element.asType().asTypeName()

        return elementType as ClassName
    }

    //自动生成构造方法,主要使用kotlinpoet
    fun generateConstructor(): FunSpec {
        //构造方法,传入activity参数
        val builder = FunSpec.constructorBuilder().addParameter("target", className)
            .addParameter("view", viewClass)

        if (layoutId != -1) {
            builder.addStatement("target.setContentView(%L)", layoutId)
        }

        viewMap.forEach { (id, variableElement) ->
            builder.addStatement(
                "target.%N = view.findViewById(%L)",
                variableElement.simpleName,
                id
            )
        }

        clickListenerMap.forEach { (id, element) ->

            when (element.parameters.size) {
                //没有参数
                0 -> builder.addStatement(
                    "(view.findViewById(%L) as View).setOnClickListener{target.%N()}"
                    , id
                )
                //一个参数
                1 -> {
                    if (getClassName(element.parameters[0]) != viewClass) {
                        mLogger?.error("element.simpleName function parameter error")
                    }
                    builder.addStatement(
                        "(view.findViewById(%L) as View).setOnClickListener{target.%N(it)}"
                        , id, element.simpleName
                    )
                }
                //多个参数错误
                else -> mLogger?.error("element.simpleName function parameter error")
            }

        }

       return builder.build()
    }

}

三、app module中引入上面两个lib

//gradle引入
    implementation project(':lib_annotations')
    kapt project(':lib_processor')
@BindLayout(R.layout.activity_main)
class MainActivity : AppCompatActivity() {

    @BindView(R.id.tv_hello)
    lateinit var textView: TextView
    @BindView(R.id.bt_click)
    lateinit var btClick: Button

    private var mClickBtNum = 0
    private var mClickTvNum = 0
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main)
        //这里第4步内容
        BindApi.bind(this)

        textView.text = "测试成功......"
        btClick.text = "点击0次"
    }

    @OnClick(R.id.bt_click, R.id.tv_hello)
    fun onClick(view: View) {
        when (view.id) {
            R.id.bt_click -> {
                mClickBtNum++
                btClick.text = "点击${mClickBtNum}次"
            }
            R.id.tv_hello -> {
                mClickTvNum++
                textView.text = "点击文字${mClickTvNum}次"
            }
        }
    }
}

现在就可以直接编译,编译后我们就可以找到编译生成的类MainActivity_BindTest,

import android.view.View

class MainActivity_BindTest(
    target: MainActivity,
    view: View) {
    init {
        target.setContentView(2131361820)
        target.btClick = view.findViewById(2131165250)
        target.textView = view.findViewById(2131165360)
        (view.findViewById(2131165250) as View).setOnClickListener { target.onClick(it) }
        (view.findViewById(2131165360) as View).setOnClickListener { target.onClick(it) }
    }
}

这里当然还不能用,因为我们没有把MainActivity_BindTest和MainActivity关联上。

四、创建App module(lib_api)

object BindApi {

    //类似ButterKnife方法
    fun bind(target: Activity) {
        val sourceView = target.window.decorView
        createBinding(target, sourceView)
    }

    private fun createBinding(target: Activity, source: View) {
        val targetClass = target::class.java
        var className = targetClass.name
        try {
            //获取类名
            val bindingClass = targetClass.classLoader!!.loadClass(className + "_BindTest")
            //获取构造方法
            val constructor = bindingClass.getConstructor(targetClass, View::class.java)
            //向方法中传入数据activity和view
            constructor.newInstance(target, source)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
        } catch (e: NoSuchMethodException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        } catch (e: InstantiationException) {
            e.printStackTrace()
        } catch (e: InvocationTargetException) {
            e.printStackTrace()
        }
    }
}

并在app中引用

implementation project(':lib_api')

五、总结

流程还是比较简单,创建annotation、processor、lib_api 3个module,我们打包时并不需要processor包,它的目的仅仅是生成相应的文件代码。

注意点:

1、annotation 和processor要引入

apply plugin: 'kotlin'

2、编译时打印使用Messager,注意JDK8打印NOTE无法显示

3、lib_api 文件在反射时要注意和processor对应,修改时注意同步修改等

有用的话加个关注哦!!!