从自定义Annotation、APT、JavaPoet再到Butterknife原理
发布日期:2021-08-25 15:35:20 浏览次数:15 分类:技术文章

本文共 19041 字,大约阅读时间需要 63 分钟。

一眨眼功夫又到周末,总觉得小日子过得不够充实(其实是王者荣耀不好玩了...)。

不过我可以跟Butterknife谈情说爱(RTFSC:Read The Fucking Source Code),本篇文章便是“爱的结晶” 。

1、Butterknife是什么?

Android大神JakeWharton的作品(其它还有OKHttp、Retrofit等)。Butterknife使用注解代替findViewById()等,可读性高、优雅、开发效率提高等。这里就不再列举Butterknife优点,相信各位老司机早就精通Butterknife使用。本篇主要学习Butterknife核心源码,万丈高楼平地起,直接杀入源码难免无头苍蝇,一脸懵逼,文章从以下几个部分聊起(瞎扯)。

  1. 自定义注解
  2. 反射解析注解
  3. APT解析注解
  4. JavaPoet生成Java源文件
  5. 浅析Butterknife源码

(强行插入表情包)

来不及解释了 快上车 滴... 学生卡。

Butterknife 基本使用姿势

1、在App build.gradle配置依赖

dependencies {    implementation 'com.jakewharton:butterknife:8.8.1'    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'}复制代码

2、在Activity使用

public class MainActivity extends AppCompatActivity {    @BindView(R.id.tv_name)    TextView tvName;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ButterKnife.bind(this);    }}复制代码

2、注解

上面看到使用非常简单,几个注解就搞定之前一大堆findViewById()等代码,那么它是怎么做到的?答案是:butterknife会扫描这些自定义注解,根据注解信息生成Java文件,ButterKnife.bind(this)实际会执行自动生成的代码。红色框选部分文章后面会详细分析,可以了解到 在读Butterknife源码之前得先回顾Java基础-注解。

目录:app\build\generated\source\apt\ (APT扫描解析 注解 生成的代码)。

注解

注解可以理解为代码的标识,不会对运行有直接影响。

1、内置注解

Java内置几个常用注解,这部分标识源码,会被编译器识别,提示错误等。

@Override 标记覆盖方法

@Deprecated 标记为过时

@SuppressWarnings 忽略警告

2、元注解

假设我们要自定义一个注解,这时候就需要用元注解去声明自定义注解,包括像注解作用域、生命周期等。

@Documented 可以被javadoc文档化。

@Target 注解用在哪

  1. CONSTRUCTOR 构造函数
  2. FIELD 域声明
  3. LOCAL_VARIABLE 局部变量
  4. METHOD 方法
  5. PACKAGE 包声明
  6. PARAMETER 参数
  7. TYPE 类、接口

@Inherited

  1. 允许子类继承父类注解

@Retention

  1. SOURCE:编译时剔除
  2. CLASS:在CLASS文件保留标识,运行时剔除
  3. RUNTIME 运行时保留标识

3、自定义注解

使用元注解创建自定义注解

//直到运行时还保留注解@Retention(RetentionPolicy.RUNTIME)//作用域 类@Target(ElementType.TYPE)public @interface BindV {    int resId() default 0;}复制代码

4、解析注解

在代码中定义了标识,现在想拿到这些信息,可以通过反射、APT获取注解的值。

1、反射

通过反射解析注解(这里也不阐述什么是反射)

BindV.java 文件

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface BindV {    int resId() default 0;}复制代码

MainActivity.java 文件

@BindV(resId = R.layout.activity_main)public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);                //这里是举个栗子反射当前类,设置布局文件,当然在实际中这部分代码可能是经过封装的。        Class clz = MainActivity.class;        BindV bindV = (BindV) clz.getAnnotation(BindV.class);//拿到布局文件id        setContentView(bindV.resId());    }}复制代码

2、APT

APT(Annotation Processing Tool)注解处理器,是在编译期间读取注解,生成Java文件。反射解析注解是损耗性能的,接下来通过APT来生成java源码,从而避免反射。

1、首先新建项目,再新建JavaLibrary。

JavaLibrary的Gradle配置,要想Java识别注解处理器,需要注册到META-INF,这里使用auto-service这个库实现自动注册。

apply plugin: 'java-library'dependencies {    implementation fileTree(dir: 'libs', include: ['*.jar'])    implementation 'com.google.auto.service:auto-service:1.0-rc2'}sourceCompatibility = "1.7"targetCompatibility = "1.7"复制代码

2、新建BindLayout注解

package com.example.abstractprocessorlib;public @interface BindLayout {    int viewId();}复制代码

3、新建AbstractProcessor子类

