13.从架构设计角度分析AAC源码-Room源码解析第2篇:RoomCompilerProcessing源码解析

前言

有人说细节决定成败,或者说别人注重的是结果,我们在意的是过程,roomCompilerProcessing源码读起来感觉好难哦,尤其细节处!!!

下面三浅一深,咳咳咳…由浅入深讲解roomCompilerProcessing源码。

下面每个部分,自己认真写,读者认真看。个人建议,可以在深入一点:在理解大框架的基础上,不要忽略细节部分(提醒自己和读者),否则自己想写出高逼格代码照样很难!!!

13.从架构设计角度分析AAC源码-Room源码解析第2篇:RoomCompilerProcessing源码解析_第1张图片

APT、KAPT和KSP的理解

简介

  1. apt:Annotation Processing Tool
  • 需要apply相应的android-apt插件,比如apply plugin: ‘com.neenbedankt.android-apt’。android gradle插件版本2.2以下使用,2.2发版时宣布不再维护。只支持 javac编译方式。
  1. annotationProcessor:已经取代apt
  • 无需apply android-apt插件。android gradle插件版本2.2及以上使用。同时支持javac和jack编译方式;
  1. kapt:Kotlin Annotation Processing Tool
  • kotlin注解处理工具。因kotlin-kapt不是android gradle内置插件,需要额外apply plugin: ‘kotlin-kapt’

  • 和annotationProcesor的区别是,kapt处理kotlin文件,当然如果是kotlin或java混合,那么也是必须使用kapt处理的。速度上交apt(或annotationProcessor)肯定要慢的,因为首先会将kotlin解析成Java代码,再通过apt处理;

  1. ksp:Kotlin Symbol Processing
  • 在进行Android利用开发时Kotlin 的编译速度慢,而KAPT 便是拖慢编译的首恶之一。Android的很多库都会应用注解简化模板代码,著名的有 Room、Dagger 等,而默认状况下Kotlin 应用的是 KAPT 来解决注解的。KAPT没有专门的注解处理器,须要借助APT实现的,因而须要先生成 APT 可解析的 stub (Java代码),这拖慢了 Kotlin 的整体编译速度。

  • KSP 正是在这个背景下诞生的,它基于 Kotlin Compiler Plugin(简称KCP) 实现,不须要生成额定的 java代码,编译速度是 KAPT 的 2 倍以上。

以上文字多数抄袭,融入了个人观点,为了让我们简单了解一下各个编译的区别。

demo

AbstractProcessor

kapt和annotationProcessor的使用完全一致。

@AutoService(Processor.class) //自动生成注解处理器路径文件
public class BindingProcessor extends AbstractProcessor {

	
	init(ProcessingEnvironment processingEnv):初始化操作

	getSupportedSourceVersion():设置版本

	getSupportedAnnotationTypes():设置支持的注解

	process(Set annotations, RoundEnvironment roundEnv):主要的注解元素解析方法
 
}

没有AbstractProcessor基础的,可参考Java学习之注解(五)Android循序渐进实现高逼格自定义ViewBinder。

SymbolProcessor

interface SymbolProcessor {
   	//处理节点
    fun process(resolver: Resolver): List

	//处理结束
    fun finish() {}

	//处理异常
    fun onError() {}
}

具体实现如下:

class RoomKspProcessor @JvmOverloads constructor(
    symbolProcessorEnvironment: SymbolProcessorEnvironment
) : SymbolProcessor {
	
	//继承SymbolProcessor三个方法,懒得写。。。

	//ksp内部会调用当前类的create方法,用于生成RoomKspProcessor对象
	 class Provider : SymbolProcessorProvider {
        override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
            return RoomKspProcessor(environment)
        }
    }
}

