APT-自定义ButterKnife

177 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

ButterKnife是用来解放开发者双手的依赖库,该库内通过注解替换了findViewById的操作,进一步提升开发人员编码效率,下面我们来看下,如果要实现一个ButterKnife(仅以findViewById为例,onClick等类型由大家自行探索),我们需要做些什么?

findViewById的实现要素

假设TestActivity的布局文件如下:

 <?xml version="1.0" encoding="utf-8"?>
 <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".TestActivity">
 ​
     <TextView
         android:id="@+id/textview1"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Hello"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 ​
 </android.support.constraint.ConstraintLayout>

那么在Activity中引用TextView时,其实现代码如下所示:

 public class TestActivity extends AppCompatActivity {
 ​
     private TextView mTextView;
 ​
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_test);
         mTextView = findViewById(R.id.textview1);
     }
 }

可以看到如果要自动完成findViewById的操作,我们主要要完成两项工作:

  • 让layout中定义的ResId和java代码中定义的成员具有映射关系,一一对应
  • 当遍历映射关系组时,调用activity.findViewById为Activity中定义的成员变量赋值,自动完成类成员的初始化

下面我们一起来看下,如何解决这两个问题。

resID-类成员映射关系

从前文可知,自定义注解可以携带value信息,那么我们只需要自定义一个注解,该注解的目标为成员属性,接受的取值为resId,当开发者在成员变量上,使用该注解时,即可搭建成员变量和resID之间的一一映射关系。

新建ButterKnifeDemo项目,在项目内创建命令为butterknife-annotation的java模块,在该模块内创建BindView注解,由于我们要求BindView注解的取值必须是资源id,所以我们可以引入androidx.annotation库借助其内部注解做限制,引入库的代码如下图所示:

1-7-3-6

随后编写BindView注解代价如下图所示:

1-7-3-1

随后让app模块依赖butterknife-annotation模块即可。

BindView注解解析

前文中我们已经利用BindView注解搭建了resID和成员变量之间的关系,接下来我们需要解决的是在发生注解的类中通过findViewById获取到View对象后将其赋值给Activity中的成员变量,伪代码如下:

 public void init(Activity activity) {
     // mTextView为注解修饰的变量名,resId为注解取值
     activity.mTextView = activity.findViewById(resId);
 }

那么如何实现该效果呢?有两种方案:

  1. 发现成员变量被@BindView注解后,在Activity onCreate函数中利用字节码插桩将findViewById这行代码写在onCreate函数内
  2. 生成中间类,该类持有Activity引用,要求开发者在Activity内调用初始化函数,我们在初始化函数内反射构建中间类,完成成员变量赋值

从两种方案可以看出,方案1虽然不需要开发者参与,但具有显式缺点:

  • onCreate中插入字节码位置无法确定,开发者编写的View使用代码可能在我们插入的字节码前,也有可能在我们插入的字节码后
  • ASM操作字节码会拉长编译时间
  • ASM和字节码本身复杂度较高,实现难度较大

故我们选取第二种方案,生成中间类。

JavaPoet生成中间类

这里我们不妨手动完成中间类的实现,再去将其自动化,我们定义中间类的命令规则为Activity类名+ViewBinding,那么我们前面提到的TestActivity的中间类名应该为TestActivityViewBinding,其大致代码如下所示:

 public class TestActivityViewBinding {
     public TestActivityViewBinding(TestActivity activity) {
         bindViews(activity);
     }
 ​
     private void bindViews(TestActivity activity) {
         activity.mTextView = activity.findViewById(R.id.textview1);
     }
 }
 ​
 // TestActivity.java onCreate函数
 @Override
 protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_test);
     // 构造TestActivityViewBinding对象即可
 }

使用@BindView已知信息,我们可以替换中间类中信息,得到如下伪代码如下:

 // 类名 = 包含注解的类名+ViewBinding
 public class 类名 {
     public 类名(使用注解的类名 activity) {
         bindViews(activity);
     }
 ​
     private void bindViews(使用注解的类名 activity) {
         activity.注解修饰的变量名 = activity.findViewById(注解的取值);
     }
 }

综上,只需要在解析到BindView注解时,生成一个这样的中间类即可,在Java中生成类需要借助javapoet库。

