04.手写ButterKnife(ButterKnife源码阅读)

  1. 云栖社区>
  2. 博客>
  3. 正文

04.手写ButterKnife(ButterKnife源码阅读)

黑夜路口 2018-04-26 21:12:00 浏览549
展开阅读全文

源码地址:https://github.com/renzhenming/MyButterknife

相信大多数安卓人员开发中都在使用ButterKnife进行代码生成。使用ButterKnife有几个好处,第一,提高开发效率,可以一键生成布局中所有的View对象或者设置View的点击事件,这两个功能是我们最常用的。第二,不会影响app运行效率,ButterKnife采用编译时注解的方式生成代码,相对于XUtils的注解方式,在效率上有很大的优势,看过Xutils源码的话你会发现,里边的注解是反射的方式实现的,而反射在一定程度上是会增大运行的开销的。

ButterKnife的作者是就职于谷歌的JakeWharton,在GitHub上可以看到完整的代码,https://github.com/JakeWharton/butterknife/tree/master/butterknife-annotations/src/main/java/butterknife,ButterKnife的功能是相当多的,不止是我们最长用的bindView set onClick等,看看这些注解就大概知道它所涉及到的功能点

img_364d4e0811f24f4ec60f7e13e5468634.png
826815906.png

今天打算实现的是其中一个BindView注解,后期可能会实现onClick方法,借此来加深一下编译时注解的使用

当我们在一个Activity中使用ButterKnife.bind()之后,重新编译会生成这样一个类,xxxx_ViewBinding,xxxx代表的是当前Activity的名字,这个类是什么样的有什么作用,我们简单拷贝一个来看

public final class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  MainActivity_ViewBinding(MainActivity target) {
    this.target = target;
    target.world = Utils.findViewById(target,2131427424);
    target.bitch = Utils.findViewById(target,2131427425);
  }

  @Override
  @CallSuper
  public final void unbind() {
    target.world = null;
    target.bitch = null;
  }
}

从这个类上可以看到,使用ButterKnife并非不用findViewById了,而是把这个工作交给了编译器去自动生成,然后在bind方法调用的时候,反射一次创建这个自动生成的类的对象从而实现view注入的功能,那么为什么编译时会自动生成类呢,这要用到注解的知识,接下来我们开始自己手写一个简单的ButterKnife工具

我们仿照JakeWharton的分包形式,创建一个butterknife-annotation和butterknife-compiler的Java library(butterknife-compiler必须时Java library,因为注解生成器需要继承AbstractProcessor,这个类只有Java工程可以引用到),我们在butterknife-annotation中创建注解BindView

