阅读 275

不会使用注解生成文件?赶快学起来!

前言

在之前文章我们介绍了注解的相关基础知识,以及使用反射来实现运行时注解的原理,可以先查看文章:

# 学会自定义注解,看这就够了(1)---自定义运行时注解

了解后,再来看本章内容,本章主要说一下注解的另一种形式,就是编译时注解的使用,同样以开源库源码来说其原理。

正文

既然我们是以PermissionsDispatcher开源库来说,我们先看一下这个库的简单使用。

PermissionsDispatcher

对于Android 6版本加的运行时权限这里先不赘述了,相信Android开发都很熟悉,这里介绍一个开源库来处理运行时权限的,就是这个PermissionsDispatcher,项目地址是:

github.com/permissions…

来看一下简单使用,这里的代码是一个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个注解,

使用.jpg

其中注意点是写完注解后需要进行build也就是编译项目,这时会生成文件:

生成文件.jpg

在点击按钮时就不能直接调用

showCamera()
复制代码

了,需要调用

showCameraWithPermissionCheck()
复制代码

这个生成的方法,才可以,我们还是看一下效果图,再讨论细节实现:

权限被拒绝,直到被永不提醒,

dihrr-2gdvq.gif

权限被同意,执行对应方法,

允许.gif

ok,那接下来的重头戏就是看看如何在编译完项目生成文件,同时再调用生成文件的方法。

注解处理器

和运行时注解不同需要在代码允许时边解析注解边处理,编译时注解在注解使用后,项目进行build,便可以生效生成文件,这里就需要一个注解处理器。所以这里的重点就是注解处理器了,对于注册注解和之前运行时注解一样,只是处理注解不一样。

简介

注解处理器简介.png

既然知道了注解的作用,那就是如何定义注解以及在编译时处理注解。

自定义注解处理器

之前文章的运行时注解很容易理解,在代码中有代码操作进行注册,然后通过反射去扫描注解,那这个编译时注解我们进行build时系统如何去加载注解呢 这里就要先注册注解处理器。

注册注解处理器

这里注册也非常简单,一共分为2步:

  • 在gradle中的android范围里添加
packagingOptions {
    exclude 'META-INF/services/javax.annotation.processing.Processor'
}
复制代码
  • 在这个Processor中写上自定义注解处理器即可,这个文件的地方在

image.png

里面内容是:

permissions.dispatcher.processor.PermissionsProcessor
复制代码

这个PermissionsProcessor就是我们的自定义注解处理器,在进行build编译时就会调用。

AbstractProcessor

看一下这个PermissionsProcessor的类:

class PermissionsProcessor : AbstractProcessor() {}
复制代码

会发现这里继承一个AbstractProcessor类,这里必须要明白这个抽象类,不然代码真的很难读懂,这部分的代码就和上一篇文章中的反射一样,有很多不常用的API。

AbstractProcessor.png

通过上图总结,我们也很容易理解,注解扫描等工作系统帮我们做好了,这里我们只需要根据需求,编写逻辑即可。

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的类型:

Element类型.png

所以对于一段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元素,那看看这个类有哪些方法,主要方法如下:

Element方法.png

看到这几个方法我们就好处理了,前面我们说了要处理的是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中,下面是打印:

image.png

其中还要2个关键的方法就是获取该元素节点下面的所有元素:

println("zyh enclosedElements = ${element.enclosedElements}")
println("zyh enclosingElement = ${element.enclosingElement}")
复制代码

打印是:

image.png

所以我们大体思路就很明确了,大概如下:

注解.png

来看看代码是如何操作的,这里代码比较多,我们就以扫描到的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了,先看一下官网使用示例:

square.github.io/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文件。

文章分类
Android
文章标签