探究 ButterKnife 的注解处理以及代码生成

ButterKnife

ButterKnife 是一个常用的第三方库,它在编译期间,使用注解处理器解析注解,并生成样板代码,从而达到给 Android views 绑定的效果,从而简化了我们写的一些样板代码。

为了了解 ButterKnife 的原理,我自己模仿写了一个库,几乎可以以假乱真,当然,这个库只有学习参考的价值。

既然是模仿,先看下 ButterKnife 使用 @BindString 注解的例子。

首先,参照 github 上 ButterKnife 的 README,添加依赖

    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

有没有想过,为何要添加2个库,并且添加库的方式还不一样?这个问题后面会给出解释。

在 MainActivity 中使用 @BindString

public class MainActivity extends AppCompatActivity {

    @BindString(R.string.app_name)
    String mAppName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 用 ButterKnife 绑定 Activity
        ButterKnife.bind(this);
    }
}

代码中,@BindString(R.string.app_name) 其实只是为了在编译期间生成样板代码,而真正的为 mAppName 赋值的其实是 ButterKnife.bind(this) 完成的,所以这两步缺一不可。

那么,现在不要急着运行项目,先编译一下,也就是 Android Studio 中的 Build -> Rebuild Project,编译完成后会生成一个 MainActivity_ViewBinding 的类,路径为 app\build\generated\source\apt\debug\ ,如下图

这里写图片描述
那么,在看看这个生成的代码

// Generated code from Butter Knife. Do not modify!
package com.ckt.customandroidannotation;

import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.View;
import butterknife.Unbinder;
import java.lang.Deprecated;
import java.lang.Override;

public class MainActivity_ViewBinding implements Unbinder {
  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target);
  }

  @UiThread
  public MainActivity_ViewBinding(MainActivity target, Context context) {
    Resources res = context.getResources();
    target.mAppName = res.getString(R.string.app_name);
  }

}

这个代码我精简过,而且这个类简直毫无PS痕迹,这明明就是 Android Project 中的创建的一个 Java 类。

Now,你是否按耐不住内中的冲动,想要搞清楚这他妈的代码是怎么生成的,FOLLOW ME ~

注解处理以及代码生成

注解处理器(Annotation Processor),是在编译期间处理注解的,所以它并不影响程序的性能。在普通的 Java Project 中可以很轻松的自定义一个注解处理器,但是在 Android Project,并不能为 JavaCompiler 定义一个注解处理器,但是 Android Project 可以引用一个 Java Project 作为 library,从而使用整个 library 中的自定义注解处理器来编译代码。

为了更好的理解原理,首先看下项目的结构,如下图
这里写图片描述

图中,android-annotation 和 android-compiler 为 Java Library ,mybutterknife 为 Android Library,app 为 Android Project.

首先创建一个 android-annotation 的 Java Library,用来自定义注解,可能有人不知道怎么创建 Java Library,所以我用2个图来表示创建过程,后面创建 android-compiler 的 Java Library 也是一样。

这里写图片描述

这里写图片描述

现在,在 android-annotation 中创建一个自定义的注解,搞直接点,就叫做 BindString

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

如代码所示,BindString 这个注解的目标是 FIELD,也就是成员变量,这个注解可以在 CLASS 文件中保留,当然也可以改为在只在源码中保留,因为这并不影响在编译时期获取注解。

现在,再来创建一个名为 android-compiler 的 Java Library,并在这个库中添加一个注解处理器的实现类,由于注解处理器要指定处理哪个注解,因此,android-compiler 要把 android-annotation 作为库引入进来。

那么,在 android-compiler 的 build.gradle 中引入

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':android-annotation')
}

当然,你也可以在 android-compiler 这个项目上,按 F4,然后通过添加 Module Dependency 把 android-annotation 添加进来。

现在,通过继承 AbstractProcessor 来自定义一个注解处理器

public class BindViewProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

process() 方法,是必须要重写的方法,另外一般还要重写三个方法

  1. init(),做初始化操作,用来获取一些辅助类,用来解析注解,创建文件等等。
  2. SupportedAnnotationTypes(),提供需要解析的注解的 Class。
  3. SupportedSourceVersion(),提供所支持的版本。

而 SupportedAnnotationTypes() 和 SupportedSourceVersion() 方法在 jdk 1.7 后,可以通过注解的方式来替代。

