自己动手实现一个简易版本的ButterKnife

上一篇 ButterKnife源码分析- 涉及元注解和AbstractProcessor和javapoet 我们已经了解了Butterknife的源码,总体来讲这个框架理解起来并不难, 思路很清晰,基本就是一条线没有很绕的地方。

接下来我们结合源码的实现,我们实现一个自己的简易版本的带有findViewById功能的Knife。

第一步 创建项目 引入依赖

新建一个annotations 的java Library,它主要负责定义注解。

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:support-annotations:28.0.0'
}
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

sourceCompatibility = "7"
targetCompatibility = "7"

新建一个compiler 的java Library, 它的作用主要是解析注解。

apply plugin: 'java-library'

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    //annotations是上文创建的注解module
    api project(':annotations')

    //javapoet提供了一套生成java代码的api,利用这些api处理注解,生成新的代码或源文件。
    api 'com.squareup:javapoet:1.11.1'

    //auto-service的作用是向系统注册processor(自定义注解处理器),执行编译时使用processor进行处理。
    api 'com.google.auto.service:auto-service:1.0-rc3'
    api 'com.google.auto:auto-common:0.9'
}
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
sourceCompatibility = "7"
targetCompatibility = "7"

第二步 定义注解

我们的需求很简单 所以这部分内容很少。

import android.support.annotation.IdRes;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

第三步 编写解析注解的类

继承AbstractProcessor 实现 getSupportedAnnotationTypes 和 process方法。

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

getSupportedAnnotationTypes 方法的内容很简单,返回一个该注解处理器需要处理的注解的字符串集合,我的理解他是一个注册过程。

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
    //获取所有的带有BindView注解的元素
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
    // 按照Activity 对 BindView 注解的元素进行分类
    Map<String,List<VariableElement>> activityElementMap = new HashMap<>();
    for (Element element : elements) {

        //获取对应的MainActivity名称
        VariableElement variableElement = (VariableElement) element;
        TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement();
        String packageName = MoreElements.getPackage(enclosingElement).getQualifiedName().toString();
        System.out.print("BindViewGenerator  = "+packageName);
        String activityName = enclosingElement.getQualifiedName().toString();

        //createFile(enclosingElement.getQualifiedName().toString());


        //按照activity的包路径进行分类
        List<VariableElement> elementList = activityElementMap.get(activityName);
        if (elementList == null){
            elementList = new ArrayList<>();
            activityElementMap.put(activityName,elementList);
        }
        elementList.add(variableElement);
    }


    // 生成java 文件
    Set<Map.Entry<String, List<VariableElement>>> entries = activityElementMap.entrySet();
    for (Map.Entry<String, List<VariableElement>> entry : entries) {
        String activityName = entry.getKey();
        String substring = activityName.substring(activityName.lastIndexOf(".")+1, activityName.length());

        //创建类
        TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(substring+"_ViewBinding")
                .addModifiers(Modifier.PUBLIC);
        //创建构造方法
        MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeVariableName.get(activityName),"target");

        List<VariableElement> elementList = entry.getValue();
        String packageName = MoreElements.getPackage(elementList.get(0)).getQualifiedName().toString();
        for (VariableElement element : elementList) {
            String name = element.getSimpleName().toString();
            TypeMirror typeMirror = element.asType();
            int id = element.getAnnotation(BindView.class).value();
            methodBuilder.addStatement("target."+name + " = ("+typeMirror+")"+"target.findViewById("+id+")");
        }
        MethodSpec methodSpec = methodBuilder.build();
        TypeSpec typeSpec = typeBuilder.addMethod(methodSpec).build();

        JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                .build();
        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }


    }


    return false;
}

process 方法主要就是解析指定注解上的所有信息。

参数 RoundEnvironment roundEnv :表示当前或是之前的运行环境,可以通过该对象查找找到的注解。

拿到所有的注解对象元素以后,遍历所有注解元素。

for (Element element : elements) {

    //VariableElement 是注解在成员变量身上的元素
    VariableElement variableElement = (VariableElement) element;
    //返回这个元素的最里层元素, 通常我们将注解写在了成员变量上, 所以这里返回的就是类元素
    TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement();
    String packageName = MoreElements.getPackage(enclosingElement).getQualifiedName().toString();
    System.out.print("BindViewGenerator  = "+packageName);
    //返回此类型元素的完全限定名称,即类名全路径
    String activityName = enclosingElement.getQualifiedName().toString();

    //createFile(enclosingElement.getQualifiedName().toString());

	//将一个类上的所有的ViewBind注解元素都放在一起
    //按照activity的包路径进行分类
    List<VariableElement> elementList = activityElementMap.get(activityName);
    if (elementList == null){
        elementList = new ArrayList<>();
        activityElementMap.put(activityName,elementList);
    }
    elementList.add(variableElement);
}

