一步一步带你轻松打造自己的ButterKnife注解框架(下)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/binbinqq86/article/details/79666374

转载请注明出处:https://blog.csdn.net/binbinqq86/article/details/79666374

1、概述

在上一篇文章(一步一步带你轻松打造自己的ButterKnife注解框架(上))中给大家讲解了注解的基本知识和怎么去写一个自己的运行时注解,而今天就要继续带大家来看一下,怎么去写一个自己的编译时注解,降低在大量使用注解的时候,里面的反射对性能的影响。

2、项目框架

类似Arouter、ButterKnife等,一般此类框架需要多个module,下面是本文的框架:

  • module_annotation:用于存放注解等,Java模块
  • module_compiler:用于编写注解处理器,Java模块
  • module_api:用于给用户提供使用的API,本例为Andriod模块
  • module_app:示例,本例为Andriod模块

当然如上只是本案例采用的模块方案,命名和具体模块方案也并没有一个标准的统一,比如有人嫌模块太多,就把上面三个模块放在一起,这样也是可以的,只不过分开写的话更清楚明了,职责划分也更加明确,便于以后的维护和修改以及扩展。并且compiler模块只在编译期间用到,最终打包的时候并不需要,所以是不必打包到程序中的。

对于module间的依赖关系如下:

compiler依赖annotation
api依赖compiler
app依赖api

3、注解模块的实现

注解模块基本上跟上篇文章中讲到的差不多,看代码:


@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface FindId {
    int value();
}
/**
 * @auther tb
 * @time 2018/3/28 下午4:10
 * @desc
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

两个注解分别代表了id、布局的查找和view的点击事件,不同的是我们的保留策略由Runtime变为了CLASS,也就是编译时注解。

4、注解的处理(processor)

定义完注解后,就该实现这个注解的处理了,这块内容要比上文讲的运行时注解麻烦多了,不过掌握以后也就明白其中原理,写起来就很顺手了。首先我们要引入谷歌的一个包

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api 'com.google.auto.service:auto-service:1.0-rc2'
    api project(':module_annotation')
    api 'com.squareup:javapoet:1.9.0'
}

auto-service库可以帮我们去生成META-INF等信息。
下面来看具体注解处理器怎么去写,首先就是要继承AbstractProcessor,在类的顶部加入注解:@AutoService(Processor.class),主要是用来自动生成 META-INF 信息。(注意:Processor千万不要写错为Process,笔者当初就是写错了,导致郁闷了好多天怎么都不生成java代码,惭愧~)
然后覆盖其中的几个方法,分别是:

  • public synchronized void init(ProcessingEnvironment processingEnvironment)
  • public SourceVersion getSupportedSourceVersion()
  • public Set getSupportedAnnotationTypes()
  • public boolean process(Set set, RoundEnvironment roundEnvironment)

其中init方法用来初始化一些变量信息,如下:

  • Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
  • Elements mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
  • Messager mMessager;跟日志相关的辅助类。

看代码:

private Filer mFileUtils;
private Elements mElementUtils;
private Messager messager;

@Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFileUtils = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
    }

getSupportedSourceVersion主要是声明支持的Java源码版本
getSupportedAnnotationTypes主要是声明 Processor 处理的注解,注意这是一个数组,表示可以处理多个注解,如果少加了,那么这个注解肯定不会生效的,楼主在写的时候就犯了这个错误,导致排查半天。

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

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

而这两个方法也可以换一种方式去写(采用注解):

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"example.tb.com.module_annotation.FindId"})

下面就是我们最后一个方法了,也是今天的主角,所有的注解处理都是在这里完成的。process方法的实现需要分为两个步骤走:

  1. 需要生成的类的所有信息的收集
  2. 根据收集的信息生成具体的java代码

首先我们声明一个map,用来存放收集的所有使用我们的注解的每一个类的所有信息:


    /**
     * 一个需要生成的类的集合(key为类的全名,value为该类所有相关的需要的信息)
     */
    private Map<String, ProxyInfo2> mProxyMap = new HashMap<String, ProxyInfo2>();

我们以类的全名为key,类的相关所有信息为value来进行保存。ProxyInfo2这个类里面声明的生成java代码所需要的信息:

/**
 * @auther tb
 * @time 2018/3/27 下午2:44
 * @desc 对应需要生成某个类的全部相关信息
 */
public class ProxyInfo2 {
    /**
     * 类
     */
    public TypeElement typeElement;
    /**
     * 类注解的值(布局id)
     */
    public int value;
    public String packageName;
    /**
     * key为id,也就是成员变量注解的值,value为对应的成员变量
     */
    public Map<Integer, VariableElement> mInjectElements = new HashMap<>();
    /**
     * key为id,也就是方法注解的值,value为对应的方法
     */
    public Map<Integer, ExecutableElement> mInjectMethods = new HashMap<>();