在butterknife-processor模块中添加javapoet依赖,按照描述设计process函数代码如下所示:

 //第一个参数为该注解处理器支持的所有类型
 @Override
 public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
     /**
      * 从所有元素中查找使用了支持注解的元素
      * 1.roundEnvironment.getRootElements() 获取所有的element对象
      * 2.getTypeElementsToProcess中匹配支持的所有元素
      */
     for (TypeElement typeElement : getTypeElementsToProcess(roundEnvironment.getRootElements(),set)){
         // 从类元素中获取包名类名
         String packageName = mProcessingEnvironment.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
         String typeName = typeElement.getSimpleName().toString();
         ClassName className = ClassName.get(packageName, typeName);
 ​
         // 构造生成的中间类名
         ClassName generatedClassName = ClassName.get(packageName, typeName + "ViewBinding");
         // 根据类名创建类构造器
         TypeSpec.Builder classBuilder = TypeSpec.classBuilder(generatedClassName).addModifiers(Modifier.PUBLIC);
         // 向类构造器中添加构造函数,构造函数的参数为类名,其内部调用bindViews函数
         classBuilder.addMethod(MethodSpec.constructorBuilder()
                 .addModifiers(Modifier.PUBLIC)
                 .addParameter(className, "activity")
                 .addStatement("$N($N)",
                         "bindViews",
                         "activity")
                 .build());
 ​
         // 构造bindViews函数实现,其使用private修饰,返回值是void
         MethodSpec.Builder bindViewsMethodBuilder = MethodSpec
                 .methodBuilder("bindViews")
                 .addModifiers(Modifier.PRIVATE)
                 .returns(void.class)
                 .addParameter(className, "activity");
 ​
         // 获取类的所有成员属性
         for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {
             BindView bindView = variableElement.getAnnotation(BindView.class);
             if (bindView != null) {
                 // 如果成员属性使用@BindView注解,就向bindViews函数内部加入一行
                 bindViewsMethodBuilder.addStatement("$N.$N = ($T) $N.findViewById($L)",
                         "activity",
                         variableElement.getSimpleName(),
                         variableElement,
                         "activity",
                         bindView.value());
             }
         }
         // 将bindViews函数加入类构造器
         classBuilder.addMethod(bindViewsMethodBuilder.build());
 ​
         // 使用JavaFile将定义的类构造器写入真实文件中
         try {
             JavaFile.builder(packageName,
                             classBuilder.build())
                     .build()
                     .writeTo(mProcessingEnvironment.getFiler());
         } catch (IOException e) {
             mProcessingEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, e.toString(), typeElement);
         }
     }
     return true;
 }
 ​
 private Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
                                                   Set<? extends TypeElement> supportedAnnotations) {
     Set<TypeElement> typeElements = new HashSet<>();
     for (Element element : elements) {
         // 如果该元素是类类型元素
         if (element instanceof TypeElement) {
             boolean found = false;
             // 获取该类内部所有的变量成员和方法
             for (Element subElement : element.getEnclosedElements()) {
                 // 获取变量成员和方法上所使用的注解
                 for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
                     for (Element annotation : supportedAnnotations) {
                         // 如果使用的注解包含在我们支持的注解内,则收集起来
                         if (mirror.getAnnotationType().asElement().equals(annotation)) {
                             typeElements.add((TypeElement) element);
                             found = true;
                             break;
                         }
                     }
                     if (found) break;
                 }
                 if (found) break;
             }
         }
     }
     return typeElements;
 }

在app模块添加butterknife-processor依赖,运行项目,在build目录可以看到我们已经顺利生成了中间类,如下图所示:

1-7-3-4

反射初始化中间类

上文中我们已经成功生成了中间类,接下来我们提供中间类的初始化方法,以便在onCreate中调用,新建butterknife模块,其内部定义ButterKnife.java类,并使得app模块依赖该模块。

在ButterKnife.java类中我们提供bind方法,供Activity调用,在Activity调用该方法时,反射构造该类对应的中间类对象,在中间类对象的构造函数内会完成Activity成员变量的初始化,代码如下所示:

 public class ButterKnife {
 ​
     public static final String VIEW_BINDING_CLASS_SUFFIX = "ViewBinding";
 ​
     private ButterKnife() {}
 ​
     public static <T extends Activity> void bind(T activity) {
         instantiateViewBinding(activity, VIEW_BINDING_CLASS_SUFFIX);
     }
 ​
     private static <T extends Activity> void instantiateViewBinding(T target, String suffix) {
         Class<?> targetClass = target.getClass();
         String className = targetClass.getName();
         try {
             Class<?> bindingClass = targetClass
                     .getClassLoader()
                     .loadClass(className + suffix); // dynamically loads Java class into memory...
             Constructor<?> classConstructor = bindingClass.getConstructor(targetClass);
             try {
                 classConstructor.newInstance(target);
             } catch (IllegalAccessException e) {
                 throw new RuntimeException("Unable to invoke " + classConstructor, e);
             } catch (InstantiationException e) {
                 throw new RuntimeException("Unable to invoke " + classConstructor, 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 instance.", cause);
             }
         } catch (ClassNotFoundException e) {
             throw new RuntimeException("Unable to find Class for " + className + suffix, e);
         } catch (NoSuchMethodException e) {
             throw new RuntimeException("Unable to find constructor for " + className + suffix, e);
         }
     }
 }

随后在TestActivity中调用ButterKnife.bind即可,运行发现,mTextView确实已赋值完成。

至此,我们自定义的ButterKnife就完成了,大家自己动手试试吧,可以尝试添加@OnClick,@OnLongClick等注解。