@SupportedAnnotationTypes("com.example.BindString")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcessor extends AbstractProcessor {

    // 用于解析 Element
    private Elements mElementUtils;
    // 用于创建文件
    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        mElementUtils = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindString.class)) {
            // 如果注解不是应用在成员上,就过滤
            if (element.getKind() != ElementKind.FIELD) {
                continue;
            }
            // 获取注解所在类的 Element
            Element enclosingElement = element.getEnclosingElement();
            // 获取注解所在类的 Name
            Name enclosingElementName = enclosingElement.getSimpleName();
            // 生成类的名字
            String clsName = enclosingElementName.toString() + "_BindString";

            // 获取成员变量的名字
            String fieldName = element.getSimpleName().toString();
            int value = element.getAnnotation(BindString.class).value();

            // 获取包的 Element
            PackageElement packageName = mElementUtils.getPackageOf(element);

            // 创建要生成的代码的字符串
            String content = getContentFromJava(value);

            try {
                // 创建文件
                JavaFileObject javaFileObject = mFiler.createSourceFile(packageName + "." + clsName);
                Writer writer = javaFileObject.openWriter();
                // 把内容写入到文件中
                writer.write(content);
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    private String getContentFromJava(int id){
        StringBuilder sb = new StringBuilder();
        sb.append("package com.ckt.custombutterknife;\n\n");
        sb.append("import android.content.res.Resources;\n\n");
        sb.append("public class MainActivity_BindString {\n\n");
        sb.append("public MainActivity_BindString(MainActivity target) {\n");
        sb.append("    Resources res = target.getResources();\n");
        sb.append("    target.mAppName = res.getString(").append(id).append(");\n");
        sb.append("}\n");
        sb.append("}");
        return sb.toString();
    }    
}

getContentFromJava() 方法是用 StringBuilder 来创建生成的代码字符串,然而,这种方式实在是违背了程序员的“懒惰”精神,那么可以使用 Square 公司开源的 JavaPoet 库,在 build.gradle 中引入

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':android-annotation')
    compile 'com.squareup:javapoet:1.9.0'
}

那么现在用 JavaPoet 库来创建样板代码

    private String getContentFromJavaPoet(Name enclosingElementName, String clsName, String fieldName, int value, PackageElement packageName) {
        ClassName resource = ClassName.get("android.content.res", "Resources");
        ClassName mainActivity = ClassName.get(packageName.toString(), enclosingElementName.toString());

        MethodSpec constructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(mainActivity, "target")
                .addStatement("$T res = target.getResources()", resource)
                .addStatement("target.$L = res.getString($L)", fieldName, value)
                .build();

        TypeSpec typeSpec = TypeSpec.classBuilder(clsName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(constructor)
                .build();

        return JavaFile.builder(packageName.toString(), typeSpec).build().toString();
    }

这个代码就不多做解释了,有兴趣的朋友可以到 github 上看看 JavaPoet 的 wiki,里面有详细的使用教程。

Ok,到这了,一个自定义的注解处理器的类就完成了,但是 android-compiler 这个库还没有完成,因为 JavaComipler 还不知道怎么找到这个注解处理器呢,按照 Oracle 官方的做法,是需要手动创建包和文件,来指定注解处理器的路径,然而,这种机械化的操作可以由 Google 开源的 auto-service 库解决,因此在 build.gradle 中再次引入。

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':android-annotation')
    compile 'com.squareup:javapoet:1.9.0'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
}

使用这个库就更简单了,直接给注解处理器的类加个注解就完了

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.example.BindString")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class HelloProcessor extends AbstractProcessor {}

现在,JavaCompiler 就可以找到这个处理 BindString 注解的注解处理器了。

那么,现在 android-compiler 这个库就完成了,不过,你应该也想知道这个生成的类到底是什么样子吧? 很简单,把 app 这个 Android Project 引入这2个 Java Library,然后使用 @BindString 注解(与例子中使用方法一样),然后 rebuild 即可看到下面的代码

package com.ckt.custombutterknife;

import android.content.res.Resources;

public class MainActivity_BindString {

public MainActivity_BindString(MainActivity target) {
    Resources res = target.getResources();
    target.mAppName = res.getString(2131099681);
}
}