    /**
     * 采用类名方式不能被混淆(否则编译阶段跟运行阶段,该字符串会不一样),或者采用字符串方式
     */
    public static final String PROXY = "TA";
    public static final String ClassSuffix = "_" + PROXY;

    public String getProxyClassFullName() {
        return typeElement.getQualifiedName().toString() + ClassSuffix;
    }

    public String getClassName() {
        return typeElement.getSimpleName().toString() + ClassSuffix;
    }

    //......
}

生成一个类,我们大致需要这些信息就够了。下面看看怎么去收集:

/**
     * 收集所需生成类的信息
     *
     * @param roundEnvironment
     */
    private void collectionInfo(RoundEnvironment roundEnvironment) {
        //process可能会多次调用,避免生成重复的代理类
        mProxyMap.clear();
        //获得被该注解声明的类和变量
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(FindId.class);
        //收集信息
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {
                //获取注解的值
                TypeElement typeElement = (TypeElement) element;
                //类的完整路径
                String qualifiedName = typeElement.getQualifiedName().toString();
                /*类名*/
                String clsName = typeElement.getSimpleName().toString();
                /*获取包名*/
                String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();

                FindId findId = element.getAnnotation(FindId.class);
                if (findId != null) {
                    int value = findId.value();
                    //处理类注解
                    ProxyInfo2 proxyInfo = mProxyMap.get(qualifiedName);
                    if (proxyInfo == null) {
                        proxyInfo = new ProxyInfo2();
                        mProxyMap.put(qualifiedName, proxyInfo);
                    }

                    proxyInfo.value = value;
                    proxyInfo.typeElement = typeElement;
                    proxyInfo.packageName = packageName;
                }
            } else if (element.getKind() == ElementKind.FIELD) {
                //获取注解的值
                FindId findId = element.getAnnotation(FindId.class);
                if (findId != null) {
                    int value = findId.value();
                    //处理成员变量注解
                    VariableElement variableElement = (VariableElement) element;
                    //这里先要获取上层封装类型,然后强转为TypeElement
                    String qualifiedName = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
                    ProxyInfo2 proxyInfo = mProxyMap.get(qualifiedName);
                    if (proxyInfo == null) {
                        proxyInfo = new ProxyInfo2();
                        mProxyMap.put(qualifiedName, proxyInfo);
                    }
                    proxyInfo.mInjectElements.put(value, variableElement);
                }
            } else {
                continue;
            }
        }
        //获得被该注解声明的方法
        Set<? extends Element> elementsMethod = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
        for (Element element : elementsMethod) {
            if (element.getKind() == ElementKind.METHOD) {
                //获取注解的值
                OnClick onClick = element.getAnnotation(OnClick.class);
                if (onClick != null) {
                    int[] value = onClick.value();
                    if (value != null && value.length > 0) {
                        for (int i = 0; i < value.length; i++) {
                            ExecutableElement executableElement = (ExecutableElement) element;
                            //这里先要获取上层封装类型,然后强转为TypeElement
                            String qualifiedName = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
                            ProxyInfo2 proxyInfo = mProxyMap.get(qualifiedName);
                            if (proxyInfo == null) {
                                proxyInfo = new ProxyInfo2();
                                mProxyMap.put(qualifiedName, proxyInfo);
                            }
                            proxyInfo.mInjectMethods.put(value[i], executableElement);
                        }
                    }
                }
            } else {
                continue;
            }
        }
    }

这里提一下Element这个类的几个子类:

  • TypeElement 代表类
  • VariableElement 代表变量
  • ExecutableElement 代表方法

首先我们调用一下mProxyMap.clear();,因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常。然后我们就是通过roundEnv.getElementsAnnotatedWith拿到我们通过@FindId和@OnClick注解的元素,然后去循环遍历这些所有注解的类、变量、方法。把他们的信息一一收集到我们的map中。(注意一定要分开注解去循环遍历处理,本文就分了两种@FindId和@OnClick,笔者写的时候就犯了此错误导致点击事件死活不生效)

收集完我们需要的所有信息后,就该去生成代理类了:

    /**
     * 生成代理类
     */
    private void generateClass() {
        for (String key : mProxyMap.keySet()) {
            ProxyInfo2 proxyInfo = mProxyMap.get(key);
            JavaFileObject sourceFile = null;
            try {
                sourceFile = mFileUtils.createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.typeElement);
                Writer writer = sourceFile.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                error(proxyInfo.typeElement, "===tb===%s", e.getMessage());
            }
        }
    }

