Kotlin編譯時註解,簡單實現ButterKnife

  • 2019 年 10 月 10 日
  • 筆記

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對應,修改時注意同步修改等

有用的話加個關注哦!!!

代碼