通过反射和annotationProcessor来实现BindView

ButterKnife相信大家都用过,哪怕没用过至少也应该都听说过。虽然官网已经表示deprecated,但是不影响我们对它的学习。记得刚开始使用的时候觉得这东西好神奇呀,减少了太多的代码,后来用熟了之后开始捣鼓起了源码,虽然一知半解,但至少也知道它是通过反射和自动生成代码来达到自动为我们写了很多讨人厌的findViewById和setOnClickListener的目的。

本篇文章的目的是为了帮助大家熟悉通过反射和annotationProcessor来实现BindView的功能,也为了加深下对两种技术的理解。

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.textView) TextView textView;

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

        textView.setText("Hello World!");

        Binding.bind(this);
    }
}

上面的代码我们再熟悉不过了,不需要写findViewById就能初始化textView,先看下通过反射是怎么实现的。

先写一个注解类BindView:

//保留的意思 保留到运行时
@Retention(RetentionPolicy.RUNTIME)
//作用范围
@Target(ElementType.FIELD)
public @interface BindView {
    //如果这样命名方法 使用的地方就要显式的写出id=xxx
//    int id();
//    String name() default "longdw";

    //可以省略value=xxx
    int value();
}

不用做过多的解释,直接看代码注释吧。

接下来写Binding类:

public class Binding {
    public static void bind(Activity activity) {
        //遍历所有的activity字段
        for (Field field : activity.getClass().getDeclaredFields()) {
            //获取字段前面的注解
            BindView bindView = field.getAnnotation(BindView.class);
            //如果注解不为空,说明这个字段是用BindView注解来申明的
            if (bindView != null) {
                try {
                    //这句话很重要,因为TextView字段默认是private类型的
                    //直接field.set是没法调用的,设置了这句话才能调用。
                    field.setAccessible(true);
                    field.set(activity, activity.findViewById(bindView.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上面简单的Binding类就能帮助我们实现类似ButterKnife自动给变量赋值的目的。其实这样写是有效率问题的,如果类中只有很少的变量是通过BindView注解的,这样写看着没多大问题,但是注解的变量一旦多起来,这样写是不行的,因为field.getAnnotation()这段代码很耗时,调用一两次可能没多大感觉,如果每次加载一个页面都调用十几次甚至几十次,那就很明显感觉到页面卡顿了。

所以接下来我们采用类似ButterKnife自动生成代码的方式来实现。

先简单说下实现思路,编译期间通过java的annotationProcessor来扫描所有的类文件,如果发现有被BindView注解的变量,就生成一个以Binding结尾的类,比如上面例子中MainActivity,编译后生成一个MainActivityBinding类,构造方法里面生成所需要的代码,然后通过反射调用MainActivityBinding的构造方法。

先看下怎么调用Binding结尾的类的构造方法。新建一个Android Library,然后创建Binding类:

public class Binding {
    public static void bind(Activity activity) {
        try {
            Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding");
            Constructor constructor = bindingClass.getConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

接下来主要任务就是自动化生成Binding结尾的类了。要用到Java提供的annotationProcessor。

首先新建Java or Kotlin Library项目,将BindView注解类放进去。

因为是自动生成代码,所以需要修改BindView的Retention:

//保留的意思 只在编译时用
@Retention(RetentionPolicy.SOURCE)
//作用范围
@Target(ElementType.FIELD)
public @interface BindView {
    //如果这样命名方法 使用的地方就要显式的写出id=xxx
//    int id();
//    String name() default "longdw";

    //可以省略value=xxx
    int value();
}

接下来就是重点了,创建Java or Kotlin Library项目:

直接贴上BindingProcessor类的源码吧:

public class BindingProcessor extends AbstractProcessor {

    Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//        ClassName className = ClassName.get("com.david.apt", "Test");
//        TypeSpec buildClass = TypeSpec.classBuilder(className).build();
//        try {
//            JavaFile.builder("com.david.apt", buildClass).build().writeTo(filer);
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
        for (Element element : roundEnvironment.getRootElements()) {
            String packageStr = element.getEnclosingElement().toString();
            String classStr = element.getSimpleName().toString();
            ClassName className = ClassName.get(packageStr, classStr + "Binding");
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");
            boolean hasBinding = false;

            for (Element enclosedElement : element.getEnclosedElements()) {
                if (enclosedElement.getKind() == ElementKind.FIELD) {
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if (bindView != null) {
                        hasBinding = true;
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }

            TypeSpec builtClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build())
                    .build();
            if (hasBinding) {
                try {
                    JavaFile.builder(packageStr, builtClass).build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
        return false;
    }

    /**
     * 返回所有注解类型
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BindView.class.getCanonicalName());
    }
}

这里用到一个第三方的库

dependencies {
    implementation project(':lib-annotations')
    implementation 'com.squareup:javapoet:1.12.1'
}

javapoet能很方便的帮我们生成Java文件。

代码核心逻辑是查找主工程中的所有类,寻找FIELD类型的元素,也就是成员变量,检查是否有元素是被BindView注解类修饰的,然后生成文件。

最后注意修改build.gradle文件中的依赖:

implementation project(':lib')
annotationProcessor project(':lib-processor')

执行命令./gradlew :app:compileDebugJava看下是否会在build文件夹下生成所需要的类。