package com.example.abstractprocessorlib;@AutoService(Processor.class)public class MyProcessor extends AbstractProcessor {    private Types typesUtils;//类型工具类    private Elements elementsUtils;//节点工具类    private Filer filerUtils;//文件工具类    private Messager messager;//处理器消息输出(注意它不是Log工具)    //init初始化方法,processingEnvironment会提供很多工具类,这里获取Types、Elements、Filer、Message常用工具类。    @Override    public synchronized void init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);        typesUtils = processingEnvironment.getTypeUtils();        elementsUtils = processingEnvironment.getElementUtils();        filerUtils = processingEnvironment.getFiler();        messager = processingEnvironment.getMessager();    }    //这里扫描、处理注解,生成Java文件。    @Override    public boolean process(Set
set, RoundEnvironment roundEnvironment) { //拿到所有被BindLayout注解的节点 Set
elements = roundEnvironment.getElementsAnnotatedWith(BindLayout.class); for (Element element : elements) { //输出警告信息 processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "element name:" + element.getSimpleName(), element); //判断是否 用在类上 if (element.getKind().isClass()) { //新文件名 类名_Bind.java String className = element.getSimpleName() + "_Bind"; try { //拿到注解值 int viewId = element.getAnnotation(BindLayout.class).viewId(); //创建文件 包名com.example.processor. JavaFileObject source = filerUtils.createSourceFile("com.example.processor." + className); Writer writer = source.openWriter(); //文件内容 writer.write("package com.example.processor;\n" + "\n" + "import android.app.Activity;\n" + "\n" + "public class " + className + " { \n" + "\n" + " public static void init(Activity activity){\n" + " activity.setContentView(" + viewId + ");\n" + " }\n" + "}"); writer.flush(); //完成写入 writer.close(); } catch (IOException e) { e.printStackTrace(); } } } return false; } //要扫描哪些注解 @Override public Set
getSupportedAnnotationTypes() { Set
annotationSet = new HashSet<>(); annotationSet.add(BindLayout.class.getCanonicalName()); return annotationSet; } //支持的JDK版本,建议使用latestSupported() @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }}复制代码

2、在App的Gradle文件 添加配置

dependencies {    annotationProcessor project(path: ':AbstractProcessorLib')    implementation project(path: ':AbstractProcessorLib') }复制代码

3、在app\build\generated\source\apt\debug 目录下,可以看到APT 生成的文件。

可以在Activity使用刚才生成的文件

假设遇到这个错误,可以参考修改Grandle配置

Gradle配置

1、JavaPoet

从上面APT可以看到拼接Java文件是比较复杂的,好在Square开源了JavPoet这个库,不然整个文件全靠字符串拼接... 有了这个库生成代码,就像写诗一样。修改刚才process()方法

@Overridepublic boolean process(Set
set, RoundEnvironment roundEnvironment) { Set
elements = roundEnvironment.getElementsAnnotatedWith(BindLayout.class); for (Element element : elements) { processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "element name:" +element.getSimpleName(), element); if (element.getKind().isClass()) { String className = element.getSimpleName() + "_Bind"; try { int viewId = element.getAnnotation(BindLayout.class).viewId(); //得到android.app.Activity这个类 ClassName activityClass = ClassName.get("android.app", "Activity"); //创建一个方法 MethodSpec initMethod = MethodSpec.methodBuilder("init") .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//修饰符 .addParameter(activityClass, "activity")//参数 .returns(TypeName.VOID)//返回值 .addStatement("activity.setContentView(" + viewId + ");")//方法体 .build(); //创建一个类 TypeSpec typeSpec = TypeSpec.classBuilder(className)//类名 .addModifiers(Modifier.PUBLIC)//修饰符 .addMethod(initMethod)//将方法加入到这个类 .build(); //创建java文件,指定包名类 JavaFile javaFile = JavaFile.builder("com.example.processor", typeSpec) .build(); javaFile.writeTo(filerUtils); } catch (Exception e) { e.printStackTrace(); } } } return false;}复制代码

Butterknife 源码分析

先到gayhub 下载源码,butterknife、butterknife-annotations、butterknife-compiler三个核心模块,也是主要阅读的部分。

1、首先来分析一下build\generated\source\apt**\MainActivity_ViewBinding.java 这个文件生成大概过程,之前我们是用注解处理器生成代码,在butterknife-compiler模块的ButterKnifeProcessor.java类负责生成 ClassName_ViewBinding.java文件。 过程如下:

  • 扫描注解信息
  • 生成ClassName_ViewBinding.java文件
  • Butterknife.init()方法找到对于ViewBinding文件并执行绑定方法。

(为了方便快速阅读代码,把注释或代码强壮性判断移除)首先是init方法,没有过多复杂的,主要是工具类获取。

@Override  public synchronized void init(ProcessingEnvironment env) {   super.init(env);   elementUtils = env.getElementUtils();   typeUtils = env.getTypeUtils();   filer = env.getFiler();   trees = Trees.instance(processingEnv); }复制代码

getSupportedSourceVersion、getSupportedAnnotationTypes方法。

@Override public SourceVersion getSupportedSourceVersion() {  return SourceVersion.latestSupported();}@Overridepublic Set
getSupportedAnnotationTypes() { Set
types = new LinkedHashSet<>(); for (Class
annotation : getSupportedAnnotations()) { types.add(annotation.getCanonicalName()); } return types;}private Set
> getSupportedAnnotations() { Set
> annotations = new LinkedHashSet<>(); annotations.add(BindAnim.class); annotations.add(BindArray.class); ... return annotations;}复制代码

接下来重点是process方法

@Override public boolean process(Set
elements, RoundEnvironment env) { Map
bindingMap = findAndParseTargets(env); for (Map.Entry
entry : bindingMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingSet binding = entry.getValue(); JavaFile javaFile = binding.brewJava(sdk, debuggable); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); } } return false;}复制代码

分析findAndParseTargets(env)这行,找到这个private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env)这个方法核心部分

省略...// Process each @BindView element.for (Element element : env.getElementsAnnotatedWith(BindView.class)) {  // we don't SuperficialValidation.validateElement(element)  // so that an unresolved View type can be generated by later processing rounds  try {    parseBindView(element, builderMap, erasedTargetNames);  } catch (Exception e) {    logParsingError(element, BindView.class, e);  }}省略...复制代码

继续往下找 parseBindView()方法

private void parseBindView(Element element, Map
builderMap, Set
erasedTargetNames) { //当前注解所在的类 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //校验类、要绑定的字段 修饰符 private static,不可以在Framework层使用已java. android.开始的包名 boolean hasError = isInaccessibleViaGeneratedCode(BindViews.class, "fields", element) || isBindingInWrongPackage(BindViews.class, element); // 要绑定的字段是否View的子类、或接口 TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } Name qualifiedName = enclosingElement.getQualifiedName(); Name simpleName = element.getSimpleName(); if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { if (elementType.getKind() == TypeKind.ERROR) { note(element, "@%s field with unresolved type (%s) " + "must elsewhere be generated as a View or interface. (%s.%s)", BindView.class.getSimpleName(), elementType, qualifiedName, simpleName); } else { error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindView.class.getSimpleName(), qualifiedName, simpleName); hasError = true; } } //校验不通过 if (hasError) { return; } // Assemble information on the field. //保存View ID之间映射 int id = element.getAnnotation(BindView.class).value(); BindingSet.Builder builder = builderMap.get(enclosingElement);//从缓存取出来 Id resourceId = elementToId(element, BindView.class, id); if (builder != null) {//说明之前被绑定过 String existingBindingName = builder.findExistingBindingName(resourceId); if (existingBindingName != null) { error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", BindView.class.getSimpleName(), id, existingBindingName, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } } else { //构建要绑定View的映射关系,继续看getOrCreateBindingBuilder方法信息 builder = getOrCreateBindingBuilder(builderMap, enclosingElement); } String name = simpleName.toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); //我们Activity类有很多 BindView注解的控件,是以类包含字段,这里的builder相当于类,判断如果类存在就往里加字段。 builder.addField(resourceId, new FieldViewBinding(name, type, required)); // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement);}//直接看newBuilderprivate BindingSet.Builder getOrCreateBindingBuilder( Map
builderMap, TypeElement enclosingElement) { BindingSet.Builder builder = builderMap.get(enclosingElement); if (builder == null) { builder = BindingSet.newBuilder(enclosingElement); builderMap.put(enclosingElement, builder); } return builder; } //相当于创建一个类,类名是一开始看到的 类名_ViewBinding static Builder newBuilder(TypeElement enclosingElement) { TypeMirror typeMirror = enclosingElement.asType(); boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE); boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE); boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE); TypeName targetType = TypeName.get(typeMirror); if (targetType instanceof ParameterizedTypeName) { targetType = ((ParameterizedTypeName) targetType).rawType; } String packageName = getPackage(enclosingElement).getQualifiedName().toString(); String className = enclosingElement.getQualifiedName().toString().substring( packageName.length() + 1).replace('.', '$'); ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding"); boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL); return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);} 回到process方法,这行就是将刚扫描的信息写入到文件JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);复制代码

至此我们知道*_ViewBinding文件生成过程,接下来看怎么使用,从Butterknife.bind()方法查看

public static Unbinder bind(@NonNull Activity target) {    View sourceView = target.getWindow().getDecorView();//拿到decorView    return createBinding(target, sourceView);}复制代码

进入createBinding()方法

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {    Class
targetClass = target.getClass(); if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName //拿到类_ViewBinding的构造方法 Constructor
constructor = findBindingConstructorForClass(targetClass); if (constructor == null) { return Unbinder.EMPTY; } //noinspection TryWithIdenticalCatches Resolves to API 19+ only type. try { return constructor.newInstance(target, source);//执行构造方法。 } catch (IllegalAccessException e) { throw new RuntimeException("Unable to invoke " + constructor, e); } catch (InstantiationException e) { throw new RuntimeException("Unable to invoke " + constructor, e); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } if (cause instanceof Error) { throw (Error) cause; } throw new RuntimeException("Unable to create binding instance.", cause); } }复制代码

继续看findBindingConstructorForClass()方法,根据当前类先从缓存找构造方法,没有的话根据类名_ViewBinding找到构造方法。

private static Constructor
findBindingConstructorForClass(Class
cls) { Constructor
bindingCtor = BINDINGS.get(cls); if (bindingCtor != null) { if (debug) Log.d(TAG, "HIT: Cached in binding map."); return bindingCtor; } String clsName = cls.getName(); if (clsName.startsWith("android.") || clsName.startsWith("java.")) { if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search."); return null; } try { Class
bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding"); //noinspection unchecked bindingCtor = (Constructor
) bindingClass.getConstructor(cls, View.class); if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor."); } catch (ClassNotFoundException e) { if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName()); bindingCtor = findBindingConstructorForClass(cls.getSuperclass()); } catch (NoSuchMethodException e) { throw new RuntimeException("Unable to find binding constructor for " + clsName, e); } BINDINGS.put(cls, bindingCtor); return bindingCtor; }复制代码

//到这一步我们清楚,是先生成文件,然后Butterknife.bind()方法关联生成的文件并执行构造。接下来看看生成的文件构造做了什么 //我在apt目录下找到一个文件SimpleActivity_ViewBinding.java文件,代码量多 我简化一下,直接看findRequireViewAsType()方法。

@UiThread  public SimpleActivity_ViewBinding(SimpleActivity target) {    this(target, target.getWindow().getDecorView());  }  @UiThread  public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {    this.target = target;    View view;    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);  }    //继续看findRequiredView  public static 
T findRequiredViewAsType(View source, @IdRes int id, String who, Class
cls) { View view = findRequiredView(source, id, who); return castView(view, id, who, cls); } //findRequiredView()方法,可以看到其实还是使用findViewByID()查找View只不过是自动生成。 public static View findRequiredView(View source, @IdRes int id, String who) { View view = source.findViewById(id); if (view != null) { return view; } String name = getResourceEntryName(source, id); throw new IllegalStateException("Required view '" + name + "' with ID " + id + " for " + who + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'" + " (methods) annotation."); }复制代码

//以上就是Butterknife基本原理,第一部分apt扫描注解信息生成文件,第二部分Butterknife.bind方法找到对于文件,执行构造,并执行findView获取view。

推荐一篇文章:Android主项目和Module中R类的区别 https://www.imooc.com/article/23756

最后:其实在MVVM、Kotlin等出来之后,Butterknife热度慢慢下降,但这根本不影响我们去了解 这如此优秀的框架。

炮友们有问题,请留言 感谢!

关于作者 Android-张德帅

  • 1996年,就读于德国慕尼黑特种兵学校。
  • 1998年,在美国宾夕法尼亚大学心理系进修。
  • 2000年,加入海波突击队。
  • 2003年,攻破日本情报系统,获取10份绝密文件,令其战争阴谋破产。
  • 2005年,前往叙利亚执行任务,成功解救三千人质。
  • 2006年,获得诺贝尔和平奖提名。
  • 2008年,参加美国总统选举,以一票只差落选。
  • 2011年,被奥巴马跪请回到海波突击队,同年击毙拉登。
  • 2015年,被提名为全球最有影响力人物。
  • 2018年,放弃一生荣誉加入"天星技术团队",持续更新文章。

参考资料

  1. Butterknife: https://github.com/JakeWharton/butterknife
  2. 《Java编程思想 第四版》: https://item.jd.com/10058164.html
  3. Auto-Service: https://github.com/google/auto-service (上面什么都没有,奇怪) 'com.google.auto.service:auto-service:1.0-rc2'
  4. Java注解处理器:https://race604.com/annotation-processing/
  5. JavaPoet 看这一篇就够了:https://juejin.im/entry/58fefebf8d6d810058a610de/

转载地址:https://blog.csdn.net/weixin_33858336/article/details/88001084 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Quick-Task 动态脚本支持框架之使用介绍篇
下一篇:JavaEE 微信支付

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年02月28日 03时17分37秒