由上可知,AbstractProcessor和SymbolProcessor还是有所区别的:

  1. AbstractProcessor中的处理顺序是:先init,再getSupportedAnnotationTypes当前需要处理的注解;然后调用process处理注解;

  2. SymbolProcessor:先直接调用process处理;如果完成调用finish,否则调用onError做异常处理;

  3. AbstractProcessor判断是否完成全部处理的办法:在process方法有个参数RoundEnvironment,该参数提供processingOver方法判断当前处理是否完毕;

  4. SymbolProcessor如果也想要init初始化,那么可以在process方法中调用;

  5. AbstractProcessor的使用时继承当前类并且使用@AutoService(Processor::class)注解修饰该类;SymbolProcessor是继承该类,使用SymbolProcessorProvider的create实例化使用该ksp处理注解的类;

  6. SymbolProcessor方法中没有getSupportedAnnotationTypes方法(支持处理的注解),所以可以直接在process方法中定义需要处理的注解;

以上就相当于一个demo的讲解了,由于个人感觉不难,所有没这方面知识的读者可以自行去了解。

roomCompilerProcessing架构循序渐进

引子

如果按照以上demo的用法,我们以AbstractProcessor的使用为例:

如果需要处理的注解非常多,怎么办;而且每个(或者每一类)注解又有自己的意义,我们不可能所有的注解放一起,在getSupportedAnnotationTypes中一次性表达全部需要处理的全部注解,再在process一次性处理;

roomCompilerProcessing的诞生能很好解决这个问题,将不同的注解分类并且对当前分类处理,那么怎么做呢

甄别注解,并且对不同类型注解处理

以下手敲版,不要过于在意代码,主要是代码中溢出的杀气…咳咳咳,思想才是最重要的。

  1. 定义step接口用于收集某一类注解并且对该类注解处理:

     public interface Step{
     	//收集当前step处理的注解
     	public Set getAnnotations();
     
     	//处理当前收集到的注解集合的具体业务逻辑,返回当前被拒绝处理的节点
     	public List process(ProcessingEnv env,Set);	
     }
    
  2. 定义一个抽象类BasicProcessor继承AbstractProcessor,收集Step:

     public abstract BasicProcessor extends AbstractProcessor{
     	
     	List processingSteps();
     	
     	//预留一个注解处理完成的方法,继承者用就用,不用就算了
     	void postOverRound(MultableSet annotations,RoundEnvironment rounEnv){}
     
     	List steps;
     
     	final override void init(ProcessingEnvironment processingEnv){
     		super.init(processingEnv);
     		steps = processingSteps();
     	}
     
     	override Set getSupportedAnnotationTypes(){
    
     		steps.flatMap { it.annotations() }.toSet();
     	}
     
     	override boolean process(MultableSet annotations,RoundEnvironment rounEnv){
     		if (roundEnv.processingOver()) {
     			postOverRound(annotations,rounEnv);
     		}
     		steps.stream().forEach(step ->{
     			step.process();
     		});
     	}
     }
    
  3. Step实例StepInstall:

     public StepInstall interface Step {
     	override Set getAnnotations{
     		//表示当前收集的是Database注解
     		mutableSetOf(Database.qualifiedName)
     	}
     
     	override Set process(ProcessingEnv env,Set){
     		//当前处理的就是Database注解的业务场景
     		...
     		//当前被拒绝处理的节点
     		return rejectedElements;
     	}
     }
    
  4. AbsractProcessor实例:

     public RoomProcessor extends BasicProcessor {
     	
     	override List processingSteps(){
     		listof(StepInstall());
     	}
     
     	override SourceVersion getSupportedSourceVersion(){
     		return SourceVersion.latest();
     	}
     }
    

写的不是很好,但是那种高逼格意境表达出来了!!!

扩展:延迟或拒绝处理的节点

什么情况下注解处理的节点需要延迟处理或拒绝处理呢?

  1. 节点尚没有生成,又称为无效节点,需要延时处理;

  2. 在Step处理过程中,根据业务需求,被拒绝处理的节点;

