前言
公司项目要开发自己的路由模块,在研究了已有的开源库和进行模拟之后,选择使用编译时注解来开发,好处主要是对已有的代码入侵程度最小也更灵活.之前一直都用别人现有的注解类的库直接开发,虽然知道一些原理,但是毕竟没有实践经验,理解并不深.结合这次的机会,对开发流程做一个记录.
像我这么懒的人,写文章其实也有一点无奈.之前看过一些趣文,也看过斯坦福的 Android 开发公开课,米国中情局的内部黑客培训教程,从最开始的环境搭建开始讲,中间会细致到使用软件中的某个步骤的按钮在哪里都会标出来,清清楚楚,斯坦福的教授讲解时用的 eclipse 开发,跳转代码的快捷键是什么都会跟同学说,还会说忘记快捷键应该怎么操作.然后呢,就不得不吐槽一下网络上搜到的技术文的文风,经常是一个开发流程可能分为 ABC 三个步骤,博文直接从 B 开始讲了,B 怎么来的,原理是什么,也没有说明,整个文章看下来中间也有代码,也有截图,但是就是感觉无从下手,没办法得到博主的结果,感觉很无厘头.看了还不如不看,本来是带着一个 怎么弄 的问题去搜文章,看完了脑子里全变成了 为什么.
理论
主要是参考了 codeKK -- 公共技术点之 Java 注解 Annotation 这篇文章,对不太了解的关键词进行了搜索查阅,同时结合 butterknife 和 EventBus 两个开源库的源码帮助自己更快的理解上手.
关于理论这方面每个人的理解能力不一样,我通常是在脑海里给自己做类比或者建模,自己的讲解可能会让人更困扰,就不在这里多说了,推荐的文章说的很详细,我也未必讲解的比别人更清楚.
需要说明的是不管是 butterknife 使用的编译时注解,还是 EventBus 使用的运行时注解都会用到反射技术,所以注解的应用还是看开发项目时的取舍.
分析
编译时注解,主要的开发工作是对 自定义的注解 进行 编译时 处理,这么说明可能有点绕口,下面会用一个完整的示例来解释这句话,现在需要知道三点:
- 定义 注解
- 处理 注解
- 操作 处理结果
工具
AndroidStudio 最新稳定版本(3.0.1)即可
开发
- 新建一个 Android 项目,按照提示新建一个空 activity,然后一路点击 next 即可
-
在工具栏,选择 file -> new -> new Module... 在弹出框中选择 Java Library ,新建一个用来编写注解的java库工程 --
router-annotations
-
新建一个注解,其中
@Target
表示该注解可以用在哪些元素上,这里标记的是Type
也就是 类 (稍后会看到使用),@Retention
注解保留策略,编译时注解使用RetentionPolicy.CLASS
,给这个注解定义了一个默认值为heheda
的参数成员name()
-
同步骤2,新建一个进行注解处理的java库工程 --
router-compiler
-
这时候可能就会开始有为什么了
- 为什么要建两个库工程
- 这两个工程有什么区别
问题1:我们先来看看 butterknife 的使用说明, 依赖声明中添加了两个声明
compile 'com.jakewharton:butterknife:8.8.1' // api库 annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' //字面意思是注解处理器
如果我们不引用这个注解处理器的库, 只引用 api 这个库,构建之后,在项目的依赖目录下和引用注解处理器库是没有任何区别的,并没有增加任何信息, 编码的时候也可以各种正常使用,比如调用api库里面的类和注解进行编码, 但是项目运行之后,使用了 butterknife 注解的变量就会报空指针异常,这说明了两个问题:首先注解处理器库不管里面有什么不会打到包里面去,因为看不到任何类;其次,它是用来处理注解的,什么时候处理,绕来绕去其实就是说 注解处理器在项目编译时处理了注解 ,然后它就不再参与打包流程.废话说的有点多有点绕,可以自行搜索
annotationProcessor
加强个人的理解.
那么问题2:步骤2和步骤4建的项目有什么区别,首先既然都是 java library 那最后能提供的都是jar,问题缩小到jar之间有什么区别,我就不贴图了,直接按照我的理解说,jar包除了提供编译好的类(在包目录下)之外,还有一个 META-INFO 的目录,有一个 MANIFEST.MF 文件(其实是可有可无的), 但是 能声明为annotationProcessor
的jar在该目录下会多出一个 services 的目录,里面有一个固定为 javax.annotation.processing.Processor 的文件,文件中会一行行列出声明为注解处理器的类的路径,方便编译器加载类.(在这说一下,有些文章说 "继承自AbstractProcessor(下面会说到)的类会自动参与编译时注解处理" 我觉得有点问题,如果所有继承自AbstractProcessor的类会自动参与注解处理,难道编译器会遍历一个个的子类加到编译器中?) 我没有继续做深入的理论研究,现在我们只需要知道这么多,上面的话引出另一个问题,如何才能生成上面说了那么多废话的目录,文件,以及文件内容.很显然如果是固定套路就应该是一个自动化的流程,google为我们提供了相关库,把下面的依赖信息添加到router-compiler
的依赖声明中compile project(':router-annotations') compile 'com.google.auto.service:auto-service:1.0-rc2' //提供 @AutoService() 注解
第一个依赖是让
router-compiler
可以调用到刚刚在router-annotations
中生成的注解,第二个依赖就是用来解决上面所说的问题的, google开发的这个库可以在编译jar包的时候把上面所说的信息自动生成(也是用的注解机制) 然后就是对刚刚定义好的
@Test
注解进行处理的编码工作,定义一个AbstractProcessor
的子类, 加上@AutoService(Processor.class)
该注解来自上面说到的google提供的包
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
}
- 重写相关的方法,首先是java版本,其次是要告诉编译器 -- 本处理器能处理哪些注解,当前只有一个注解,所以我们返回刚才声明的
@Test
/**
*
* @return 声明java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
/**
*
* @return 支持处理的注解集合
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(Test.class.getCanonicalName());
return annotations;
}
- 重写
process
函数,开始我们的处理逻辑.返回值true
表示该注解已经被处理完了,不需要继续传递给其他注解处理器( AbstractProcessor 的子类),false
相反,会继续传递
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
-
@Test
的作用域声明为Type
所以我们处理CLASS
种类的元素
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set.isEmpty()) {
return false;
}
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Test.class);
try {
processTestAnnotationClass(elements);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
private void processTestAnnotationClass(Set<? extends Element> elements) throws IOException {
for (Element classElement : elements) {
switch (classElement.getKind()) {
case CLASS:
//todo 处理逻辑
generateClassAnnotation(classElement);
break;
}
}
}
- 这里我的处理的逻辑是在 指定的包 下面生成一个 指定名称 的java类,并且重写
toString()
函数,返回一些信息. 关于动态编写代码,这里使用的是com.squareup:javapoet:1.9.0
库 (相关用法可以查阅对应的文档, 刚接触不是很了解, 使用该库的相关api,可以免去自己编写 import 的语句,还是很方便的),同时我希望返回的信息是有结构的,所以这里也引用了 Android 里面使用的 apache 的 json 库,
compile 'com.squareup:javapoet:1.9.0'
compile 'org.json:json:20160810'
添加到 router-compiler
的依赖声明中.这里的返回信息是被注解的类的路径和声明 @Test
注解时添加的 name()
成员信息 (其实查阅相关api可以直接用其他方式直接返回 xxxActivity.class,这里只是为了演示获取 被注解的类的相关信息 的用法)
private void generateClassAnnotation(Element classElement) throws IOException {
/* 获取到注解对象,可以读取定义的信息 */
Test test = classElement.getAnnotation(Test.class);
/* 填充要记录的信息 */
Map<String, Object> params = Maps.newHashMap();
//获取被注解的 class 的完整路径
params.put("p", ClassName.get((TypeElement) classElement).reflectionName());
//获取标记注解时输入的信息
params.put("n", test.name());
//以下类信息来自 com.squareup:javapoet:1.9.0 主要用来动态生成代码,推荐使用
//方法生成器
MethodSpec.Builder toStringBuilder = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(javax.lang.model.element.Modifier.PUBLIC)
.returns(String.class)
.addStatement("return $S", new JSONObject(params).toString());
//类生成器
TypeSpec typeSpec = TypeSpec.classBuilder(classElement.getSimpleName().toString())
.addJavadoc("DO NOT EDIT THIS FILE!")
.addModifiers(javax.lang.model.element.Modifier.PUBLIC)
.addMethod(toStringBuilder.build())
.build();
}
- 要生成的类和重写函数都有了,这个时候就要开始生成java文件了,重写
AbstractProcessor
的init
函数,从编译环境参数中获取Filer
对象,该对象可以在编译时写文件,将类和函数组装在一起,使用JavaFile
就可以写文件了, 第一个参数是 10 里面说到的 指定的包 路径声明,第二个参数是java类的信息(包含类定义和重写的toString()
函数),最后通过Filer
输出文件
/**
* 编译时可以做写操作的封装类
*/
private Filer mFiler; // File util, write class file into disk.
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnvironment.getFiler();
}
private void generateClassAnnotation(Element classElement) throws IOException {
/* 获取到注解对象,可以读取定义的信息 */
Test test = classElement.getAnnotation(Test.class);
/* 填充要记录的信息 */
Map<String, Object> params = Maps.newHashMap();
//获取被注解的 class 的完整路径
params.put("p", ClassName.get((TypeElement) classElement).reflectionName());
//获取标记注解时输出的信息
params.put("n", test.name());
//以下类信息来自 com.squareup:javapoet:1.9.0 主要用来动态生成代码,推荐使用
//方法生成器
MethodSpec.Builder toStringBuilder = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(javax.lang.model.element.Modifier.PUBLIC)
.returns(String.class)
.addStatement("return $S", new JSONObject(params).toString());
//类生成器
TypeSpec typeSpec = TypeSpec.classBuilder(classElement.getSimpleName().toString())
.addJavadoc("DO NOT EDIT THIS FILE!")
.addModifiers(javax.lang.model.element.Modifier.PUBLIC)
.addMethod(toStringBuilder.build())
.build();
//往 com.ll.support.router.table 包下面通过 mFiler 写 java 文件
JavaFile.builder("com.ll.support.router.table", typeSpec).build().writeTo(mFiler);
}
- 验证结果.添加相关的依赖到 Android
app
工程,注解处理器需要对应的声明annotationProcessor project(':router-compiler')
,但是这里的工程用到了kotlin(包括纯kotlin和java混合kotlin),所以这里是使用的是kapt
.注解处理器里面 jdk 声明的是 7, 所以标记开发环境是 java7, 然后进行 gradle 同步,依赖构建完成之后,在app
工程中的 MainActivity 类声明我们的注解@Test
,最后进行 build -> reBuild 或者直接 run
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.ll.router"
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
kapt project(":router-compiler")
compile project(":router-annotations")
}
- 成功之后打开 app\build\generated\source\kapt\debug\com\ll\support\router\table 目录(如果使用的
annotationProcessor
声明,那 kapt替换成apt目录),就可以看到我们动态生成的java类了,同时查看我们的 apk 文件,也可以在 10 里面的 指定的包 下面找到我们的类.
这里面没有对分析的第三步进行讲解 -- 操作处理结果.这个地方每个人的需求不同,就不再展开了,类似于 butterknife 自己开发相关 api 处理库就可以了.
开发流程就是上面所说的,掌握了这些操作之后,剩下的就是各种脑洞了.最后说一下在实践中会遇到的非理论性问题:经常是刚刚创建了 module 但是 Android studio 无法识别,我的解决办法是,在 settings.gradle
文件中删掉 module 声明,然后同步,再加上 module 声明再同步,或者干脆退出重新打开 studio,多试几次之后才会好,具体原因还不知道,世界上总有那么多未解之谜,只要中间不迷惑走到头就行了.另外这一招也可以用在重命名项目上.