使用自定义注解自己实现ButterKnife功能

333 阅读4分钟

最近在研究注解,就看了下apt及butterknife的源码,还看了一些文章,见文末。自己手写了一遍,才算稍微搞明白了点东西。写下笔记,记录一下,免得忘记。

注解的介绍可参考之前的一篇文章 Java注解Annotation 简单总结及应用 - 掘金 (juejin.cn)

1. 环境

Android studio : Dolphin | 2021.3.1 Patch 1 Build #AI-213.7172.25.2113.9123335, built on September 30, 2022

gradle:gradle-7.0.2-bin

2. 反射实现

最早的butterknife是通过反射实现的,我也学着自己搞一下。

2.1 首先创建自定义注解类MyInjectView

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

2.2 创建工具类InjectUtil

public class InjectUtil {
    private final static String TAG = InjectUtil.class.getSimpleName();

    public static void inject(Activity activity) {
        Class<? extends Activity> aClass = activity.getClass();
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(MyInjectView.class)) {//是否有指定的注解
                //获取注解对象
                MyInjectView myInjectView = field.getAnnotation(MyInjectView.class);
                //获取注解的值
                int value = myInjectView.value();
                //非public属性或方法需要设置否则无法访问
                field.setAccessible(true);
                try {
                    //通过反射调用
                    Method findViewById = aClass.getMethod("findViewById", int.class);
                    findViewById.setAccessible(true);
                    Object invoke = findViewById.invoke(activity, value);
                    
                    //给filed变量设值
                    field.set(activity, invoke);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

其中通过反射调用findViewById的三行代码,也可以替换为View view = activity.findViewById(id);然后field.set(activity, view)

2.3 在activity中加入注解

public class MainActivity extends Activity {

    @MyInjectView(R.id.ip_tv)
    public TextView ipTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectUtil.inject(this);
        ipTv.setText("你成功了");
  }
}

到这里,就用反射技术实现了butterknife的功能,当然,这是丐版。

3. apt实现

需要创建注解、注解处理器和工具类三个模块。必须创建三个模块:一是因为注解处理器模块的接口只有java library才能引用的到;二是放一起时打包会出错。

3.1 注解模块myannotation

该模块是java library,即在使用Android studio创建module时,选择Java or kotlin library,用来放自定义注解:

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

3.2 注解处理器模块myprocessor

该模块也是Java library,用来放注解处理器,并且需要添加依赖:

dependencies {
    implementation project(':myannotation')

    // java代码生成框架
    implementation 'com.squareup:javapoet:1.13.0'
    //引入autoservice,自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件,并在该文件中写入注解处理器的全称,包括包路径;
    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    //因为gradle版本问题,需要使用annotationProcessor添加依赖,否则该文件无法生成
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
}

继承抽象类AbstractProcessor,重写注解处理器:

// @SupportedAnnotationTypes({"com.aya.myannotation.MyInjectView"}) 注解表示哪些注解需要注解处理器处理,可以多个注解校验。也可通过重写父类方法实现
//@SupportedSourceVersion(SourceVersion.RELEASE_7) //注解 用于指定jdk使用版本。也可通过重写父类方法实现
@AutoService(Processor.class) //添加注解,自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件,并在该文件中写入注解处理器的全称,包括包路径
public class MyBindingProcessor extends AbstractProcessor {

    private Filer mFiler;//过滤器
    private Messager mMessager; //打印日志
    private JavacTrees mTrees; //java语法树
    private TreeMaker mTreeMaker; //创建或修改方法的AST变量
    private Names mNames; //标识符,获取变量使用

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        mTreeMaker = TreeMaker.instance(context);
        mNames = Names.instance(context);

        mMessager.printMessage(Diagnostic.Kind.NOTE, "*********init*******");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        mMessager.printMessage(Diagnostic.Kind.NOTE, "********process********");

        //获取全部的类
        for (Element element : roundEnvironment.getRootElements()) {
            //获取类的包名
            String pkgName = element.getEnclosingElement().toString();
            //获取类的名字
            String clsName = element.getSimpleName().toString();
            //构建新的类的名字:原类名 + _MyViewBinding
            ClassName className = ClassName.get(pkgName, clsName + "_MyViewBinding");

            mMessager.printMessage(Diagnostic.Kind.NOTE, "pkgName:" + pkgName + ", clsName:" + clsName);

            //构建新的类的构造方法
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(pkgName, clsName), "activity");
            //判断是否要生成新的类,假如该类里面有 MyBindView 注解,那么就不需要新生成
            boolean hasBuild = false;
            //获取类的元素,例如类的成员变量、方法、内部类等
            for (Element enclosedElement : element.getEnclosedElements()) {
                //仅获取成员变量
                if (enclosedElement.getKind() == ElementKind.FIELD) {
                    MyInjectView bindView = enclosedElement.getAnnotation(MyInjectView.class);
                    //判断是否被 MyInjectView 注解
                    if (bindView != null) {
                        hasBuild = true;
                        //在构造方法中加入代码
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }

            //添加一个测试方法
            MethodSpec testMethod = MethodSpec
                    .methodBuilder("test")//方法名
                    .addModifiers(Modifier.PUBLIC)//修饰符,public、private、static、final等
                    .returns(void.class)//返回值
                    .addParameter(String.class, "data")//添加参数
                    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加方法题,具体执行代码
                    .build();

            //是否需要生成
            if (hasBuild) {
                try {
                    //构建新的类
                    TypeSpec builtClass = TypeSpec.classBuilder(className)
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(constructorBuilder.build())
                            .addMethod(testMethod)
                            .build();
                    //生成 Java 文件
                    JavaFile javaFile = JavaFile
                            .builder(pkgName, builtClass)
                            .addFileComment("This class is generated automatically by apt!")
                            .build();
                    //写文件
                    javaFile.writeTo(mFiler);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
//        返回true:表示当前注解已经处理;返回false:可能需要后续的 processor 来处理
        return false;
    }

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

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

}

3.3 注解工具类模块mybindview

该模块是Android模块,即在使用Android studio创建module时,选择Android library

  1. 首先添加依赖:
api project(':myannotation')
  1. InjectUtil工具类移至该模块下,并添加方法injectApt
public static void injectApt(Activity activity) {
    try {
        //获取"当前的activity类名+_MyViewBinding"的class对象
        Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "_MyViewBinding");
        //获取class对象的构造方法,该构造方法的参数为当前的activity对象
        Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
        //调用构造方法
        constructor.newInstance(activity);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

到这里,三个模块都已经创建好了,代码也都有注释,不再多做解释。

3.4 使用

在app模块里加入依赖,注意添加依赖的方式:

implementation project(':mybindview')
annotationProcessor project(':myprocessor')

在MainActivity中使用:

public class MainActivity extends Activity {

    @MyInjectView(R.id.ip_tv)
    public TextView ipTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectUtil.injectApt(this);
        ipTv.setText("你成功了");
    }
}

为了验证,在as中可以选择Build---Rebuild project,然后在Build Output中可以看到MyBindingProcessor中的打印。

运行app,就可以看到效果啦。

3.4 Javapoet addStatement占位符介绍:

$T 是类型替换,一般用于("$T foo", List.class) => List foo. $T。好处是JavaPoet会自动添加文件开头的import。如果直接写("List foo")虽然也能生成List foo,但是最终的java文件会缺少import java.util.List

$L 是字面量替换,比如("abc$L123", "FOO") => abcFOO123。也就是直接替换。

$S 是字符串替换,比如:("$S.length()", "foo") => "foo".length()。注意$S是将参数替换为了一个带双引号的字符串。

$N 是名称替换,比如你之前定义了一个函数MethodSpec methodSpec = MethodSpec.methodBuilder("foo").build(); 。现在你可以通过N获取这个函数的名称("N获取这个函数的名称`("N", methodSpec) => foo`。

4. apt使用扩展

为了可以在多个activity中使用多个注解,我修改了MyBindingProcessor的代码,同时定义了一个新的注解MyInjectView2,可用于在编译器修改控件一些属性比如setText等。代码如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInjectView2 {
    int id();
    String value();
}

MyBindingProcessor的代码:

// @SupportedAnnotationTypes({"com.aya.myannotation.MyInjectView"}) 注解表示哪些注解需要注解处理器处理,可以多个注解校验。也可通过重写父类方法实现
//@SupportedSourceVersion(SourceVersion.RELEASE_7) //注解 用于指定jdk使用版本。也可通过重写父类方法实现
@AutoService(Processor.class)
//添加注解,自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件,并在该文件中写入注解处理器的全称,包括包路径
public class MyBindingProcessor extends AbstractProcessor {

    private static HashMap<ClassName, MethodSpec.Builder> clsMap = new HashMap<>();
    private static boolean hasMadeJavaFile = false;//process方法会被调用多次,如果重复创建Java会报错
    private Filer mFiler;//过滤器
    private Messager mMessager; //打印日志
    private JavacTrees mTrees; //java语法树
    private TreeMaker mTreeMaker; //创建或修改方法的AST变量
    private Names mNames; //标识符,获取变量使用

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        mTreeMaker = TreeMaker.instance(context);
        mNames = Names.instance(context);

        mMessager.printMessage(Diagnostic.Kind.NOTE, "*********init*******");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (TypeElement typeElement : set) {
            //这里可以获取所有支持的注解类型
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(typeElement);
            mMessager.printMessage(Diagnostic.Kind.NOTE, "********: typeElement=" + typeElement + ", elements=" + elements);

        }
        handleMyInjectView(roundEnvironment.getElementsAnnotatedWith(MyInjectView.class));
        handleMyInjectView2(roundEnvironment.getElementsAnnotatedWith(MyInjectView2.class));
        makeJavaFile();
        // 返回true:表示当前注解已经处理;返回false:可能需要后续的 processor 来处理
        return false;
    }

    private void handleMyInjectView2(Set<? extends Element> elements) {
        if (elements.isEmpty()) {
            return;
        }
        // 遍历所有被注解的 Element
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            // 获得它所在的类
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //获取类的包名
            String pkgName = classElement.getEnclosingElement().toString();
            //获取类的名字
            String oldClsNameStr = classElement.getSimpleName().toString();
            mMessager.printMessage(Diagnostic.Kind.NOTE, "********process********: pkgName =" + pkgName + ", clsName=" + oldClsNameStr + ", element=" + element);

            //构建新的类的名字:原类名 + _MyViewBinding
            ClassName newClassName = ClassName.get(pkgName, oldClsNameStr + "_MyViewBinding");

            mMessager.printMessage(Diagnostic.Kind.NOTE, "############ new class pkgName =" + newClassName.packageName() + ", clsName=" + newClassName.simpleName() + ", className=" + newClassName);
            MyInjectView2 bindView2 = variableElement.getAnnotation(MyInjectView2.class);
            if (bindView2 != null) {
                MethodSpec.Builder constructorBuilder = null;
                if (clsMap.get(newClassName) != null) {
                    //构建新的类的构造方法
                    constructorBuilder = clsMap.get(newClassName);
                } else {
                    constructorBuilder = MethodSpec.constructorBuilder()
                            .addModifiers(Modifier.PUBLIC)
                            .addParameter(ClassName.get(pkgName, oldClsNameStr), "activity");
                    clsMap.put(newClassName, constructorBuilder);
                }
                //在构造方法中加入代码
                constructorBuilder.addStatement("activity.$N = activity.findViewById($L)", variableElement.getSimpleName(), bindView2.id())
                        .addStatement("activity.$N.setText($S)", variableElement.getSimpleName(), bindView2.value());
            }
        }

    }

    private void handleMyInjectView(Set<? extends Element> elements) {
        if (elements.isEmpty()) {
            return;
        }
        // 遍历所有被注解的 Element
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            // 获得它所在的类
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //获取类的包名
            String pkgName = classElement.getEnclosingElement().toString();
            //获取类的名字
            String oldClsNameStr = classElement.getSimpleName().toString();
            mMessager.printMessage(Diagnostic.Kind.NOTE, "********process********: pkgName =" + pkgName + ", clsName=" + oldClsNameStr + ", element=" + element);

            //构建新的类的名字:原类名 + _MyViewBinding
            ClassName newClassName = ClassName.get(pkgName, oldClsNameStr + "_MyViewBinding");

            mMessager.printMessage(Diagnostic.Kind.NOTE, "############ new class pkgName =" + newClassName.packageName() + ", clsName=" + newClassName.simpleName() + ", className=" + newClassName);
            MyInjectView bindView = variableElement.getAnnotation(MyInjectView.class);
            if (bindView != null) {
                MethodSpec.Builder constructorBuilder = null;
                if (clsMap.get(newClassName) != null) {
                    //构建新的类的构造方法
                    constructorBuilder = clsMap.get(newClassName);
                } else {
                    constructorBuilder = MethodSpec.constructorBuilder()
                            .addModifiers(Modifier.PUBLIC)
                            .addParameter(ClassName.get(pkgName, oldClsNameStr), "activity");
                    clsMap.put(newClassName, constructorBuilder);
                }
                //在构造方法中加入代码
                constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                        variableElement.getSimpleName(), bindView.value());
            }
        }
    }

    private void makeJavaFile() {
        if (!hasMadeJavaFile) {
            hasMadeJavaFile = true;
            //统一生成java类
            for (ClassName className : clsMap.keySet()) {
                try {
                    //构建新的类
                    TypeSpec builtClass = TypeSpec.classBuilder(className)
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(clsMap.get(className).build())
                            .build();
                    mMessager.printMessage(Diagnostic.Kind.NOTE, "*****start make java file***:" + className);
                    //生成 Java 文件
                    JavaFile javaFile = JavaFile
                            .builder(className.packageName(), builtClass)
                            .addFileComment("This class is generated automatically by apt!")
                            .build();
                    //写文件
                    javaFile.writeTo(mFiler);
                    mMessager.printMessage(Diagnostic.Kind.NOTE, "*****end make java file***");
                } catch (IOException e) {
                    mMessager.printMessage(Diagnostic.Kind.NOTE, "*****make java file fail ***:" + e);
                    e.printStackTrace();
                }
            }
        }
    }

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

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

5.小结

AnnotationProcessor技术,可以将运行期的部分工作提前到编译器,这样用户在使用app时就可以更加顺畅了。

注解的学习到这里就告一段落。

参考文章:

ButterKnife 原理解析 - 掘金 (juejin.cn)

注解处理器(Annotation Processor)简析 - 掘金 (juejin.cn)

从手写ButterKnife到掌握注解、AnnotationProcessor - 掘金 (juejin.cn)

JavaPoet 看这一篇就够了 - 掘金 (juejin.cn)