美文网首页Android开发Android技术知识Android深入
Android技术分享——APT实现ButterKnife【实战

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

作者: 谁动了我的代码 | 来源:发表于2022-12-29 21:15 被阅读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核心技术手册》里面几十个技术板块划分,上千个小技术点带你进阶进入高工领域。

    文末

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

    程序的周期 :

    • 源码期 : 开发时 , 刚编写完 " .java " 代码 , 还未编译之前 , 就处于源码期 ;
    • 编译期 : 程序由 java 源码编译成 class 字节码文件 ;
    • 运行期 : 将字节码文件加载到 Java 虚拟机中运行 ;

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

    ① 编译时注解

    ② 注解处理器

    相关文章

      网友评论

        本文标题:Android技术分享——APT实现ButterKnife【实战

        本文链接:https://www.haomeiwen.com/subject/zcvzqdtx.html