这个还是很简单的,循环遍历整个map,生成我们所需要的代码。咦?生成代码?怎么生成的呢?哈哈~原来都被我们封装在ProxyInfo中了。

public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("//自动生成的注解类,勿动!!!\n");
        builder.append("package ").append(packageName).append(";\n\n");
        builder.append("import example.tb.com.module_api.*;\n");
        builder.append("import android.support.annotation.Keep;\n");
        builder.append("import android.view.View;\n");
        builder.append("import " + typeElement.getQualifiedName() + ";\n");
        builder.append('\n');

        builder.append("@Keep").append("\n");//禁止混淆,否则反射的时候找不到该类
        builder.append("public class ").append(getClassName());
        builder.append(" {\n");

        generateMethod(builder);

        builder.append("}\n");
        return builder.toString();
    }

    private void generateMethod(StringBuilder builder) {
        builder.append("    public " + getClassName() + "(final " + typeElement.getSimpleName() + " host, View object) {\n");

        if (value > 0) {
            builder.append("        host.setContentView(" + value + ");\n");
        }
        for (int id : mInjectElements.keySet()) {
            VariableElement variableElement = mInjectElements.get(id);
            String name = variableElement.getSimpleName().toString();
            String type = variableElement.asType().toString();

            //这里object如果不为空,则可以传入view等对象
            builder.append("        host." + name).append(" = ");
            builder.append("(" + type + ")object.findViewById(" + id + ");\n");
        }

        for (int id : mInjectMethods.keySet()) {
            ExecutableElement executableElement = mInjectMethods.get(id);
            VariableElement variableElement = mInjectElements.get(id);
            String name = variableElement.getSimpleName().toString();
            builder.append("        host." + name + ".setOnClickListener(new View.OnClickListener(){\n");
            builder.append("            @Override\n");
            builder.append("            public void onClick(View v) {\n");
            builder.append("                host." + executableElement.getSimpleName().toString() + "(host." + name + ");\n");
            builder.append("            }\n");
            builder.append("        });\n");
        }

        builder.append("    }\n");
    }

看了这些生成代码的代码,你是不是瞬间觉得很low?!原来就是拼出来的代码啊。其实就是这样,看似很高深的东西,弄清楚原理之后是很简单的。如果你觉得low,那么我们再来一个高大上一些的——采用JavaPoet来替我们生成代码:

/**
     * $L,原封不动
     * $S,字符串
     * $T,类
     *
     * @return
     */
    public String generateJavaCode() {
        ClassName viewClass = ClassName.get("android.view", "View");
        ClassName keepClass = ClassName.get("android.support.annotation", "Keep");
        ClassName clickClass = ClassName.get("android.view", "View.OnClickListener");
        ClassName typeClass = ClassName.get(typeElement.getQualifiedName().toString().replace("." + typeElement.getSimpleName().toString(), ""), typeElement.getSimpleName().toString());

        MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(typeClass, "host", Modifier.FINAL)
                .addParameter(viewClass, "object", Modifier.FINAL);
        if (value > 0) {
            builder.addStatement("host.setContentView($L)", value);
        }
        for (int id : mInjectElements.keySet()) {
            VariableElement variableElement = mInjectElements.get(id);
            String name = variableElement.getSimpleName().toString();
            String type = variableElement.asType().toString();
            //这里object如果不为空,则可以传入view等对象
            builder.addStatement("host.$L=($L)object.findViewById($L)", name, type, id);
        }
        for (int id : mInjectMethods.keySet()) {
            ExecutableElement executableElement = mInjectMethods.get(id);
            VariableElement variableElement = mInjectElements.get(id);
            String name = variableElement.getSimpleName().toString();
            TypeSpec comparator = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(clickClass)
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .addParameter(viewClass, "view")
                            .addStatement("host.$L(host.$L)", executableElement.getSimpleName().toString(), name)
                            .returns(void.class)
                            .build())
                    .build();
            builder.addStatement("host.$L.setOnClickListener($L)", name, comparator);
        }
        MethodSpec methodSpec = builder.build();
        TypeSpec typeSpec = TypeSpec.classBuilder(getClassName())
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(keepClass)
                .addMethod(methodSpec)
                .build();
        JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
        return javaFile.toString();
    }

怎么样?这样看上去是不是好多了呢,至于javapoet的使用,本文就不去专门讲解了,感兴趣的同学可以自行学习,本文最后给出了参考链接。到这里整个注解的处理过程就算讲解完了,是不是很麻烦呢,比起之前的运行时注解?

5、api的实现

最后一步就是api的实现了,也就是怎么让注解生效呢,其实这个也很简单了,我们仿照butterKinfe:

