通过编译期生成代码方式实现的仿ButterKnife功能Demo

  • 整体概述
  • ButterKnife原理简介
  • 相关的技术点
  • 实战
  • 调试
  • 总结

1.整体概述

好久好久以前,我写过一篇文章 通过反射实现的仿ButterKnife功能Demo 说过,会写一篇按照ButterKnife(https://jakewharton.github.io/butterknife/)实现方式,即编译期生成代码方式实现的相关文章,终于在这里出炉了。。

关于编译期生成代码,首先就要对注解了解,注解基础知识本文就不做介绍了,不熟悉可以看下Java 注解指导手册 – 终极向导

通过反射实现的仿ButterKnife功能Demo只需要获取相应的注解并做反射操作就可以完成相应控件的注入工作。今天要讲的编译期生成代码的注入整体处理起来相对复杂些,但是性能会比反射高,所以现在很多主流IOC框架比如ButterKnife,Dagger,都采用了编译期生成代码方式完成注入工作。

 

2.ButterKnife原理简介

我们都知道ButterKnife帮我们省略了很多代码,但是其实那些代码还是存在的,只不过在编译期自动帮我们生成了,所以要知道其原理首先就要看下生成的代码是怎样的。

这里是随机找到的ButterKnife生成的一部分代码的(主Module--generate--sources--apt):

public class LoginActivity$$ViewBinder<T extends com.txm.hunlimaomerchant.activity.LoginActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131558571, "field 'mEtAccount' and method 'accountInputing'");
    target.mEtAccount = finder.castView(view, 2131558571, "field 'mEtAccount'");
    view.setOnFocusChangeListener(
      new android.view.View.OnFocusChangeListener() {
        @Override public void onFocusChange(
          android.view.View p0,
          boolean p1
        ) {
          target.accountInputing();
        }
      });
    view = finder.findRequiredView(source, 2131558574, "field 'mEtPassword', method 'onEditorAction', and method 'passwordInputing'");
    target.mEtPassword = finder.castView(view, 2131558574, "field 'mEtPassword'");

 

咋一看有点懵逼,但是看到findRequiredView以及castView,是不是觉得很熟悉?是的,这就类似于我们写得不厌其烦的:

TextView textView = (TextView )findViewById(R.id.textView);

换一个简化版本,大概是这样子的:

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.title=(TextView)target.findViewById(2131427373);
    target.content=(TextView)target.findViewById(2131427415);
  }
}

是不是非常简单哈哈~

注意到类名是MainActivity$$ViewBinder,这不就是个内部类的名字么?是的,相当于编译期给MainActivity增加了一个内部类专门用来绑定MainActivity中的控件。

 

3.相关的技术点

1.注解:

就像上文所说,首先还是注解。在通过反射实现的仿ButterKnife功能Demo,注解的RetentionPolicy都是RetentionPolicy.RUNTIME,而这一次,我们的注解的RetentionPolicy是RetentionPolicy.CLASS,关于注解的生命周期:

1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;

2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;

3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

 

首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。

 需要注意的一点是,运行时注解(RetentionPolicy.RUNTIME)源码注解(RetentionPolicy.SOURCE)也可以在注解处理器进行处理,不同的注解有各自的生命周期。

2.APT:

先放一段Oracle官网对APT的定义(APT工具):

What is apt?

The command-line utility apt, annotation processing tool, finds and executes annotation processors based on the annotations present in the set of specified source files being examined. The annotation processors use a set of reflective APIs and supporting infrastructure to perform their processing of program annotations (JSR 175). The apt reflective APIs provide a build-time, source-based, read-only view of program structure. These reflective APIs are designed to cleanly model the JavaTMprogramming language's type system after the addition of generics (JSR 14). First, apt runs annotation processors that can produce new source code and other files. Next, apt can cause compilation of both original and generated source files, thus easing the development cycle.

相信各位肯定基本能看懂。简单来说APT(Annotation Processing Tool)就是一个注解的处理工具,可以在编译期扫描源文件通过 annotation processors根据注解对相关文件进行处理。 如果是生成相关的源文件则可以将原本的源文件和生成的新源文件一起进行编译生成对应的class文件。

所以有了apt,为我们在编译期生成代码奠定了基础。

关于APT的使用官方也有详细的说明APT工具,简单来说就是在编译器的时候,apt会去扫描Java源文件中的注解,将带有指定需要处理的注解的Java源文件按照Element的方式传到我们指定的Processor中的process方法中,然后我们可以按照我们自己的逻辑进行处理,比如根据注解生成代码。

那什么是Element呢?把Java源文件当成一个普通的文本文件,它是有层级结构的,比如:

package com.example;    // PackageElement

    public class Foo {        // TypeElement

        private int a;      // VariableElement
        private Foo other;  // VariableElement

        public Foo() {}    // ExecuteableElement

        public void setA(  // ExecuteableElement
             int newA   // TypeElement
        )
    }

apt会将Java源文件按照这样的从层级元素解析到我们自己的Processor中。

还没看懂?没关系,请听下面分解~~

 

3.Java代码生成工具