生成的代码很简单,就一个 Android Project 中普通的 Java 类以及一个构造方法,从构造方法中也可以看出,只要传递一个 MainActivity 对象,就可以给 MainActivity 的 mAppName 赋值了,那么在 rebuild 后,就可以直接在 MainActivity 中调用 new MainActivity_BindString(this); 就可以为 mAppName 赋值了。

然而,这有个很明显的缺陷,那就是要先编译,再自己手动创建对象,也就等于手动赋值了。而 ButterKnife 却不是这样做的,ButterKnife 创建一个 Android Library(build.gradle 中 compile ‘com.jakewharton:butterknife:8.8.1’),通过传入的 Activity 对象,加载生成的类,通过反射获取生成类的构造函数,并创建了这个类的对象。 于是乎,Android Project 引用这个 Android Library,就可以在不用编译的情况下,也可以使用 ButterKnife.bind(this);,只是没有任何作用,一旦编译甚至运行后,这个代码就起作用了。

加载生成的代码

在前面使用 ButterKnife 的例子中提到过, ButterKnife.bind(this) 为 mAppName 赋值,它的原理是加载生成的类,通过反射获取生成类的构造方法,然后创建这个类的对象。那么,我也来模仿下这个操作,创建一个名为 mybutterknife 的 Android Library,注意,不是 Java Library, 也不是 Android Project。具体步骤,如下2个图所示

这里写图片描述

这里写图片描述

然后,在 mybutterknife 这个类中,创建一个 MyButterKnife 的 Java 类

public class MyButterKnife {
    public static void bind(Activity target) {
        // 获取 activity 的 Class 对象
        Class<? extends Activity> targetCls = target.getClass();
        // 获取 activity 的名字
        String clsName = targetCls.getName();
        try {
            // 通过 activity 的 ClassLoader 加载生成的类
            Class<?> generatedCls = targetCls.getClassLoader().loadClass(clsName + "BindString");
            // 找到构造方法
            Constructor<?> constructor = generatedCls.getConstructor(targetCls);
            if (constructor != null) {
                // 创建对象
                constructor.newInstance(target);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }
}

通过 Activiy 的 ClassLoader 加载了生成的类,通过反射获取了构造方法,然后创建了生成类的对象,从而达到了为 mAppName 赋值的目的。

使用注解

在使用 ButterKnife 的例子中,加载了两个 ButterKnife 的库

    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

butterknife,这个已经说过,是一个 Android Library,而 butterknife-compiler,我想你也猜到了吧,是注解处理器的 Java Library,而 annotationProcessor 的意思是,app 项目编译时候,使用 butterknife-compiler ,然而并不会合并到最终的 apk 中,这是一种优化处理。

ok,到现在为止,模仿的项目中已经创建了两个 Java Library,android-annotation 和 android-compiler,一个 Android Library,mybutterknife,那么按照 ButterKnife 添加依赖库的方式,添加我们自己创建的依赖库

    compile project(':mybutterknife')
    annotationProcessor project(':android-compiler')

很明显,如果想要在 Activity 中使用自定义注解 @BindString,还需要把 android-annotation 加进来,这个可以在 mybutterknife 的 build.gradle 中添加进来

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:26.+'
    testCompile 'junit:junit:4.12'
    // 添加自定义注解项目为依赖库
    compile project(':android-annotation')
}

好的,到这里,app 项目就可以正常使用 @BindString 注解了

public class MainActivity extends AppCompatActivity {
    @BindString(R.string.app_name)
    String mAppName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyButterKnife.bind(this);
        System.out.println(mAppName);
    }
}

与 ButterKnife 的使用方式完成一样,并且不需要预编译,也就是不需要先 rebuild,直接运行,mAppName 就可以被赋值。

总结

本篇文章,抱着学习 ButterKnife 原理的心态,主要探究了注解处理的过程,代码生成的过程,并模仿 ButterKnife 做了一些优化。 当然,这篇文章只是抛砖引玉,想要了解更多,还是要深入研究下 ButterKnife 这个库。

参考链接

https://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Processor.html
https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html

https://medium.com/@iammert/annotation-processing-dont-repeat-yourself-generate-your-code-8425e60c6657
https://stablekernel.com/the-10-step-guide-to-annotation-processing-in-android-studio/
http://www.jianshu.com/p/d7567258ae85

https://github.com/square/javapoet

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值