/**
 * @auther tb
 * @time 2018/3/27 下午2:51
 * @desc 实现帮助注入的类
 */
public class TAHelper2 {
    /**
     * 用来缓存反射出来的类,节省每次都去反射引起的性能问题
     */
    static final Map<Class<?>, Constructor<?>> BINDINGS = new LinkedHashMap<>();

    public static void inject(Activity o) {
        inject(o, o.getWindow().getDecorView());
    }

    public static void inject(Activity host, View root) {
        String classFullName = host.getClass().getName() + ProxyInfo.ClassSuffix;
        try {
            Constructor constructor=BINDINGS.get(host.getClass());
            if(constructor==null){
                Class proxy = Class.forName(classFullName);
                constructor=proxy.getDeclaredConstructor(host.getClass(),View.class);
                BINDINGS.put(host.getClass(),constructor);
            }
            constructor.setAccessible(true);
            constructor.newInstance(host,root);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我们使用的时候只需要调用inject函数就可以实现注解注入啦~

6、注解的使用

终于写好了我们的注解了,下面就看看怎么去使用吧:

/**
 * @auther tb
 * @time 2018/3/23 下午2:13
 * @desc
 */
@FindId(R.layout.activity_test)
public class TestActivity extends Activity {
    @FindId(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        TAHelper.inject(this);
        TAHelper2.inject(this);
        if(tv!=null){
            tv.setText("hahaha,how are you?");
        }
    }

    @OnClick({R.id.tv})
    void clickTest(View view){
        switch (view.getId()){
            case R.id.tv:
                Toast.makeText(TestActivity.this,"test click",Toast.LENGTH_LONG).show();
                break;
        }
    }
}

怎么样,是不是很简单,用起来跟butterKnife一样一样的~我们再来看看具体生成的类:
这里写图片描述
代码:

package example.tb.com.tannotation;

import android.support.annotation.Keep;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;

@Keep
public class TestActivity_TA {
  public TestActivity_TA(final TestActivity host, final View object) {
    host.setContentView(2131296283);
    host.tv=(android.widget.TextView)object.findViewById(2131165303);
    host.tv.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.clickTest(host.tv);
      }
    });
  }
}

看到这里你应该就明白了,其实就是生成一个类专门去调用activity里面的一些变量,去给他们设置一些属性,原理居然这么简单!!!所以我们的变量,方法都不能写成private,并且这个类不能被混淆,因为我们api里面是采用的反射创建的这个类,否则打包进行代码优化的时候,会去掉该类,导致注解失效,而clickTest不必像上篇中的运行时注解一样保留下来,因为这里直接调用了该方法,所以打包的时候不会被优化掉,而运行时采用的依然是反射,所以才需要keep。

7、调试

相信在编写的路上,肯定不是一次性成功那么顺利,笔者也是磕磕碰碰才算走到这一步,遇到问题最有效的方案就是调试,那么就说一下怎么去对注解处理器这块进行调试,因为它是在编译的时候执行的,所以跟日常的代码调试还不太一样。

首先我们需要配置gradle.properties。找到本地电脑的 gradle home,它一般在当前用户的目录下,比如我的 Windows 电脑位置在:C:\Users\用户名.gradle,而Mac电脑一般在:~/用户名/.gradle。打开(如果没有,新建一个)gradle.properties 文件,增加下面两行:

org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

第二步,找到菜单栏Run/Edit Configurations…打开如图窗口:
这里写图片描述
点击左上角的+,选择Remote,随便填入一个名字,采用默认配置点击ok
这里写图片描述

然后选择你刚才的名字,点击debug按钮,成功后会出现如下字符:
这里写图片描述

这里写图片描述
这样我们就可以开始调试我们的代码了。步骤如下:
选择AS菜单栏:Build/Rebuild Project,就会进入到我们在compiler的注解处理代码里面的断点了~

注意:
这里写图片描述

如果我们在点击debug按钮的时候出现上图情况,我们只需要Rebuild Project一下,然后在点击debug按钮就可以了。具体原因暂不明,有知道的同学可以告知下。

8、常见问题

疑问:这里的日志打印Messager类,笔者一直不明白怎么使用,也看不到所谓的日志,希望有知道的大神可以指点一二。

9、总结

到此我们的编译时注解就讲解完了,明白了其中原理后,写起来还是很顺手的,而需要注意的就是混淆的事项,我们在两篇文章中都讲了其中的注意事项了,如果有其他疑问的同学可以在下方留言,最后同样奉出源码:

源码下载

参考文章:

猜你喜欢

转载自blog.csdn.net/binbinqq86/article/details/79666374