Android Apt技术简析与ButterKnife

2,820 阅读5分钟

ButterKnife 的简单实现:反射方式

InnerBinding辅助findViewByid

首先通过InnerBinding辅助实现Activity的findViewById,真机运行->生效

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_apt);

        InnerBinding.bind(this);
        haha();

    }
public class InnerBinding {
    public static void bind(AptActivity activity){
        activity.textView = activity.findViewById(R.id.textView);
    }
}

由于这样做的灵活性很差,所以可以通过反射改造InnerBinding,真机运行->生效

public class InnerBinding {
    public static void bind(AptActivity activity){
        //activity.textView = activity.findViewById(R.id.textView);
        //通过反射获取到activity内的view
        for (Field field : activity.getClass().getDeclaredFields()) {
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView!=null){
                try {
                    View view = activity.findViewById(bindView.value());
                    field.set(activity,view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

移植代码到module库lib_reflection

注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

public class Binding {
    public static void bind(Activity activity){
        //通过反射获取到activity内的view
        for (Field field : activity.getClass().getDeclaredFields()) {
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView!=null){
                try {
                    //通过反射扩大访问权限->public
                    field.setAccessible(true);
                    View view = activity.findViewById(bindView.value());
                    field.set(activity,view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

使用跟butterknife一样

public class AptActivity extends AppCompatActivity {

    @BindView(R.id.textView) TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_apt);

        Binding.bind(this);

    }
}

注解技术

分类:

  • 普通注解
    • @Override @Deprecated @SupressWarnings
  • 元注解:注解其他注解的注解
    • @Documented:被javaDoc工具记录
    • @Target:使用范围
    • @Rention:描述注解生命周期
    • @Inherited:可继承,class子类
  • 自定义注解
    • public @interface test()
  • ButterKnife的BindView注解
    • 作用域class,编译时生成,所以会影响编译速度,但是不影响运行时的速度
    @Retention(CLASS) @Target(FIELD)
    public @interface BindView {
      /** View ID to which the field will be bound. */
      @IdRes int value();
    }
    

注解处理器

  • 注解处理器(Annotation Processor)是javac的一个工具, 编译时扫描和处理注解
  • 每一个处理器都是继承于AbstractProcessor
    • init方法:被注解处理工具调用
    • process:处理器的主函数
  • 注解处理器工作流程图
  • ButterKnife注解APT工作流程:
    • 声明的注解的生命周期为CLASS
    • 继承于AbstractProcessor类
    • 再调用AbstractProcessor的process方法

反射+运行时注解举例

  • 1 判断任意一个对象所属的类
  • 2 构造任意一个类的对象;
  • 3 判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法)
  • 4 调用任意一个对象的方法
  • 缺点:浪费性能,产生很多临时变量,造成频繁GC;同时反射出的代码无法被编译器优化;

APT技术对ButterKnife的简单实现

Annotation Processing

  1. 编译的时候扫描注解,并做相应的处理,生成java代码,生成 Java 代码是调用javapoet库生成的
  2. 调用 ButterKnife.bind(this);方法的时候,将ID与对应的上下文绑定在一起

依赖注入的概念

  • 把依赖的决定权交给外部,即依赖注入.
  • Dagger:外部的依赖图来决定依赖的值,对象自己只负责「索要」,而不负责 指定值,所以 Dagger 是依赖注入
  • ButterKnife:自己决定依赖的的获取,只把执行过程交给 ButterKnife,所以只 是一个视图绑定库,而不是依赖注入

如dagger

@Inject Data data;
DaggerUtils.inject(this);
data.get();
data.set(newData);

运行时自动创建辅助类AptActivityBinding的实现

通过InnerBinding在运行时反射创建AptActivityBinding辅助findViewById

public class InnerBinding {

    private static final String TAG = "InnerBinding";
    
    public static void bind(AptActivity activity){
        String bindingClassName = activity.getClass().getCanonicalName()+"Binding";//com.dsh.txlessons.annotaionprocessing.AptActivityBinding

        try {
            Class bindingClass = Class.forName(bindingClassName);
            Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

生成类类似如下↓↓↓↓↓↓

public class AptActivityBinding {
    public AptActivityBinding(AptActivity activity) {
        activity.textView = activity.findViewById(R.id.textView);
    }
}

通过InnerBinding.bind(this)调用,真机运行->生效

真正的Annotation Processing改造,编译期动态生成

AptActivityBinding是我们自己写好的,上面实测可行,所以下一步我们要让xxxActivityBinding这样的类能够自动生成

首先修改BindView注解

修改BindView,由于要在编译期生成辅助代码,所以修改Retention的值

@Retention(RetentionPolicy.SOURCE)//不保留只在编译的时候使用
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

然后新建注解处理java库lib-processor

  • Annotation Processing 用法:
    • resources/META-INF/services/javax.annotation.processing.Processor
    • 继承 AbstractProcessor
    • 重写 getSupportedAnnotationTypes() 和 process()
      • annotaions: 程序中出现的已注册的 Annotations;roundEnv:各个 java 文件
    • 依赖:annotationProcessor
  • 自动生成代码:
    • 需要把 Annotation 单独拆成一个 java lib module,被主项目和 processor 分别依赖
    • 还需要一个 lib module,依赖 annotation,把 bind 那些东⻄写在这里。 主项目依赖 lib,lib 依赖 annotations。最终主项目中有两个依赖:lib 和 processor

首先新建注解处理类BindingProcessor


package com.dsh.lib_processor;

...

public class BindingProcessor extends AbstractProcessor {
  Filer filer;

  @Override
  public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    filer = processingEnvironment.getFiler();
  }

  @Override
  public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

    //手动造一个已知类名等信息的
    //String packageName = "com.dsh.txlessons.annotaionprocessing";
    //ClassName className = ClassName.get(packageName,"AptActivityBinding");
    //TypeSpec builtClass = TypeSpec.classBuilder(className)
    //        .addModifiers(Modifier.PUBLIC)
    //        .addMethod(MethodSpec.constructorBuilder()
    //                .addModifiers(Modifier.PUBLIC)
    //                .addParameter(ClassName.get(packageName,"AptActivity"),"activity")
    //                .addStatement("activity.textView = activity.findViewById(R.id.textView)")
    //                .build())
    //            .build();
    //try {
    //    JavaFile.builder(packageName, builtClass)
    //            .build().writeTo(filer);
    //} catch (IOException e) {
    //    e.printStackTrace();
    //}

    for (Element element : roundEnvironment.getRootElements()) {
      String packageStr = element.getEnclosingElement().toString();
      String classStr = element.getSimpleName().toString();
      ClassName className = ClassName.get(packageStr, classStr + "Binding");
      MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
          .addModifiers(Modifier.PUBLIC)
          .addParameter(ClassName.get(packageStr, classStr), "activity");
      boolean hasBinding = false;

      for (Element enclosedElement : element.getEnclosedElements()) {
        if (enclosedElement.getKind() == ElementKind.FIELD) {
          BindView bindView = enclosedElement.getAnnotation(BindView.class);
          if (bindView != null) {
            hasBinding = true;
            constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                enclosedElement.getSimpleName(), bindView.value());
          }
        }
      }

      TypeSpec builtClass = TypeSpec.classBuilder(className)
          .addModifiers(Modifier.PUBLIC)
          .addMethod(constructorBuilder.build())
          .build();

      if (hasBinding) {
        try {
          JavaFile.builder(packageStr, builtClass)
              .build().writeTo(filer);
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
    return false;
  }

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    return Collections.singleton(BindView.class.getCanonicalName());
  }
}

然后固定写法新建文件javax.annotation.processing.Processor
目录结构:src/main/resources/META-INF/services/
文件内写入如下代码注册(猜测是注册使用)

com.dsh.lib_processor.BindingProcessor

然后使用它

Binding.bind(this);

apt小结

在项目的app.build.gradle中依赖了注解所需的库

    implementation project(':lib')
    annotationProcessor project(':lib-processor')
  • 首先通过lib-processor中的BindingProcessor读取注解,并生成各个Activity对应的类,里面包含UI绑定的代码,这些是在编译期做的
    public class AptActivityBinding {
      public AptActivityBinding(AptActivity activity) {
        activity.textView = activity.findViewById(2131231047);
        activity.layout = activity.findViewById(2131230906);
      }
    }
    
  • 在Activity中,lib中的Binding通过反射方式创建xxxActivityBinding对象
        public class Binding {
        
        public static void bind(Activity activity){
    
            String bindingClassName = activity.getClass().getCanonicalName()+"Binding";//com.dsh.txlessons.annotaionprocessing.AptActivityBinding
    
            try {
                Class bindingClass = Class.forName(bindingClassName);
                Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
                constructor.newInstance(activity);
            } catch (xxxException e) {
                e.printStackTrace();
            } 
            ...
        }
    }
    
  • 这样Activity中的UI控件就能够绑定了

拓展

例:方法过时注解
如下,通过注解和注释,可以告诉开发者不要使用旧的方法

/**
 * 方法过时,使用{@link #hehe()}来替代它
 */
@Deprecated
void haha(){
    
}

void hehe(){
    
}

代码戳 -> git