和ButterKnife一样,我们也是用著名的JavaPoet,这个工具封装了生成Java代码文件的所有逻辑,帮我们隔离了非常繁琐的文件生成过程,它的API非常简单易用。具体使用教程可以参考JavaPoet 看这一篇就够了

 

4.处理器注册

这里使用AutoService。

AutoService会自动在META-INF文件夹下生成Processor配置信息文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候, 就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

 

4.实战

前面啰嗦了这么多,终于进入主题了。。

项目结构整体基本和ButterKnife一样。

  1. app为主Module
  2. inject为专门注入控件的Module,是一个Android Module
  3. inject-annotation为专门提供注解的Module,是一个Java库的Module
  4. inject-compile是专门使用apt进行编译期处理生成代码的Module,也是Java库Module。

之所以这样拆分是因为将各个功能点进行分离,使其不同模块更加独立内聚。

 

先看inject-annotation模块:

定义一个BindView注解,功能和ButterKnife的BindView类似。

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

注意它的RetentionPolicy为RetentionPolicy.CLASS,意味着它只能在编译期有作用。

 

然后看下inject模块:

1.定义了一个ViewBinder接口,所有生成的用于绑定控件的Java类都要实现该接口,比如前面讲的 MainActivity$$ViewBinder。

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

2.定义一个InjectView类,用于对一个Activity的控件进行具体的注入操作。具体会在Activity的setContentView方法调用之后被调用。

public class InjectView {
    public static void bind(Activity activity){
        String className = activity.getClass().getName();

        try {
            // 得到我们生成的对应该Activity的ViewBinder类的Class对象
            Class<?> viewBinderClass = Class.forName(className + "$$ViewBinder");
            ViewBinder viewBinder = (ViewBinder) viewBinderClass.newInstance();
            // 绑定Activity的控件
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

可以看到这里用到了反射。咦?不是说编译期生成代码么,怎么还有反射?其实这里的反射和和具体的注入逻辑没关系,反射主要是因为具体的ViewBinder类是在编译期生成的,类都还没存在,怎么在这里显式使用呢?而且这里是所有的Activity都共同调用的方法,用了反射才可以灵活支持所有不同的Activity。

 

看下最重要的inject-compile:

首先要注意的是这里Gradle的配置:

annotationProcessor  project(':inject-compile')

要在Android Studio使用APT的功能,需要使用一个Gradle插件。之前一位开发者自己开发的一个插件叫android-apt,代码托管在这里,在该网站中文档可以写着:

The android-apt plugin assists in working with annotation processors in combination with Android Studio. It has two purposes:

  • Allow to configure a compile time only annotation processor as a dependency, not including the artifact in the final APK or library
  • Set up the source paths so that code that is generated from the annotation processor is correctly picked up by Android Studio.

可见它的作用就是:

1.在编译期独立使用annotation processor(即注解处理器,下面会讲到),不会将其打包到APK或者库中。

2.设置好生成代码的路径使得Android Studio可以正确处理它们。

不过,从Gradle安卓插件2.2开始,已经内置了android-apt的功能,叫annotationProcessor,所以可以直接使用annotationProcessor去声明我们的编译期生成代码Module就可以了~~android-apt作者写的具体的迁移文档

 

回到我们的代码:

这里一个控件的绑定可以拆分为三个要素:控件变量名,控件id,控件类型。

这里用一个类表示:

public class BindViewField {
    // 控件变量名
    private String fieldName;
    // 控件类型
    private TypeMirror classType;
    // 控件id
    private int resId;

    public BindViewField(String fieldName, TypeMirror classType, int resId) {
        this.fieldName = fieldName;
        this.classType = classType;
        this.resId = resId;
    }

    public String getFieldName() {
        return fieldName;
    }

    public void setFieldName(String fieldName) {
        this.fieldName = fieldName;
    }

    public TypeMirror getClassType() {
        return classType;
    }

    public void setClassType(TypeMirror classType) {
        this.classType = classType;
    }

    public int getResId() {
        return resId;
    }

    public void setResId(int resId) {
        this.resId = resId;
    }
}

然后开始写我们的主角Processor了~~

我们定义一个类继承AbstractProcessor,这里命名为BindViewProcessor,首先定义两个成员变量:

// Java文件Element的处理工具类
private Elements elementUtils;
// 生成源文件的工具
private Filer filer;

 重写AbstractProcessor四个方法:

1.init:

@Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }

完成刚才两个变量的初始化。

 

2.指定支持的jdk版本:

 @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

3.指定需要处理的注解类型,这里指定BindView注解。

@Override
    public Set<String> getSupportedAnnotationTypes() {
       
        Set<String> typeSet = new LinkedHashSet<>();
        typeSet.add(BindView.class.getCanonicalName());
        return typeSet;
    }

4.具体的注解处理方法process:

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

        // key为Activity,value为该Activity中标注相应注解的集合
        Map<TypeElement, List<BindViewField>> targetMap = new HashMap<>();
        // 遍历所有包含BindView注解的Element,将其按照Activity分类归纳到targetMap中
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {


            //拿到该Element对应的类的TypeElement
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            // 拿到targetMap该Activity对应的bindViewFieldList,如果没有则新建一个bindViewFieldList
            List<BindViewField> bindViewFieldList = targetMap.computeIfAbsent(typeElement, k -> new ArrayList<>());
            //获取控件绑定的三要素
            int resId = element.getAnnotation(BindView.class).value();
            String fieldName = element.getSimpleName().toString();
            TypeMirror type = element.asType();

            BindViewField bindViewField = new BindViewField(fieldName, type, resId);
            bindViewFieldList.add(bindViewField);
        }
        // 遍历targetMap,使用JavaPoet对每个Activity生成对应的ViewBinder类文件
        for (Map.Entry<TypeElement, List<BindViewField>> entry : targetMap.entrySet()) {

            if (entry.getValue().isEmpty()) {
                continue;
            }
         
            TypeElement typeElement = entry.getKey();

            String packageName = getPackageName(typeElement);
            String activityName = getActivityName(typeElement, packageName);
            ClassName activityClass = ClassName.bestGuess(activityName);
            ClassName viewBinderClass = ClassName.get("com.nanfeng.inject", "ViewBinder");
            // 生成类相关信息
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName + "$$ViewBinder")
                    .addModifiers(Modifier.PUBLIC)
                    .addTypeVariable(TypeVariableName.get("T",activityClass))
                    .addSuperinterface(ParameterizedTypeName.get(viewBinderClass, activityClass));
            // 生成方法
            MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("bind")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID)
                    .addParameter(activityClass, "target", Modifier.FINAL);
            //方法中的语句
            for (BindViewField bindViewField : entry.getValue()) {
                methodBuild.addStatement("target.$L = target.findViewById($L)", bindViewField.getFieldName(), bindViewField.getResId());
            }
            // 方法放入类中
            classBuilder.addMethod(methodBuild.build());

