Android开发Android技术知识Android深入

Android技术分享——APT实现ButterKnife【实战

2022-12-29  本文已影响0人  谁动了我的代码

APT

APT (Annotation Processing Tool)

是一种处理注释的工具,它对源代码文件进行检测并找出其中的 Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够运行,必须要通过 APT 工具来处理。

简单说:根据规则,帮我们生成代码、生成类文件

编译时注解就是通过 APT 来通过注解信息生成代码来完成某些功能,典型代表有 ButterKnife、Dagger、ARouter 等 ButterKnife 原理分析

使用 ButterKnife :

① 添加依赖 :

dependencies {
    implementation 'com.jakewharton:butterknife:10.2.3'
    annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}

② Activity 中使用 ButterKnife :

package kim.hsl.apt;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.hello)
    TextView hello;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        ButterKnife.bind(this);
    
        hello.setText("ButterKnife");
    }
}

BindView 注解分析 : 在 TextView hello 成员变量处添加了 @BindView(R.id.hello) 注解 ;

@Target(FIELD) 元注解 : 表示其作用与类的成员字段 ;

@Retention(RUNTIME) 元注解 : 表示该注解保留到运行时阶段 ;

int value() 注解属性 : 只有一个注解属性 , 并且属性名是 value , 则使用注解时 “value =” 可省略 ;

@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

TextView hello 需要使用 findViewById 进行赋值 , 在上述代码中没有写 findViewById 相关的代码 ; 肯定是在某个地方执行了 findViewById 的方法 ;

ButterKnife.bind(this) 代码就是执行了 findViewById 方法 ;

ButterKnife 用到了编译时技术会 , 在项目编译时 , 会生成 MainActivity_ViewBinding 类 , 在该类中 , 会查找添加了 @BindView 直接的成员变量 , 再获取 注解属性 value 的值 , 然后调用 findViewById 方法获取组件并为成员变量赋值 ;

// Generated code from Butter Knife. Do not modify!
package kim.hsl.apt;

import android.view.View;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import butterknife.Unbinder;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.hello = Utils.findRequiredViewAsType(source, R.id.hello, "field 'hello'", TextView.class);
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.hello = null;
  }
}

ButterKnife 涉及到的源码 :

public final class ButterKnife {
  /**
   * BindView annotated fields and methods in the specified {@link Activity}. The current content
   * view is used as the view root.
      *
   * @param target Target activity for view binding.
      */
    @NonNull @UiThread
    public static Unbinder bind(@NonNull Activity target) {

    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
  }

  /**
   * BindView annotated fields and methods in the specified {@code target} using the {@code source}
   * {@link View} as the view root.
      *
   * @param target Target class for view binding.
   * @param source View root on which IDs will be looked up.
      */
    @NonNull @UiThread
    public static Unbinder bind(@NonNull Object target, @NonNull View source) {

    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> 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);
    }
  }

  @Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      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<? extends Unbinder>) 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;
  }
}

实战APT实现Butterknife

在开始之前,我们先做个假设,要是我们可以把以下代码摘出来到另一个文件,通过某种方式自动生成这种代码会发生什么事。

package com.calm.baseknowledge02;

import com.calm.annotations.IBind;

import android.view.View;

public class MainActivity_ViewBinding implements IBind<com.calm.baseknowledge02.MainActivity> {
    @Override
    public void bind(com.calm.baseknowledge02.MainActivity target) {
        target.tvInfo = (android.widget.TextView) target.findViewById(2131231077);
        target.btnChange = (android.widget.Button) target.findViewById(2131230807);
        target.findViewById(2131230807).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                target.onClick(view);
            }
        });
        target.findViewById(2131230807).setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                target.onLongClick(view);
                return true;
            }
        });
    }
}


我们将findViewById、setOnClickListener、setOnLongClickListener这种代码摘到另一个文件,然后实例化该类与原始的Activity进行绑定。由于这些代码非常类似,我们可以通过模板化处理,就不需要我们在一行行去写了,只需要一个注解就搞定。那么关键来了,这份代码如何生成,这时候我们的APT就该上场了。 先将我们的注解写出来,由于这次的注解只需要在源码期存在即可,因此我们的作用域有一定变化,且较上篇会简单些。

/**
 * 绑定控件
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
    int value();
}
/**
 * 单击事件
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnClick {
    int[] value() default -1;
}

/**

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnLongClick {
    int[] value() default -1;
}

为了做绑定和注入,我们统一一个公共的接口。

public interface IBind<T> {
    void bind(T target);
}

注解处理器需要用到的相关插件,已经有现成的了就不需要我们从头去弄了。

dependencies {
    implementation project(path: ':annotations')
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
}

关键的类就是注解处理器的类,请看代码

/**
 * 注解处理器
 */
@AutoService(Processor.class)
public class AnnotationsCompiler extends AbstractProcessor {
    private Filer filer;
    private Messager messager;
    private static String END_WITH = "_ViewBinding";
    private static String BIND_VIEW = "BindView";
    private static String ON_CLICK = "OnClick";
    private static String ON_LONG_CLICK = "OnLongClick";

