Kotlin编译时注解,简单实现ButterKnife

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)

Kotlin对编译时注解时Retention 并没有太多的要求,一般我们使用AnnotationRetention.BINARY或者SOURCE,但是我发现ButterKnife用的是Runtime,测试也可以。

但具体为什么用,不是特别明白,自己认为是AnnotationRetention.RUNTIME基本包含了BINARY或者SOURCE的功能,还支持反射。

二、创建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对应,修改时注意同步修改等

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

代码

原文地址:https://www.cnblogs.com/doubleyoujs/p/11651402.html

时间: 2024-07-30 08:18:17

Kotlin编译时注解,简单实现ButterKnife的相关文章

Android 如何编写基于编译时注解的项目

本文已在CSDN<程序员>杂志刊登. 本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发. 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/51931859: 本文出自:[张鸿洋的博客] 一.概述 在Android应用开发中,我们常常为了提升开发效率会选择使用一些基于注解的框架,但是由于反射造成一定运行效率的损耗,所以我们会更青睐于编译时注解的框架,例如: butterknife免去我们编写

Android 编译时注解-提升

Android 编译时注解-提升 背景 在前面的文章中,讲解了注解和编译时注解等一些列相关的内容,为了更加全面和真是的了解Android 编译时注解在实战项目中的使用,本文采取实现主流框架butterknife注入view去全面认识编译时注解. 注解专栏-博客 效果 先来张图压压惊,实现效果butterknife的view绑定 使用 仿照butterknife实现了@BindView注解,通过WzgJector.bind方法绑定当前MainActivity,整体和butterknife使用完全一

关于java编译时注解你需要知道的二三事。解除你的顾虑!

转载请注明出处: http://blog.csdn.net/liu470368500/article/details/51316066 做Android开发.大家肯定会关心你的app的性能问题.不知道从何时开始.网上有流传一句.不要使用注解.用注解会影响性能.这不能说错.但是也不能说对.这里普及一下关于注解的一些你需要知道的知识 网上常说的注解.基本是运行时注解.而所说的注解会影响性能.则是指的此类型的注解.因为运行时注解的解析.完全依赖于反射.而反射的效率.是比原生的慢的.特别是对于原先的老机

Android 打造编译时注解解析框架 这只是一个开始

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43452969 ,本文出自:[张鸿洋的博客] 1.概述 记得很久以前,写过几篇博客,容我列举一下: Android 进阶 教你打造 Android 中的 IOC 框架 [ViewInject] (上) Android 进阶 教你打造 Android 中的 IOC 框架 [ViewInject] (下) Android 框架炼成 教你如何写组件间通信框架EventBus 大家可以关

java 编译时注解框架 lombok-ex

lombok-ex lombok-ex 是一款类似于 lombok 的编译时注解框架. 编译时注,拥有运行时注解的便利性,和无任何损失的性能. 主要补充一些 lombok 没有实现,且自己会用到的常见工具. 创作目的 补充 lombok 缺失的注解,便于日常开发使用. lombok 的源码基本不可读,应该是加密处理了. 为其他注解相关框架提升性能提供基础,后期考虑替换为编译时注解. 特性 @Serial 支持 (1)类实现序列化接口 (2)类生成 serialVersionUID 字段,字段的值

Java编译时注解自动生成代码

在开始之前,我们首先申明一个非常重要的问题:我们并不讨论那些在运行时(Runtime)通过反射机制运行处理的注解,而是讨论在编译时(Compile time)处理的注解.注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具.可以为特定的注解,注册自己的注解处理器. 一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出.可以生成Java代码,这些生成的Java代码是在生成的.java文件中,所以不能修改已经存在的Java类,例如

自定义注解之运行时注解(RetentionPolicy.RUNTIME)

对注解概念不了解的可以先看这个:Java注解基础概念总结 前面有提到注解按生命周期来划分可分为3类: 1.RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃: 2.RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期: 3.RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在: 这3个生

编译时,运行时解释

在开发和设计的时候,我们需要考虑编译时,运行时以及构建时这三个概念.理解这几个概念可以更好地帮助你去了解一些基本的原理.下面是初学者晋级中级水平需要知道的一些问题. Q.下面的代码片段中,行A和行B所标识的代码有什么区别呢? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ConstantFolding {     static final  int number1 = 5;     static final  int n

Android运行时注解

Android的注解有编译时注解和运行时注解,本文就介绍下运行时注解. 其实非常简单,直接上代码:本文主要是替代传统的findViewById()的功能,就是在我们Activity中不需要再使用findViewById()去给View赋值了,通过注解在运行阶段自动赋值.以及setOnClickListener()也是一样的原理.使用注解和反射技术. 1. 定义自己的annotation注解. 定义findViewbyId这个功能的注解 package com.xxx.xxx.xxx; impor