專案中要減少反射,提高性能,可以apt或是aop,網上有很多java apt的文章,可是利用kotlin文章比較少,有的也不夠詳細,
Demo 仿著名的butterknife實作一個簡單的View系結
編譯時注解核心三個模塊,一個安卓庫(實作一些需要的功能),一個java compiler庫(實作編譯時生成代理),一個java annotaions庫(注解庫),
架構
我們需要新建三個模塊

依賴
compiler 增加kapt插件和依賴,如下:
plugins {
id 'java'
id 'java-library'
id 'kotlin'
id 'kotlin-kapt'
}
java {
//默認創建為1.7,一定要改1.8,不然無法匯入com.squareup:kotlinpoet:1.8.0
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation project(path: ':apt-annotations')
implementation "com.google.auto.service:auto-service:1.0"
kapt "com.google.auto.service:auto-service:1.0"
implementation "com.squareup:kotlinpoet:1.8.0"
}
關鍵是JavaVersion.VERSION_1_8 ,默認1.7,浪費了我幾個小時排查這個錯誤,
kotlinpoet是輔助生產代理的工具,如果不用可以直接用io流寫檔案,優點類似jsp,kotlinpoet官方API說明檔案
app模塊整加依賴,如下:
plugins {
.......
id 'kotlin-kapt'
}
android{
.......
}
dependencies {
......
implementation project(path: ':apt')
kapt project(path: ':apt-annotations')
implementation project(path: ':apt-annotations')
//注意 一定要kapt
kapt project(path: ':apt-compiler')
.......
}
代碼
因為是demo 我們只實作一個bindview,所以注解模塊最簡單,寫一個注解
/**
* 用來注入view
* @author markrenChina
*/
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.BINARY)
annotation class BindView(
val value: Int
)
apt模塊是一些具體的功能,具體到我們模仿的butterknife,是一個Unbinder介面,一個靜態方法(反射創建生成代碼的實體),為了減少代碼還有一個工具類,如下:
Apt.kt
object Apt {
fun bind(activity: Activity): Unbinder {
//反射創建實體
try {
val bindClassName: Class<out Unbinder> =
Class.forName("${activity.javaClass.name}ViewBinding") as Class<out Unbinder>
//建構式
val bindConstructor: Constructor<out Unbinder> =
bindClassName.getDeclaredConstructor(activity.javaClass)
return bindConstructor.newInstance(activity)
} catch (e: Exception) {
e.printStackTrace()
}
return Unbinder.EMPTY
}
}
butterknife 原始碼為了保證后面代碼正確,保證回傳的不是一個空型別,但是kotlin可空型別可以很好的處理這個問題,這里我們按照butterknife的原始碼
Unbinder.kt
interface Unbinder {
@UiThread
fun unbind()
companion object {
val EMPTY: Unbinder = object : Unbinder {
override fun unbind() {}
}
}
}
Utils.kt
object Utils {
fun <T : View> findViewById(activity: Activity?, viewId: Int): T? {
return activity?.findViewById(viewId) as T?
}
}
app模塊:
MainActivity.kt
class MainActivity : AppCompatActivity() {
@BindView(R.id.hello_world)
var helloWorld: TextView? = null
private lateinit var mUnbinder: Unbinder
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mUnbinder = Apt.bind(this)
helloWorld?.text = "hello markrenChina!!"
}
override fun onDestroy() {
mUnbinder.unbind()
super.onDestroy()
}
}
布局送的TextView加一個id,hello_world
compiler模塊
代碼比較多,一個功能一個功能介紹
新建一個類繼承AbstractProcessor來實作apt,為了避免寫配置,需要@AutoService(Processor::class)注解
@AutoService(Processor::class)
class AptProcessor : AbstractProcessor() {}
一定要復寫的process是處理的關鍵方法,
在初始化時我們需要拿到2個全域變數
private var mFiler: Filer? = null
private var mElementUtils: Elements? = null
override fun init(processingEnv: ProcessingEnvironment?) {
super.init(processingEnv)
mFiler = processingEnv?.filer
mElementUtils = processingEnv?.elementUtils
}
Filer是javax包下注解處理的檔案介面
mElementUtils是大名鼎鼎的javax.lang包下的,后面用來處理類相關的,
然后是一些模板代理,大致所有的處理都是這么寫的:
//指定處理的版本
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
//給到需要處理的注解
override fun getSupportedAnnotationTypes(): MutableSet<String> {
val types: LinkedHashSet<String> = LinkedHashSet()
getSupportedAnnotations().forEach { clazz: Class<out Annotation> ->
types.add(clazz.canonicalName)
}
return types
}
private fun getSupportedAnnotations(): Set<Class<out Annotation>> {
val annotations: LinkedHashSet<Class<out Annotation>> = LinkedHashSet()
// 需要決議的自定義注解
annotations.add(BindView::class.java)
return annotations
}
核心process方法:
- 按照類整理屬性,獲取類的Element (element.enclosingElement還是Element)
override fun process(
p0: MutableSet<out TypeElement>?,
roundEnvironment: RoundEnvironment?
): Boolean {
//決議屬性 activity ->list<Element>
val elementMap = LinkedHashMap<Element, ArrayList<Element>>()
// 有注解就會進來
roundEnvironment?.getElementsAnnotatedWith(BindView::class.java)?.forEach { element ->
//按照 類 整理 屬性
val enclosingElement = element.enclosingElement
var viewBindElements = elementMap[enclosingElement]
if (viewBindElements == null) {
viewBindElements = ArrayList()
elementMap[enclosingElement] = viewBindElements
}
viewBindElements.add(element)
}
- 整理后根據一個key(activity)生成一個viewBind.kt,所有對map進行回圈,
// 生成代碼
elementMap.entries.forEach {
val clazz = it.key //Element
val viewBindElements = it.value //ArrayList<Element>
.......
}
接下去是使用kotlinpoet生成原始碼,如果用檔案流,可以忽略以下代碼:
kotlinpoet里面有一個ClassName,通過它可以拿到很多Class的屬性,比如,我們獲取apt下介面的方式:
val interfaceClassName = ClassName("com.ccand99.apt", "Unbinder")
com.ccand99.apt是包名,作為android library,app參考時包名時固定的,我們可以寫死,
獲取Element包名的方式
//動態獲取包名
val packageName = mElementUtils?.getPackageOf(clazz)?.qualifiedName?.toString()
?: throw RuntimeException("無法獲取包名")
這個包名很重要,知道這個包名可以獲取包下類,我們生成類的目錄等等,比如,我們activity的獲取
val activityStr = clazz.simpleName.toString()
val activityKtClass = ClassName(packageName, activityStr)
獲取activity的ClassName,我們才能去拼接kotlin中的 引數名:引數型別,例如,activity:MainActivity,如果用字串,MainActivity是可以直接用element.simpleName.toString獲得,直接:字串,生成的原始碼也沒有問題,但是,使用ClassName,kotlinpoet會自動幫助import的,當然javapoet也是有的,java有太多的型別 引數名的格式,還是直接用字串比較方面,
注意的幾個方法:
設定類屬性:
val property = PropertySpec.builder("target", activityKtClass.copy(nullable = true))
.initializer("target")
.mutable()//var 不加val
.addModifiers(KModifier.PRIVATE)
設定建構式
//構造建構式
val constructorMethodBuilder = FunSpec.constructorBuilder()
.addParameter("target", activityKtClass.copy(nullable = true))//java 不需要傳型別 可空
viewBindElements.forEach { element ->
val resId = element.getAnnotation(BindView::class.java).value
constructorMethodBuilder.addStatement("target?.${element.simpleName} = %T.findViewById(target,$resId)",findByIdUtilsClass)
}
占位用%,跟javapoet不一樣
構造unbind方法
//生成類方法
val unbindMethodBuilder = FunSpec.builder("unbind")
.addAnnotation(callSuperClassName)
.addModifiers(KModifier.OVERRIDE)
.addComment("銷毀")
.addStatement("this.target = target")
.addStatement("if (target == null) throw IllegalStateException(\"Binding already cleares.\")")
.addStatement("target = null")
viewBindElements.forEach { element ->
unbindMethodBuilder.addStatement("target?.${element.simpleName} = null")
}
構造類:
// 構造類
// public final 類kotlin為KModifier
val clazzBuilder = TypeSpec.classBuilder("${clazz.simpleName}ViewBinding")
.addModifiers(KModifier.FINAL, KModifier.PUBLIC)
//建構式
.primaryConstructor(constructorMethodBuilder.build())
.addProperty(property.build())
.addSuperinterface(interfaceClassName)
clazzBuilder.addFunction(unbindMethodBuilder.build())
生成類檔案
//生成類檔案
val classFile = FileSpec.builder(packageName, "${clazz.simpleName}ViewBinding")
.addType(clazzBuilder.build())
.addComment("測驗 自動生成")
.build()
//classFile.writeTo(System.out)
//輸出的檔案映射
try {
mFiler?.let { filer -> classFile.writeTo(filer) }
} catch (e: IOException) {
println(e.message)
}
生成源代理:
// 測驗 自動生成
package com.ccand99.apt
import androidx.`annotation`.CallSuper
import kotlin.Unit
public final class MainActivityViewBinding(
private var target: MainActivity?
) : Unbinder {
init {
target?.helloWorld = Utils.findViewById(target,2131230886)
}
@CallSuper
public override fun unbind(): Unit {
// 銷毀
this.target = target
if (target == null) throw IllegalStateException("Binding already cleares.")
target = null
target?.helloWorld = null
}
}
Demo代碼鏈接
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/287410.html
標籤:其他