/**
 * Created by renzhenming on 2018/4/24.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

在butterknife-compiler中创建注解生成器


/**
 * Created by renzhenming on 2018/4/24.
 * AbstractProcessor这个类是Java中的,只能在ava Library中使用
 */
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Filer mFiler;
    private Elements mElementUtils;

    /**
     * init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
     * ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
     * @param processingEnv 提供给 processor 用来访问工具框架的环境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
    }

    /**
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
     * @return  使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
     * @return  注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //仿照Butternife源码
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation: getSupportAnnotations()){
             types.add(annotation.getCanonicalName());
        }
        return types;
    }

    private Set<Class<? extends Annotation>> getSupportAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class);
        return annotations;
    }
    /**
     * 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
     * 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
     * @param set   请求处理的注解类型
     * @param roundEnvironment  有关当前和以前的信息环境
     * @return  如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
     *          如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
     *
     *          这里的log信息只能在gradle console中看到,Android logcat看不到。要注意
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
          ...........................
    }
}

注意注解生成器的这个library build.gradle的配置

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    //AutoService注解使用
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    //自动生成类相关
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':butterknife-annotation')
}
//解决 错误: 编码GBK的不可映射字符 的报错
tasks.withType(JavaCompile){
    options.encoding='UTF-8'
}
//android studio不完全能兼容Java8,所以指定编译版本位1.7
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

在我们项目主工程的app 配置文件build.gradle中设置依赖

compile project(':butterknife-annotation')
compile project(':butterknife-compiler')

这时候编译一下工程就可以执行到ButterKnifeProcessor 的process方法中了,如果看不到打印,检查两点
第一:目标文件(bind绑定的activity)是否相比于上一次编译有修改,如果代码没有改变,不会重新编译
第二:因为是Java library中打印的log,在Android studio的logcat中是无法看到的,你需要到Gradle Console中去看

process方法有执行了,那么进行下一步,编写自动生成类的代码,在process方法中加入如下代码

//------------获取注解-----------

        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //LinkedHashMap输出和输入的顺序相同,先输入就先输出
        Map<Element,List<Element>> elementsMap = new LinkedHashMap<>();
        for (Element element : elements) {
            //这里会把所有跟注解有关的field全部拿到,包括各个类中的field,也就是说在
            //编译时,项目中所有涉及到这个注解的地方的所有field都在这个Set中返回了,
            //我们需要手动进行分类
            System.out.println("-----------------------"+element.getSimpleName());
            //得到的enclosingElement是这个field所在类的类名
            Element enclosingElement = element.getEnclosingElement();
            System.out.println("------------enclosingElement-----------"+enclosingElement.getSimpleName());
            //以类名位key值存储一个类中所有的field到集合中
            List<Element> bindViewElements = elementsMap.get(enclosingElement);
            if (bindViewElements == null){
                bindViewElements = new ArrayList<>();
                elementsMap.put(enclosingElement,bindViewElements);
            }
            bindViewElements.add(element);
        }

        //------------生成代码-----------

        for (Map.Entry<Element,List<Element>> entry:elementsMap.entrySet()){
            Element enclosingElement = entry.getKey();
            List<Element> bindViewElements = entry.getValue();

            ClassName unbinderClassName = ClassName.get("com.rzm.butterknife","Unbinder");
            System.out.println("------------Unbinder-----------"+unbinderClassName.simpleName());
            //得到类名的字符串
            String activityName = enclosingElement.getSimpleName().toString();
            ClassName activityClassName = ClassName.bestGuess(activityName);
            //拼装这一行代码:public final class xxx_ViewBinding implements Unbinder
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"_ViewBinding")
                    //类名前添加public final
                    .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
                    //添加类的实现接口
                    .addSuperinterface(unbinderClassName)
                    //添加一个成员变量,这个名字target是仿照butterknife
                    .addField(activityClassName,"target",Modifier.PRIVATE);

            //实现Unbinder的方法
            //CallSuper这个注解不像Override可以直接拿到,需要用这种方式
            ClassName callSuperClass = ClassName.get("android.support.annotation","CallSuper");
            MethodSpec.Builder unbindMethod = MethodSpec.methodBuilder("unbind")//和你创建的Unbinder中的方法名保持一致
                    .addAnnotation(Override.class)
                    .addAnnotation(callSuperClass)
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC);

            //添加构造函数
            MethodSpec.Builder constructMethodBuilder = MethodSpec.constructorBuilder()
                    .addParameter(activityClassName,"target");
            constructMethodBuilder.addStatement("this.target = target");
            for (Element bindViewElement : bindViewElements) {
                String fieldName = bindViewElement.getSimpleName().toString();

                //在构造方法中添加初始化代码
                ClassName utilsClassName = ClassName.get("com.rzm.butterknife", "Utils");
                BindView annotation = bindViewElement.getAnnotation(BindView.class);
                int resId = annotation.value();
                constructMethodBuilder.addStatement("target.$L = $T.findViewById(target,$L)",fieldName,utilsClassName,resId);

                //在unbind方法中添加代码 target.textView1 = null;
                //不能用addCode,因为它不会在每一行代码后加分号和换行
                unbindMethod.addStatement("target.$L = null",fieldName);
            }
            classBuilder.addMethod(constructMethodBuilder.build());


            classBuilder.addMethod(unbindMethod.build());

            //开始生成
            try {

                //得到包名
                String packageName = mElementUtils.getPackageOf(enclosingElement)
                        .getQualifiedName().toString();

                JavaFile.builder(packageName,classBuilder.build())
                        //添加类的注释
                        .addFileComment("butterknife 自动生成")
                        .build().writeTo(mFiler);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;

创建一个butterknife安卓library,创建类ButterKnife,实现bind方法,通过反射将自动生成的类生成注入view

public class ButterKnife {

    public static Unbinder bind(Activity activity){
        try {
            Class<? extends Unbinder> clazz = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
            //构造函数

            Constructor<? extends Unbinder> unbinderConstuctor = clazz.getDeclaredConstructor(activity.getClass());
            Unbinder unbinder = unbinderConstuctor.newInstance(activity);
            return unbinder;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Unbinder.EMPTY;
    }
}

Unbinder
···
public interface Unbinder {

//仿照butterknife源码

@UiThread
void unbind();

Unbinder EMPTY = new Unbinder() {
    @Override
    public void unbind() {

    }
};

}
···
Utils

public class Utils {
    public static <T extends View> T findViewById(Activity activity,int viewId){
        return (T)activity.findViewById(viewId);
    }
}

测试一下使用

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text1)
    TextView world;

    @BindView(R.id.text2)
    TextView bitch;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        world.setText("aaaaa");
    }
}

重新编译一下,可以看到自动生成的类


img_e775a07d1cf447a32088c331ca8ba2da.png
828223296.png

运行成功,至此我们不需要写findViewById也可以得到view对象了

网友评论

登录后评论
0/500
评论
黑夜路口
+ 关注