揍一顿比方给大家看下:

  1. @Annotation1注解修饰的类,经过Step1处理生成了@Annotation2修饰的类。Step2刚好可以处理@Annotation2注解(如果这个@Annotation2修饰的节点是一个特定类,很重要,比如就叫SpecialCoolMan,在未生成这个SpecialCoolMan的情况下就属于无效节点)。

  2. 而又由于每个原因(Step2的process里有个业务场景,可能感觉这个@Annotation2修饰的当前类存在private修饰的变量,感觉它太特么小气,搞的我没面子,那我就不给他处理了,这个@Annotation2修饰的类就被叫做拒绝节点),当前Step2拒绝处理这个@Annotation2修饰的类。

对以上场景理解一番:

  1. 奔着尊重代码原则(那如果不尊重Step2在Step1前执行是否完全没问题呢,往下看有答案):Step1还是先一步执行,再去执行Step2;

  2. BasicProcessor类中有两个变量:(1)收集所有Step中被拒绝处理的节点;(2)收集所有Step中的无效节点;

  3. BasicProcessor process方法会依次执行Step process方法:(1)处理当前注解的业务逻辑;(2)之前积累的被Step拒绝的节点和无效节点是否使用了当前Step的注解,如果是则处理;再收集当前Step被拒绝的节点和无效节点。直到所有Step都处理完毕!

  4. 我们在BasicProcessor的process方法中根据RoundEnvironment.processingOver判断是否执行完毕,如果执行完毕,那么将所有无效节点和被Step拒绝处理的节点再次在所有Step中处理一遍,会产生两种情况:(1)全部处理完毕,万事大吉;(2)还存在无效节点和被拒绝处理节点那么只能报错了;

  • 这个步骤我们知道,其实Step的顺序是无所谓的,但是我们还是应该尊重一下次序,这样在处理过程中可能会快点!

代码实现

	public abstract BasicProcessor extends AbstractProcessor{
		 //收集无效节点
		private List deferredElement = new ArrayList();

		 //收集被Step拒绝处理的节点
		private Map> rejectedElementBySteps = new Map();
		
		
		List processingSteps();
		
		//预留一个注解处理完成的方法,继承者用就用,不用就算了
		void postOverRound(MultableSet annotations,RoundEnvironment rounEnv){}
	
		List steps;
	
		final override void init(ProcessingEnvironment processingEnv){
			super.init(processingEnv);
			steps = processingSteps();
		}
	
		override Set getSupportedAnnotationTypes(){

			steps.flatMap { it.annotations() }.toSet();
		}
	
		override boolean process(MultableSet annotations,RoundEnvironment rounEnv){

			//注解处理完毕
			if (roundEnv.processingOver()) {
				postOverRound(annotations,rounEnv);

				//处理完成后,对剩下的无效节点和拒绝节点再次在所有Step中处理一遍
				steps.forEach { step ->
					//和下面的雷同
					...
				}
			}

			steps.forEach(step ->{
				
				//前面无效节点,是否使用了当前step的注解,使用了,则返回这些节点信息
				Map> previousRoundDeferredElemens = getSetpElementsByAnnotation(step,deferredElement);


				//所有前面step拒绝处理的节点
				Map> stepDeferredElementsByAnnotation = getSetpElementsByAnnotation(step,rejectedElementBySteps被step拒绝处理的所有节点);

				//添加被Step拒绝节点
				rejectedElementBySteps.add(step,step.process(当前step注解使用的节点 + 前面拒绝节点(该节点使用当前step注解) + 前面无效节点(该节点使用了当前step注解)));

			});
		}

		//根据传递进来的节点集合,查找当前节点是否使用了当前step的注解,筛选当前注解修饰的节点集合
		private Map> getSetpElementsByAnnotation(Step step,Set typeElementNames){
			if (typeElementNames.isEmpty()) {
	            return emptyMap()//空
	        }

			Map> elementsByAnnotation = new HashMap();
			
			List stepAnnotations = step.annotations();

			typeElementNames.forEach { typeElement -> 

				typeElement.getAllAnnotations().map { it.qualifiedName }
				.forEach { annotationName
					if(stepAnnotations.contains(annotationName)){
						List elements = elementsByAnnotation.getKey(annotationName) ;
						if(elements == null){
							elements = new ArrayList();
							
						}
						elements.add(typeElement);
						elementsByAnnotation.put(annotationName,elements);
					}
				}
			}

			return elementsByAnnotation;
		}
	}

