前言
在之前文章我们介绍了注解的相关基础知识,以及使用反射来实现运行时注解的原理,可以先查看文章:
了解后,再来看本章内容,本章主要说一下注解的另一种形式,就是编译时注解的使用,同样以开源库源码来说其原理。
正文
既然我们是以PermissionsDispatcher开源库来说,我们先看一下这个库的简单使用。
PermissionsDispatcher
对于Android 6版本加的运行时权限这里先不赘述了,相信Android开发都很熟悉,这里介绍一个开源库来处理运行时权限的,就是这个PermissionsDispatcher,项目地址是:
来看一下简单使用,这里的代码是一个activity点击按钮申请相机权限:
@RuntimePermissions
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//点击按钮 申请权限
val buttonCamera: Button = findViewById(R.id.button_camera)
buttonCamera.setOnClickListener {
//调用方法
showCameraWithPermissionCheck()
}
}
//系统申请权限回调
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
onRequestPermissionsResult(requestCode, grantResults)
}
//必须在权限申请成功后才会调用
@NeedsPermission(Manifest.permission.CAMERA)
fun showCamera() {
Log.i(TAG, "showCamera: 相机权限获取成功后")
supportFragmentManager.beginTransaction()
.replace(R.id.sample_content_fragment, CameraPreviewFragment.newInstance())
.addToBackStack("camera")
.commitAllowingStateLoss()
}
//申请权限被拒绝
@OnPermissionDenied(Manifest.permission.CAMERA)
fun onCameraDenied() {
Log.i(TAG, "onCameraDenied: 相机权限被拒绝")
Toast.makeText(this, "相机权限被拒绝", Toast.LENGTH_SHORT).show()
}
//拒绝后说明要这个权限的理由
@OnShowRationale(Manifest.permission.CAMERA)
fun showRationaleForCamera(request: PermissionRequest) {
Log.i(TAG, "showRationaleForCamera: 显示相机权限的理由")
showRationaleDialog(R.string.permission_camera_rationale, request)
}
//永远不再提示
@OnNeverAskAgain(Manifest.permission.CAMERA)
fun onCameraNeverAskAgain() {
Log.i(TAG, "onCameraNeverAskAgain: 永远不再询问")
Toast.makeText(this, R.string.permission_camera_never_ask_again, Toast.LENGTH_SHORT).show()
}
//弹窗说明理由
private fun showRationaleDialog(@StringRes messageResId: Int, request: PermissionRequest) {
AlertDialog.Builder(this)
.setPositiveButton(R.string.button_allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.button_deny) { _, _ -> request.cancel() }
.setCancelable(false)
.setMessage(messageResId)
.show()
}
}
复制代码
其实不外乎就是权限申请的几种情况,申请成功、被拒绝、永不询问这3种情况,所以上面对应了3个注解,
其中注意点是写完注解后需要进行build也就是编译项目,这时会生成文件:
在点击按钮时就不能直接调用
showCamera()
复制代码
了,需要调用
showCameraWithPermissionCheck()
复制代码
这个生成的方法,才可以,我们还是看一下效果图,再讨论细节实现:
权限被拒绝,直到被永不提醒,
权限被同意,执行对应方法,
ok,那接下来的重头戏就是看看如何在编译完项目生成文件,同时再调用生成文件的方法。
注解处理器
和运行时注解不同需要在代码允许时边解析注解边处理,编译时注解在注解使用后,项目进行build,便可以生效生成文件,这里就需要一个注解处理器。所以这里的重点就是注解处理器了,对于注册注解和之前运行时注解一样,只是处理注解不一样。
简介
既然知道了注解的作用,那就是如何定义注解以及在编译时处理注解。
自定义注解处理器
之前文章的运行时注解很容易理解,在代码中有代码操作进行注册,然后通过反射去扫描注解,那这个编译时注解我们进行build时系统如何去加载注解呢 这里就要先注册注解处理器。
注册注解处理器
这里注册也非常简单,一共分为2步:
- 在gradle中的android范围里添加
packagingOptions {
exclude 'META-INF/services/javax.annotation.processing.Processor'
}
复制代码
- 在这个Processor中写上自定义注解处理器即可,这个文件的地方在
里面内容是:
permissions.dispatcher.processor.PermissionsProcessor
复制代码
这个PermissionsProcessor就是我们的自定义注解处理器,在进行build编译时就会调用。
AbstractProcessor
看一下这个PermissionsProcessor的类:
class PermissionsProcessor : AbstractProcessor() {}
复制代码
会发现这里继承一个AbstractProcessor类,这里必须要明白这个抽象类,不然代码真的很难读懂,这部分的代码就和上一篇文章中的反射一样,有很多不常用的API。
通过上图总结,我们也很容易理解,注解扫描等工作系统帮我们做好了,这里我们只需要根据需求,编写逻辑即可。
PermissionsProcessor
既然明白了AbstractProcessor的几个抽象方法作用,那紧接着看一下这个PermissionsProcessor的实现:
//工具类 后面细说
var ELEMENT_UTILS: Elements by Delegates.notNull()
//工具类 后面细说
var TYPE_UTILS: Types by Delegates.notNull()
class PermissionsProcessor : AbstractProcessor() {
//java和kotlin的具体操作器,先不管
private val javaProcessorUnits = listOf(JavaActivityProcessorUnit(), JavaFragmentProcessorUnit())
private val kotlinProcessorUnits = listOf(KotlinActivityProcessorUnit(), KotlinFragmentProcessorUnit())
//生成文件的类
private var filer: Filer by Delegates.notNull()
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
//很重要的init方法,从参数中可以获得工具
filer = processingEnv.filer
ELEMENT_UTILS = processingEnv.elementUtils
TYPE_UTILS = processingEnv.typeUtils
}
override fun getSupportedSourceVersion(): SourceVersion? {
//一般不做修改
return SourceVersion.latestSupported()
}
override fun getSupportedAnnotationTypes(): Set<String> {
//返回需要扫描和处理的注解
return hashSetOf(RuntimePermissions::class.java.canonicalName)
}
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
// 具体操作逻辑
val requestCodeProvider = RequestCodeProvider()
roundEnv.getElementsAnnotatedWith(RuntimePermissions::class.java)
.sortedBy { it.simpleName.toString() }
.forEach {
val rpe = RuntimePermissionsElement(it as TypeElement)
val kotlinMetadata = it.getAnnotation(Metadata::class.java)
if (kotlinMetadata != null) {
processKotlin(it, rpe, requestCodeProvider)
} else {
processJava(it, rpe, requestCodeProvider)
}
}
return true
}
//kotlin代码处理,先不管
private fun processKotlin(element: Element, rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider) {
val processorUnit = findAndValidateProcessorUnit(kotlinProcessorUnits, element)
val kotlinFile = processorUnit.createFile(rpe, requestCodeProvider)
kotlinFile.writeTo(filer)
}
//java代码处理,先不管
private fun processJava(element: Element, rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider) {
val processorUnit = findAndValidateProcessorUnit(javaProcessorUnits, element)
val javaFile = processorUnit.createFile(rpe, requestCodeProvider)
javaFile.writeTo(filer)
}
}
复制代码
在这里我们看到是先扫描和处理RuntimePermissions这个注解,通过PermissionsDispatcher库的使用,这个注解是注解在activity或者fragment中,是必不可少的,说明该页面需要动态权限,所以这样设计很合理。
先重点关注一下处理代码
roundEnv.getElementsAnnotatedWith(RuntimePermissions::class.java)
.sortedBy { it.simpleName.toString() }
.forEach {
val rpe = RuntimePermissionsElement(it as TypeElement)
val kotlinMetadata = it.getAnnotation(Metadata::class.java)
if (kotlinMetadata != null) {
processKotlin(it, rpe, requestCodeProvider)
} else {
processJava(it, rpe, requestCodeProvider)
}
}
复制代码
又是一些不太熟悉的API,这里先不要方,和前面反射一样,先普及一些基本知识。
扫描Java文件
在注解器工作过程中,是通过扫描Java源文件来工作的,和运行时反射不一样,这里的扫描Java文件就像解析xml一样,比如下面代码:
package com.example; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
复制代码
这里其实并没有运行Java代码,只是进行编译扫描,这里把Java代码给解析成一个个Element元素,然后再进行处理,下面是所有Element的类型:
所以对于一段Java代码,经过扫描后,就是这几种Element,对特定的Element进行处理即可。其实这里也很容易理解,就是Java代码的组成部分。
获取特定的Element
在之前的PermissionsProcessor中,我们设置了注解在Activity或者Fragment上的@RuntimePermissions注解,那么在process中就会把扫描到的带该注解的Element进行回调,这里的类型就是TypeElement。
//获取所有带该注解的Element
roundEnv.getElementsAnnotatedWith(RuntimePermissions::class.java)
.sortedBy { it.simpleName.toString() }
.forEach {
//根据定义,这个元素是TypeElement类型
val rpe = RuntimePermissionsElement(it as TypeElement)
//再对该Elemenet进行解析和处理
val kotlinMetadata = it.getAnnotation(Metadata::class.java)
if (kotlinMetadata != null) {
processKotlin(it, rpe, requestCodeProvider)
} else {
processJava(it, rpe, requestCodeProvider)
}
}
复制代码
根据权限库的使用,这里返回的是类Element,那我们还需要根据这个类元素来找到里面各种方法的注释,我们来看一下如何做的。
处理特定的Element
既然处理Element元素,那看看这个类有哪些方法,主要方法如下:
看到这几个方法我们就好处理了,前面我们说了要处理的是TypeElement,找出类中的方法,那看一下源码是不是这样做的,在process中,调用下面方法:
val rpe = RuntimePermissionsElement(it as TypeElement)
复制代码
所以看一下RuntimePermissionsElement类:
//解析TypeElement元素节点
class RuntimePermissionsElement(val element: TypeElement) {
//拿到各种这个元素的信息
val typeName: TypeName = TypeName.get(element.asType())
val ktTypeName = element.asType().asTypeName()
val typeVariables = element.typeParameters.map { TypeVariableName.get(it) }
val ktTypeVariables = element.typeParameters.map { it.asTypeVariableName() }
val packageName = element.packageName()
val inputClassName = element.simpleString()
val generatedClassName = inputClassName + GEN_CLASS_SUFFIX
val needsElements = element.childElementsAnnotatedWith(NeedsPermission::class.java)
private val onRationaleElements = element.childElementsAnnotatedWith(OnShowRationale::class.java)
private val onDeniedElements = element.childElementsAnnotatedWith(OnPermissionDenied::class.java)
private val onNeverAskElements = element.childElementsAnnotatedWith(OnNeverAskAgain::class.java)
init {
//先进行打印 看看都是啥
println("zyh $typeName")
println("zyh $ktTypeName")
println("zyh $typeVariables")
println("zyh $ktTypeVariables")
println("zyh $packageName")
println("zyh $inputClassName")
println("zyh $generatedClassName")
println("zyh $needsElements")
println("zyh $onRationaleElements")
println("zyh $onDeniedElements")
println("zyh $onNeverAskElements")
validateNeedsMethods()
validateRationaleMethods()
validateDeniedMethods()
validateNeverAskMethods()
}
}
复制代码
这里就注意必须要使用println来打印,不能使用Log了,因为Log是android的库,这里属于编译期的编译,引用不到android的库,同时打印也在Build Output中,不在logcat中,下面是打印:
其中还要2个关键的方法就是获取该元素节点下面的所有元素:
println("zyh enclosedElements = ${element.enclosedElements}")
println("zyh enclosingElement = ${element.enclosingElement}")
复制代码
打印是:
所以我们大体思路就很明确了,大概如下:
来看看代码是如何操作的,这里代码比较多,我们就以扫描到的TypeElement节点元素来找到下面@NeedPermissions注解定义的方法,其他几种注解是一样的代码如下:
private fun validateNeedsMethods() {
//必须要定义的,否则报错
checkNotEmpty(needsElements, this, NeedsPermission::class.java)
//不能是private修饰符
checkPrivateMethods(needsElements, NeedsPermission::class.java)
//方法返回值必须是Void
checkMethodSignature(needsElements)
//获取注解里的值
checkMixPermissionType(needsElements, NeedsPermission::class.java)
//判断这个方法是否是真是这个类里的
checkDuplicatedMethodName(needsElements)
}
复制代码
经过这一系列操作,我们就很容易得到注解所包含的内容了,到这里解析注解也全部结束。
生成文件
这里又是编译时注解的精髓所在,在我们通过扫描文件的方式获取到注解以及其中的元素后,便需要根据情况生成文件了。
kotlinpoet或者javapoet
对于生成文件,肯定不用我们来处理,这里推荐使用poet开源库来帮我们完成,我这里因为是Android就使用了kotlinpoet了,先看一下官网使用示例:
会发现其实还很简单的,依据你想生成的文件,把Java文件拆成一个个节点进行拼接即可,代码如下:
private fun processKotlin(element: Element
, rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider) {
val processorUnit = findAndValidateProcessorUnit(kotlinProcessorUnits, element)
val kotlinFile = processorUnit.createFile(rpe, requestCodeProvider)
kotlinFile.writeTo(filer)
}
复制代码
这里的关键地方就是createFile函数,通过这个来拼接想生成的文件:
override fun createFile(rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider): FileSpec {
return FileSpec.builder(rpe.packageName, rpe.generatedClassName)
.addComment(FILE_COMMENT)
.addAnnotation(createJvmNameAnnotation(rpe.generatedClassName))
.addProperties(createProperties(rpe, requestCodeProvider))
.addFunctions(createWithPermissionCheckFuns(rpe))
.addFunctions(createOnShowRationaleCallbackFuns(rpe))
.addFunctions(createPermissionHandlingFuns(rpe))
.addTypes(createPermissionRequestClasses(rpe))
.build()
}
复制代码
其实这个直接看名字就能发现这个需要添加的文件内容,比如注释、注解、属性、方法、类型等,看一下生成的文件:
// This file was generated by PermissionsDispatcher. Do not modify!
@file:JvmName("MainActivityPermissionsDispatcher")
package permissions.dispatcher.sample
import androidx.core.app.ActivityCompat
import java.lang.ref.WeakReference
import kotlin.Array
import kotlin.Int
import kotlin.IntArray
import kotlin.String
import permissions.dispatcher.PermissionRequest
import permissions.dispatcher.PermissionUtils
private const val REQUEST_SHOWCAMERA: Int = 0
private val PERMISSION_SHOWCAMERA: Array<String> = arrayOf("android.permission.CAMERA")
fun MainActivity.showCameraWithPermissionCheck() {
if (PermissionUtils.hasSelfPermissions(this, *PERMISSION_SHOWCAMERA)) {
showCamera()
} else {
if (PermissionUtils.shouldShowRequestPermissionRationale(this, *PERMISSION_SHOWCAMERA)) {
showRationaleForCamera(MainActivityShowCameraPermissionRequest(this))
} else {
ActivityCompat.requestPermissions(this, PERMISSION_SHOWCAMERA, REQUEST_SHOWCAMERA)
}
}
}
fun MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
when (requestCode) {
REQUEST_SHOWCAMERA ->
{
if (PermissionUtils.verifyPermissions(*grantResults)) {
showCamera()
} else {
if (!PermissionUtils.shouldShowRequestPermissionRationale(this, *PERMISSION_SHOWCAMERA)) {
onCameraNeverAskAgain()
} else {
onCameraDenied()
}
}
}
}
}
private class MainActivityShowCameraPermissionRequest(
target: MainActivity
) : PermissionRequest {
private val weakTarget: WeakReference<MainActivity> = WeakReference(target)
override fun proceed() {
val target = weakTarget.get() ?: return
ActivityCompat.requestPermissions(target, PERMISSION_SHOWCAMERA, REQUEST_SHOWCAMERA)
}
override fun cancel() {
val target = weakTarget.get() ?: return
target.onCameraDenied()
}
}
复制代码
会发现和上面添加是节点是一一对应的。
生成完文件,编译时注解的解析也全部说完了,难点还是扫描注解和生成文件的API太不熟悉了。
总结
相比于运行时注解能直接通过反射来获取注解信息和处理逻辑,编译时注解的操作要麻烦的多,需要定义注解解析器来扫描Java文件得到注解信息,然后再通过poet来生成Java文件。