前言
本文是IOC系列文章的第五篇,也是最后一篇,也是最重要的一篇。之所以说最重要,是因为掌握自定义注解解析器是所有Android架构师必备的技能,没有一个Android架构师说自己不会自定义注解解析器的,另外掌握注解解析器更加有助于我们理解那些优秀的开源框架,像Retrofit、EventBus和Dagger2等等。本文将详细给大家带来关于自定义注解解析器的知识。
APT的工作流程
在上一篇文章Butterknife源码全面解析的最后简单介绍了一下APT技术,这里我再给大家讲讲APT的工作流程。在代码编译阶段(javac),会扫描所有AbstractProcessor的已注册的子类,并且会调用其process()方法,在该方法中我们可以解析注解并生成java文件,然后在程序调用我们生成的代码即可,其大致流程如下图所示
对我们开发者来说,最核心的目的就是实现AbstractProcessor并生成相关代码。
AbstractProcessor
实现自定义注解必须要掌握的就是AbstractProcessor,它是虚处理器,运行在单独的JVM中,使用AbstractProcessor对象必须要有javax环境。
AbstractProcessor有四个重要的方法:
init(ProcessingEnvironment processingEnvironment):会被注解工具所调用,并传入ProcessingEnvironment参数,通过ProcessingEnvironment参数我们可以拿到Filer和Messager等工具类,Filer看名字就知道是文件,生成java代码的时候使用,Messager是用来输出日志信息的。
process(Set<? extends TypeElement> annotations, RoundEnvironment env) :AbstractProcessor中最重要的方法,相当于main()函数,通过RoundEnvironment参数我们可以获得所有被注解标注的程序元素(包、类、成员变量、构造方法、成员方法...),在这里我们可以解析这些程序元素并生成java文件
getSupportedAnnotationTypes():需要解析的注解集合,这里返回的Set<String>,这里可以用@SupportedAnnotationTypes()来代替
getSupportedSourceVersion:获得支持的java版本,一般直接返回SourceVersion.latestSupported(),同样可以用@SupportedSourceVersion注解来代替
注册AbstractProcessor
写好了AbstractProcessor我们必须要注册才会生效,如何注册呢?我们必须把注解解析器打包到jar包,需要在META-INF/services路径下生成一个javax.annotation.processing.Processor文件,并在该文件中生成你所声明的Processor对象,是不是听起来很麻烦,其实做起来也很麻烦。谷歌爸爸为了方便广大开发者,特意开发了auto-service库,我们只需要在项目中引入auto-service库,并且在我们声明的
Processor类上面使用@AutoService(Processor.class)即可
@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {
private Filer mFiler;//文件类
private Messager mMessager;//打印错误信息
private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//绑定的view集合
注:这里需要注意的使用gradle版本大于5.0以上
#Sun Apr 26 15:32:47 PDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
在gradle5.0以上会自动忽略auto-service,所以在引入的时候我们需要
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'(必须要加上这行代码,否则无法注册成功)
接着,我们可以编译一下项目,然后打开build下面的classes
如图所示,即代表注册成功了
JavaPoet
当我们解析好了注解的程序元素以后就需要生成java文件,一行行手打代码就会很麻烦,java很贴心地推出了JavaPoet开源库,我们只需要引入这个类库即可。它可以很帮助开发者很轻松地生成需要的代码,直接放上几行代码给大家看一下效果
引入类库
implementation 'com.squareup:javapoet:1.12.1'
FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成员变量
MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法对象
.addModifiers(Modifier.PUBLIC)//方法的修饰符
.addParameter(className, paramName)//方法中的参数,第一个是参数类型,第二个是参数名
.addCode(builder.build())//方法体重的代码
.build();
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//类对象,参数:类名
.addMethod(methodSpec)//添加方法
.addField(fieldSpec)//添加成员变量
.build();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile对象,最终用来写入的对象,参数1:包名;参数2:TypeSpec
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
具体用法这里不多说了,JavaPoet使用起来很简单,最好就是先去GitHub上看一下官方文档,接着写一个Hello JavaPoet就能掌握了,送上github地址 JavaPoet
使用Android Studio搭建IOC项目
了解了AbstractProcessor和JavaPoet我们开始搭建IOC项目中,刚才介绍AbstractProcessor时提到了,其必须要有javax环境,而我们默认的Android Module中没有javax,所以必须要建立一个java library要来处理annotation-processor(注解解析器),另外还需要再创建一个java library来处理annotation(注解),这里之所以要把annotation-processor和annotation分开,是因为annotation-processor我们只有在编译期才用到,所以不必要把annotation-processor的相关代码打入到APK中,这里我们通过annotationProcessor方式依赖即可。关于Android module和java library的依赖关系是Android module依赖annotation-processor和annotation,其中annotation-processor又依赖annotation,因为要通过annotation-processor解析annotation中的注解,一图胜千文
注:Demo的github地址在文章的最后
通过自定义注解实现Butterknife的BindView功能
在上一篇文章Butterknife源码全面解析中给大家介绍了Butterknife的源码和实现思路,如图
这里我们仿照Butterknife的思路,首先在annotation中声明一个@TestBindView注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface TestBindView {
int value();
String SUFFIX = "_TestBinding";
}
然后我们在自定义的BindingProcessor中的init()方法中初始化,Filer和Messager对象
@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {
private Filer mFiler;//文件类
private Messager mMessager;//打印错误信息
private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//绑定的view集合
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnvironment.getFiler();//初始化文件对象
mMessager = processingEnvironment.getMessager();//初始化信息对象
}
注意:这里Messager对象需要说一下,之所以出现错误要使用Messager对象打印出来,而不采取我们传统的try catch方法是因为往往注解解析器出错会引发一大堆的异常,这个时候如果用try catch会导致我们的异常信息特别多,定位问题很麻烦,而如果使用Messager就会让错误信息简单明了。
下面就来了解析注解并且生成java文件的核心代码了,这里我想讲一下我的思路:
- 通过process()方法里面的RoundEnvironment对象拿到所有被我们目标注解(TestBindView)注解的程序元素,然后遍历
2.对程序元素进行分类,用一个Map容器,其中key是TypeElement(类元素,这里就代表我们的Activity),value是一个ViewInfo集合,ViewInfo包含viewId和viewName
3.拿到第二步的Map容器,开始利用JavaPoet生成代码,我们这里的逻辑很简单就是声明一个后缀,通过Activity的名字拼接后缀作生成的文件名,然后在构造方法里面调用findViewById()方法
下面直接上代码
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(TestBindView.class);//获取TestBindView注解的所有元素
for (Element element : elements) {//遍历元素
VariableElement variableElement = (VariableElement) element;//因为注解的作用域是成员变量,所以这里可以直接强转成 VariableElement
Set<Modifier> modifiers = variableElement.getModifiers();//权限修饰符
if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {//类型检查
mMessager.printMessage(Diagnostic.Kind.ERROR, "成员变量的类型不能是PRIVATE或者PROTECTED");
return false;
}
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//获得外部元素对象
sortToAct(typeElement, variableElement);//以类元素进行分类
}
writeToFile();
return false;
}
第二步骤分类逻辑
/**
* 把view信息跟activity关联在一起
*/
private void sortToAct(TypeElement typeElement, VariableElement variableElement) {
List<ViewInfo> viewInfos;
if (bindViews.get(typeElement) != null) {//判断之前是否存储过这个typeElement的ViewInfo集合
viewInfos = bindViews.get(typeElement);
} else {
viewInfos = new ArrayList<>();
}
TestBindView annotation = variableElement.getAnnotation(TestBindView.class);//拿到注解
int viewId = annotation.value();//获取viewId
String viewName = variableElement.getSimpleName().toString();//获取viewName
ViewInfo viewInfo = new ViewInfo(viewId, viewName);//生成viewinfo对象
viewInfos.add(viewInfo);//放入集合
bindViews.put(typeElement, viewInfos);//存入map中
}
第三步骤生成代码
/**
* 生成文件
*/
private void writeToFile() {
Set<TypeElement> typeElements = bindViews.keySet();
String paramName = "target";
for (TypeElement typeElement : typeElements) {
ClassName className = ClassName.get(typeElement);//获取参数类型
PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();//获得外部对象
String packageName = packageElement.getQualifiedName().toString();//获得包名
List<ViewInfo> viewInfos = bindViews.get(typeElement);
CodeBlock.Builder builder = CodeBlock.builder();//代码块对象
for (ViewInfo viewInfo : viewInfos) {
//生成代码
builder.add(paramName + "." + viewInfo.getViewName() + " = " + paramName + ".findViewById(" + viewInfo.getViewId() + ");\n");
}
FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成员变量
MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法对象
.addModifiers(Modifier.PUBLIC)//方法的修饰符
.addParameter(className, paramName)//方法中的参数,第一个是参数类型,第二个是参数名
.addCode(builder.build())//方法体重的代码
.build();
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//类对象,参数:类名
.addMethod(methodSpec)//添加方法
.addField(fieldSpec)//添加成员变量
.build();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile对象,最终用来写入的对象,参数1:包名;参数2:TypeSpec
try {
javaFile.writeTo(mFiler);//写入文件
} catch (IOException e) {
e.printStackTrace();
}
}
注意:以上代码都写入了详细的注释,这里有有两点要说一下
第一:获得被标注的元素注解以后我们要判断一下它的修饰符类型,不能是private或者protected,因为我们要通过Activity对象访问它的控件对象,这一点跟Butterknife是一致的
第二:就是Element.getEnclosingElement()这个方法很重要,是获取元素的封闭元素。啥叫封闭元素呢?举个例子,就是如果你是一个VariableElement(成员变量元素),把你封闭起来的就是TypeElement(类元素);如果你是一个TypeElement的,把你封闭起来的就是PackageElement(包元素);如果你是一个PackageElement,那么返回的就是null了,因为没有东西把包封闭起来。我们注解的作用域是成员变量,所以我们直接拿到VariableElement,然后再通过VariableElement就可以拿到TypeElement和PackageElement,这对我们后面来生成代码非常重要。
BindingProcessor里面的代码写完以后,我们在App module下写一个工具类用来加载生成的java文件同时调用其构造方法
public class ViewBindUtil {
/**
* 绑定Activity
* */
public static void bind(Activity activity) {
if (activity == null) {
return;
}
String activityName = activity.getClass().getName();//获取类的全限定名
ClassLoader classLoader = activity.getClass().getClassLoader();//获得类加载器
try {
Class<?> loadClass = classLoader.loadClass(activityName + TestBindView.SUFFIX);//加载类
Constructor<?> constructor = loadClass.getConstructor(activity.getClass());
constructor.newInstance(activity);//调用其构造方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
下面我们用一下咱们写的注解解析器
public class MainActivity extends AppCompatActivity {
@TestBindView(R.id.button)
Button button;
@TestBindView(R.id.textView)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBindUtil.bind(this);
button.setText("按钮");
textView.setText("文本");
}
}
编译一下项目,先看看生成的代码,有木有问题
最后运行一下,发现一切OK,这里就不贴图,这样一个简单的TestBindView注解解析器就完成了,本文的重点是不是这个例子,而且教大家如何完成一个注解解析器,以后再有模板类的代码咱们都可以考虑使用这种方法简化,同时对一些优秀的开源如何用注解实现的代码也会更加清楚。
如何调试Processor中的代码
考虑到大家刚开始使用注解解析器难免会存在一些问题,所以如何调试Processor中的代码还是很有必要讲一讲。我们运行时的代码都会调用,点一下绿色的🕷即可,而调试Processor的代码略微麻烦一点点。
完成前面四步,接下来
在Processor代码里打好断点
最后一步,切换成app,点击运行
这里还有一个很重要的点需要强调一下,除了第一次运行外,其余每次运行都必须要在app module下改点代码(哪怕是加个空格),否则,调试不会生效,切记!!!
总结
本文是系列文章IOC(依赖控制翻转)的最后一篇,终于打完收工了,如开篇所说掌握自定义注解解析器非常重要,未来大家要给公司搭建一些项目架构,难免会用到这项技术。本文首先介绍了AbstractProcessor和JavaPoet知识,然后交了如果使用AS搭建一个ioc项目,最后用了一个仿Butterknife的例子手撸一个自定义注解解析器的demo。
最后的最后,放上Demo的github地址:apt_demo,如果您觉得本文还不错,记得给个赞,谢谢~