扩展:节点树状图

注解修饰的节点,该节点包含其他节点,节点又包含另外节点,形成一个树状结构。demo:一个注解修饰的类中包含包含变量和方法,变量又是一个类,方法可能包含泛型,另外还有方法使用了新的注解等等。如何体现这种树状结构。

如下所示,可以根据这个图自行往下深入了解:难度不大,花点心思即可。

13.从架构设计角度分析AAC源码-Room源码解析第2篇:RoomCompilerProcessing源码解析_第2张图片

上图不做解释,有几点有必要说明:

当前必须在理解如Message、Filer、Type、Element等类的基础上,即这里相当于基础类的X系列;

  • 为什么使用X系列,个人理解:(1)RoomCompilerProcessing处理Kapt和ksp两种编译方式,X系列相当于一个接口模式,kapt和ksp对接口的实现只需要继承X系列接口即可,具体实现根据自身kapt和ksp实际也无需求;(2)Message、Filter、Type基本类型可能后续会改成X系列(XMessage、XFiler);

kapt和ksp

ksp使用的是KspBasicAnnotationProcessor类;kapt使用的是KotlinBasicAnnotationProcessor。

KspBasicAnnotationProcessor类继承SymbolProcessor;KotlinBasicAnnotationProcessor继承了AbstractProcessor;

代码方面存在细微差别,但是业务逻辑是一致的,我们这里仅仅针对KotlinBasicAnnotationProcessor做简单讲解。

另:当前源码体积主要还是体现在节点树状图上面,其他代码体量不大。

KotlinBasicAnnotationProcessor源码简介:

  1. init:生成JavacProcessingEnv对象,该对象中存在大量操作节点和类型代码,节点和类型通过缓存处理,方便二次处理可直接获取(下面会简单介绍缓存用法);
  • XMessage的具体实现类是JavacProcessingEnvMessager,XFile的具体实现类是JavacFiler。使用了非常典型的模式:代理模式。
  1. getSupportedAnnotationTypes:通过processingSteps方法收集所有待处理的注解;

  2. process:使用CommonProcessorDelegate对象处理注解信息:

  • (1)对steps收集的注解(还有上个步骤拒绝处理节点和无效节点判断是否使用了当前注解,如果使用了则一并处理)步骤遍历处理;
  • (2)收集steps注解步骤中拒绝处理节点;
  • (3)收集steps注解步骤中的无效节点;
  • (4)根据RoundEnvironment的processingOver方法判断是否执行完毕,如果执行完毕,再对steps剩余的拒绝节点和无效节点再次遍历steps去处理;

扩展: 缓存

每次处理的节点都会根据节点名称去XTypeElementStore对象中查找,如果存在直接烦,否则根据当前节点名存储在XTypeElementStore对象中。

当前对象单独列出来,原因之一就是这里kotlin的写法,值得我们借鉴(kotlin我不是太熟悉,这个写法我感觉很酷)。当然原因之二是节点存取都绕不开当前存储对象。

总结

这里对源码的解说非常酸爽。因为结构性非常明显,代码也非常值得我们去揣摩。

相对注解有进一步了解的,可以去看Dagger源码系列解析

这里github源码地址,别忘了是room标签。

你可能感兴趣的:(源码解析,android,kotlin,android,studio)