    /**
     * 初始化
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    /**
     * 支持的版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        types.add(OnLongClick.class.getCanonicalName());
        return types;
    }

    /**
     * 真正做事的都在这个方法里
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if(set.size() == 0){
            return false;
        }
        //getElementsAnnotatedWith 搜索对应注解的元素
        //ExecutableElement 注解在方法节点
        //VariableElement 注解在成员变量节点
        //TypeElement 注解在类节点
        //PackageElement 注解在包节点
        Set<? extends Element> bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        Set<? extends Element> onClickElement = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
        Set<? extends Element> onLongClickElement = roundEnvironment.getElementsAnnotatedWith(OnLongClick.class);
        //存储节点的容器 结构为 <com.calm.baseknowledge02.MainActivity,<BindView,List<>>>
        Map<String,Map<String, List<Element>>> map = new HashMap<>();
        //将BindView注解的成员变量节点进行分类存储
        for (Element element : bindViewElement) {
            //得到成员变量节点
            VariableElement variableElement = (VariableElement) element;
            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
            String canonicalName = packageName+"."+activityName;
            Map<String, List<Element>> annotationMap = map.get(canonicalName);
            if(annotationMap == null){
                annotationMap = new HashMap<>();
                map.put(canonicalName,annotationMap);
                List<Element> bindViewAnnotations = new ArrayList<>();
                annotationMap.put(BIND_VIEW,bindViewAnnotations);
                bindViewAnnotations.add(element);
            }else {
                List<Element> bindViewAnnotations = annotationMap.get(BIND_VIEW);
                if(bindViewAnnotations == null){
                    bindViewAnnotations = new ArrayList<>();
                    annotationMap.put(BIND_VIEW,bindViewAnnotations);
                }
                bindViewAnnotations.add(element);
            }
        }
        //将OnClick注解节点分类存储
        for (Element element : onClickElement) {
            //得到方法节点
            ExecutableElement executableElement = (ExecutableElement) element;
            String activityName = executableElement.getEnclosingElement().getSimpleName().toString();
            TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
            String canonicalName = packageName+"."+activityName;
            Map<String, List<Element>> annotationMap = map.get(canonicalName);
            if(annotationMap == null){
                annotationMap = new HashMap<>();
                map.put(canonicalName,annotationMap);
                List<Element> onClickAnnotations = new ArrayList<>();
                annotationMap.put(ON_CLICK,onClickAnnotations);
                onClickAnnotations.add(element);
            }else {
                List<Element> onClickAnnotations = annotationMap.get(ON_CLICK);
                if(onClickAnnotations == null){
                    onClickAnnotations = new ArrayList<>();
                    annotationMap.put(ON_CLICK,onClickAnnotations);
                }
                onClickAnnotations.add(element);
            }
        }
        //将OnLongClick注解节点分类存储
        for (Element element : onLongClickElement) {
            //得到方法节点
            ExecutableElement executableElement = (ExecutableElement) element;
            String activityName = executableElement.getEnclosingElement().getSimpleName().toString();
            TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
            String canonicalName = packageName+"."+activityName;
            Map<String, List<Element>> annotationMap = map.get(canonicalName);
            if(annotationMap == null){
                annotationMap = new HashMap<>();
                map.put(canonicalName,annotationMap);
                List<Element> onLongClickAnnotations = new ArrayList<>();
                annotationMap.put(ON_LONG_CLICK,onLongClickAnnotations);
                onLongClickAnnotations.add(element);
            }else {
                List<Element> onLongClickAnnotations = annotationMap.get(ON_LONG_CLICK);
                if(onLongClickAnnotations == null){
                    onLongClickAnnotations = new ArrayList<>();
                    annotationMap.put(ON_LONG_CLICK,onLongClickAnnotations);
                }
                onLongClickAnnotations.add(element);
            }
        }
        //分类完成,开始写模板代码了
        if(map.size() == 0){
            return false;
        }
        Writer writer = null;
        Iterator<String> canonicalNames = map.keySet().iterator();
        while (canonicalNames.hasNext()){
            //全类名
            String canonicalName = canonicalNames.next();
            print(canonicalName);
            //包名
            String packageName = canonicalName.substring(0,canonicalName.lastIndexOf("."));
            print(packageName);
            String activityName = canonicalName.substring(canonicalName.lastIndexOf(".")+1);
            print(activityName);
            try {
//package com.calm.baseknowledge02;
//import com.calm.annotations.IBind;
//public class MainActivity_ViewBinding implements IBind<MainActivity> {
//    @Override
//    public void bind(MainActivity target) {
//        target.tvInfo = target.findViewById(R.id.tvInfo);
//        target.btnChange = target.findViewById(R.id.btnChange);
//    }
//}
                //创建.java文件,名称类似com.calm.baseknowledge02.MainActivity_ViewBinding
                JavaFileObject sourceFile = filer.createSourceFile(canonicalName+END_WITH);
                writer = sourceFile.openWriter();
                writer.write("package "+packageName+";\n");
                writer.write("import com.calm.annotations.IBind;\n");
                writer.write("import android.view.View;\n");
                writer.write("public class "+activityName+END_WITH+" implements IBind<"+canonicalName+"> {\n");
                writer.write("@Override\n");
                writer.write("public void bind("+canonicalName+" target){\n");
                //注解map
                Map<String,List<Element>> annotationMap = map.get(canonicalName);
                if(annotationMap != null){
                    //处理BindView注解
                    List<Element> bindViews = annotationMap.get(BIND_VIEW);
                    if(bindViews != null && bindViews.size() > 0){
                        for (Element bindView : bindViews) {
                            int id = bindView.getAnnotation(BindView.class).value();
                            writer.write("target."+bindView.getSimpleName()+" = ("+bindView.asType()+")target.findViewById("+id+");\n");
                        }
                    }
                    //处理OnClick注解
                    List<Element> onClicks = annotationMap.get(ON_CLICK);
                    if(onClicks != null && onClicks.size() > 0){
                        for (Element onClick : onClicks) {
                            int[] ids = onClick.getAnnotation(OnClick.class).value();
                            for (int id : ids) {
//        findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View view) {
//
//            }
//        });
                                writer.write("target.findViewById("+id+").setOnClickListener(new View.OnClickListener() {\n");
                                writer.write("@Override\n");
                                writer.write("public void onClick(View view) {\n");
                                writer.write("target."+onClick.getSimpleName().toString()+"(view);\n");
                                writer.write("}\n");
                                writer.write("});\n");
                            }
                        }
                    }
                    //处理OnLongClick注解
                    List<Element> onLongClicks = annotationMap.get(ON_LONG_CLICK);
                    if(onLongClicks != null && onLongClicks.size() > 0){
                        for (Element onLongClick : onLongClicks) {
                            int[] ids = onLongClick.getAnnotation(OnLongClick.class).value();
                            for (int id : ids) {
        //        findViewById(R.id.btnChange).setOnLongClickListener(new View.OnLongClickListener() {
//            @Override
//            public boolean onLongClick(View view) {
//
//                return false;
//            }
//        });
                                writer.write("target.findViewById("+id+").setOnLongClickListener(new View.OnLongClickListener() {\n");
                                writer.write("@Override\n");
                                writer.write("public boolean onLongClick(View view) {\n");
                                writer.write("target."+onLongClick.getSimpleName().toString()+"(view);\n");
                                writer.write("return true;\n");
                                writer.write("}\n");
                                writer.write("});\n");
                            }
                        }
                    }
                }
                writer.write("}\n");
                writer.write("}\n");
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if(writer != null){
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return false;
    }
    private void print(String msg){
        messager.printMessage(Diagnostic.Kind.NOTE,msg);
    }
}

关键的地方都有注释,就不一一解释了。这个类都不需要我们主动调用,在编译的时候由javac帮我们进行调用。然后编译下代码,我们就会生成一个我们最开始的那样一个类。 为了能使用这些类,我们还要写一个注入的类

public class CButterknife{
    public static void bind(Object o) {
        String name = o.getClass().getName()+"_ViewBinding";
        try {
            Class<?> clzz = Class.forName(name);
            IBind iBind = (IBind) clzz.newInstance();
            iBind.bind(o);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

这个类也很简单,由于我们生成的类是有命名套路的。比如类为MainActivity,那么生成的类则为MainActivity_ViewBinding,那么我们要进行注入就很简单了,通过反射实例化该类,调用bind方法即可,就这么简单。然后在Activity中使用就更简单,只需要一句代码即可。

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tvInfo)
    TextView tvInfo;
    @BindView(R.id.btnChange)
    Button btnChange;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CButterknife.bind(this);
    }
    @OnClick(R.id.btnChange)
    public void onClick(View view){
        if(view.getId() == R.id.btnChange){
            tvInfo.setText("我是点击按钮后变化的数据");
        }
    }
    @OnLongClick(R.id.btnChange)
    public void onLongClick(View view){
        if(view.getId() == R.id.btnChange){
            tvInfo.setText("我是长按按钮后变化的数据");
        }
    }
}

这里需要注意的是CButterknife.bind(this)这句代码一定要在setContentView之后调用。

image

以上是Android开发技术中APT实现ButterKnife的一些原理解析和实战演练,更多Android技术分享尽在《Android核心技术手册》里面几十个技术板块划分,上千个小技术点带你进阶进入高工领域。

文末

编译时技术 最重要的作用就是在编译时可以 生成模板代码 ;由于生成代码操作是在编译时进行的 , 不会对运行时的性能产生影响 。

程序的周期 :

编译时技术 APT 作用于 编译期 , 在这个过程中使用该技术 , 生成代码 ;编译时技术 2 2 2 大核心要素 : 在编译时 , 执行生成代码的逻辑 , 涉及到两个重要概念 。

① 编译时注解

② 注解处理器

上一篇下一篇

猜你喜欢

热点阅读