通过上面的方法我们可以已经将所有带有BindView的注解的元素,按照类名进行分类。

//按照类(Activity)进行遍历,生成对应的xxx_BindViewing类
Set<Map.Entry<String, List<VariableElement>>> entries = activityElementMap.entrySet();
for (Map.Entry<String, List<VariableElement>> entry : entries) {
	// 获取到要解析注解对应的类名
    String activityName = entry.getKey();
    //获取到simpleName
    String substring = activityName.substring(activityName.lastIndexOf(".")+1, activityName.length());

    //创建类 我们需要创建的类和我们的Activity在同一个包下,类名是_ViewBinding  这是一个统一的规则
    //JavaPoet提供了(TypeSpec)用于创建类或者接口,(FieldSpec)用来创建字段,(MethodSpec)用来创建方法和构造函数,(ParameterSpec)用来创建参数,(AnnotationSpec)用于创建注解。
    TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(substring+"_ViewBinding")
            .addModifiers(Modifier.PUBLIC);// 创建的类是public
    //创建构造方法
    MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()// 构造方法
            .addModifiers(Modifier.PUBLIC)// 创建的方法是public
            .addParameter(TypeVariableName.get(activityName),"target");// 需要传入的方法

	// 在方法内部加入findViewById,并把得到的值传给对应的Activity
    List<VariableElement> elementList = entry.getValue();
    String packageName = MoreElements.getPackage(elementList.get(0)).getQualifiedName().toString();
    //拿到类里面所有的注解元素  (标记成员变量的注解元素VariableElement)
    for (VariableElement element : elementList) {
    	//得到注解的变量名
        String name = element.getSimpleName().toString();
        //得到变量名的类型
        TypeMirror typeMirror = element.asType();
        // 得到变量名的值  也就是Id值
        int id = element.getAnnotation(BindView.class).value();
        // 添加指定的代码 并且添加分号和换行
        methodBuilder.addStatement("target."+name + " = ("+typeMirror+")"+"target.findViewById("+id+")");
    }
    //最后得到methodSpec 并加入到 TypeSpec, 可以理解成给一个类加一个方法
    MethodSpec methodSpec = methodBuilder.build();
    TypeSpec typeSpec = typeBuilder.addMethod(methodSpec).build();

	// 使用JavaPoet 动态创建出对应的类文件
    JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
            .build();
    try {
        javaFile.writeTo(processingEnv.getFiler());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

这里的主角就是TypeSpec 和 MethodSpec,他们都同属于JavaPoet,JavaPoet提供了(TypeSpec)用于创建类或者接口,(FieldSpec)用来创建字段,(MethodSpec)用来创建方法和构造函数,(ParameterSpec)用来创建参数,(AnnotationSpec)用于创建注解。

至此我们的编译解析类 就写好了。 只需要在我们的MainActivity加上BindView注解 ,build 就可以生成对应的findViewById文件类。

package com.example.guava;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(com.example.guava.MainActivity target) {
    target.toolbar = (android.support.v7.widget.Toolbar)target.findViewById(2131689648);
  }
}

这是我生成的类文件。 接下来的目的是 在oncreate方法中调用这个类的构造方法。

第四步 调用动态生成的类的方法

import android.app.Activity;
import android.view.View;
import com.orhanobut.logger.Logger;
import java.lang.reflect.InvocationTargetException;


public class LemonKnife {
    public static void bind(Activity activity){
    	// 获取到对应的 Activity的class
        Class<? extends Activity> aClass = activity.getClass();
        //获取到全路径字符串
        String canonicalName = aClass.getCanonicalName();
        try {
        	// 因为我们的规则是在XXX_ViewBinding 所以我们找到对应的MainActivity_ViewBinding的class
            Class<?> bindingClass = aClass.getClassLoader().loadClass(canonicalName + "_ViewBinding");
            try {
                try {
                // 获取到他的构造方法,传入对应的参数类型,执行它。
                   bindingClass.getConstructor(aClass).newInstance(activity);
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        Logger.d(canonicalName);
    }
}

这部分很简单就是通过classloader 获取到动态生成的类,调用他的构造方法。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    LemonKnife.bind(MainActivity.this); 
    Drawable logo = toolbar.getLogo();
  }

你可能感兴趣的:(源码分析)