            try {
               
                //将类写到Java源文件中
                JavaFile.builder(packageName, classBuilder.build())
                        .addFileComment("Auto Create,can not modify!")
                        .build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 得到该TypeElement对应的Activity的名称 例如:包名com.nanfeng.mybutterknifedemo.MainActivity —>MainActivity
     * @param typeElement
     * @param packageName
     * @return
     */
    private String getActivityName(TypeElement typeElement, String packageName) {
        // 包名+ 点号
        int packageNameLength = packageName.length() + 1;
        // 若两层内部类则需要替换为“$”
        return typeElement.getQualifiedName().toString().substring(packageNameLength).replace(".", "$");
    }

    /**
     * 获取TypeElement对应的包名
     * @param typeElement
     * @return
     */
    private String getPackageName(TypeElement typeElement) {
        return elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
    }

感觉代码和注释还是胜过千言,所以觉得这样写了基本大家应该能看懂吧。。

 

最后在要注入控件的MainActivity加上注解和注入语句:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.text)
    TextView textView;
    @BindView(R.id.text1)
    TextView textView1;

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

        InjectView.bind(this);
        textView.setText("我是老大");
        textView1.setText("家居第几个");

    }
}

 

编译一下,然后到 主项目——build——source——apt——debug路径下找到了我们要的代码.....

咦,怎么空空的?

还记得之前说过的AutoService么,只要给我们的BindViewProcessor加上相应的注解就可以了。

@AutoService(Process.class)
public class BindViewProcessor extends AbstractProcessor {

好了,编译完成后找到生成的Java源文件了:

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.textView = target.findViewById(2131165310);
    target.textView1 = target.findViewById(2131165311);
  }
}
  • 运行下没问题~~

 

5.调试

因为我们的BindViewProcessor的process方法是运行在编译期的,所以无法按照平时的方式断点调试,也无法通过普通的打Log和System.out.print来打印调试信息,那怎么调试呢?这里介绍两种方法:

1.调试日志:

创建一个打印调试日志的类FileUtil,通过简单的IO操作就可以将你要输出的调试信息输出到你想要的文件中,比如:

public class FileUtil {

    public static void print(String info){
        File file = new File("C:\\Users\\Administrator\\Desktop\\log.txt");
        if (!file.exists()){
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        try {
            FileWriter fileWriter = new FileWriter(file.getAbsoluteFile(),true);
            fileWriter.write("\n");
            fileWriter.write(System.currentTimeMillis() + "");
            fileWriter.write(info + "\n");
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

然后将其放到BindViewProcessor的process方法你想要调试的地方~

2.使用远程调试,具体可以参考这两篇文章

如何调试编译时注解处理器AnnotationProcessor

DEBUGGING AN ANNOTATION PROCESSOR IN YOUR PROJECT

按照这两篇文章的方法可以实现断点调试。不过我实验发现每次调试一遍后需要clean才可以重新调试= =

 

6.总结

讲了这么多,简单总结一下吧~~

首先简单介绍了ButterKnife的基本原理,根据其原理展开谈了 注解处理器(AbstractProcess)+代码生成(javaPoet)+处理器注册(AutoService)+apt 。

然后模仿ButterKnife写了一个简单的demo进行实战~

最后介绍了如何调试这种编译期执行的代码。

 

说了这么多,希望对大家有帮助~~

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值