编译时注解

编译时注解

上一篇写了一个简单的运行时注解在,这边来尝试写一下编译时注解,仿照ButterKnife写一个findViewById的编译时注解

1. 新建2个java lib和一个Android lib

这里是用来放注解的lib
这里是用来放注解的lib
这里是生成我们需要的文件的地方
这里是生成我们需要的文件的地方
这里是我们连接app工程和解析注解生成的类的地方
这里是我们连接app工程和解析注解生成的类的地方

2.去Android lib下放入一下需要的类

一个是Utils类,用来执行Android的findview

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

另一个是执行bind和unbind的类,稍后再说

3. 去annotation lib下写点需要的注解

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

这个lib基本就完成了

4. 加入配置

去app工程的build.gradle中加入上面3个lib

	//注意这个要这样声明配置
    annotationProcessor project(path: ':compiler')
    implementation project(path: ':butterknife')
    implementation project(path: ':annotation')

在整个工程的build中加入

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

在compiler的build中加入

dependencies {
   implementation project(path: ':annotation')
   compileOnly'com.google.auto.service:auto-service:1.0-rc4'
   annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
   implementation 'com.squareup:javapoet:1.9.0'
}

5.写一点Compiler内容

@AutoService(Compiler.class) //这个必须要加
public class Compiler extends AbstractProcessor {
	//1. 这里是指定支持的java版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
	//2 .这里是指定要解析的注解类型
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new LinkedHashSet<>();

        for (Class<? extends Annotation> annotation : getSupportedAnnotationType()){
            set.add(annotation.getCanonicalName()));
        }
        return set;
    }

    public Set<Class<? extends Annotation>> getSupportedAnnotationType(){
        Set<Class<? extends Annotation>> set = new LinkedHashSet<>();
        set.add(FindView.class);
        return set;
    }

// 上面的1和2也可以和@AutoService(Compiler.class)一样写成注解形式放在类上
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

6.实现process方法

在这个方法里面我们会去仿照butterKnife去生成一些java来完成我们的findViewById这个过程

	@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //这里会拿到所有包含了这个注解的元素,比如说在两个activity的4个view上都写了这个注解,那么这里将会拿到这4个view
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(FindView.class);
        //我们需要将view和它所在的activity对应起来
        Map<Element, List<Element>> elementListMap = new HashMap<>();
        for (Element element : elements) {
            //返回封装此元素(非严格意义上)的最里层元素
            Element enclosingElement = element.getEnclosingElement();

            List<Element> list = elementListMap.get(enclosingElement);
            if (list == null){
                list = new ArrayList<>();
                elementListMap.put(enclosingElement,list);
            }
            list.add(element);
        }

        //接下来生成对应的activityViewBind
        //elementListMap里面的key是activity,value是activity里面包含注解的view。所以采用entry的方式
        for (Map.Entry<Element, List<Element>> entry:elementListMap.entrySet()) {
            //不管3721,先拿值
            Element key = entry.getKey(); //这是activity
            List<Element> value = entry.getValue(); //这是view集合

            String activityName = key.getSimpleName().toString(); //获取activity的name
            ClassName className = ClassName.bestGuess(activityName); //通过activity的name去猜测类名
            //生成一个java class文件
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"ViewBind") //创建的java类名
                    .addModifiers(Modifier.FINAL,Modifier.PUBLIC) //声明为 public final
                    .addField(className,"target",Modifier.PRIVATE); //添加一个变量 private xxxActivity  target

            //创建一个方法
            MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("unbind").addModifiers(Modifier.PUBLIC);
            classBuilder.addMethod(methodBuild.build());

            //创造一个结构函数
            MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                    .addParameter(className,"target")//添加参数类型
                    .addStatement("this.target = target"); //添加一个声明?
            classBuilder.addMethod(constructor.build());

            //拿到包名
            String packName = elementUtils.getPackageOf(key).getQualifiedName().toString();
            try {
                //java文件生成
                JavaFile.builder(packName,classBuilder.build()).build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            };

        }
        return false;
    }

    Elements elementUtils;
    Filer filer;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        super.init(processingEnv);
    }

这时候,如果配置都正确的话,可以在app工程里面给某个view加入自己的注解,点击运行
在这里插入图片描述

7.给每个view执行findViewById

我们继续在process中完成这个操作

for (Element element :value){
                String fieldName = element.getSimpleName().toString();
                //获取Utils的类名 通过包名和类名
                ClassName utils = ClassName.get("com.example.butterknife", "Utils");
                //获取view的id,这个在注解中有
                int resId = element.getAnnotation(FindView.class).value();
                //我们需要通过butterknife里面的Utils类去执行 target.view = Utils.indView(target,view)
                constructor.addStatement("target.$L = $T.findView(target,$L)",fieldName,utils,resId);
            }
            //删掉上面的构造方法,我们放到这里去实现
            classBuilder.addMethod(constructor.build()).addModifiers(Modifier.PUBLIC);

            //拿到包名
            String packName = elementUtils.getPackageOf(key).getQualifiedName().toString();
            try {
                //java文件生成
                JavaFile.builder(packName,classBuilder.build()).build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            };

再次运行,这里把view的绑定就完成了

8. 反射获取

现在,我们要想去使用这个注解,只差最后一步了,那就是去反射获取我们创建好的类
我们去前面创建好的Android lib中实现他

public class Knife {
    public static Object bind(Activity activity) {
        try {
            //通过传入的activity name拼接我们生成java name
            Class<?> aClass = Class.forName(activity.getClass().getName() + "ViewBind");

			//获取构造方法
            Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(activity.getClass());
			//反射创建实例
            Object o = declaredConstructor.newInstance(activity);

            return o;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

9.使用

public class MainActivity extends AppCompatActivity {
    @FindView(R.id.tv1)
    TextView v1;


    @FindView(R.id.tv2)
    TextView v2;

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

        Knife.bind(this);
        v1.setText("1111");
        v2.setText("2222");
    }
}

10 总结

编译时注解使用了一些java工具,这些东西在Android学习过程中很难遇到,第一次写会很懵逼,很懵逼,但是,我觉得,多写几遍多看一点总是会一点点理解的。比如说TypeSpec为我们提供了java class的创建方法,MethodSpec为我们提供了构造函数和普通方法的创建方法。MethodSpec里面的addStatement和addComment有什么区别,多写几遍,多踩点坑也就明白了。
addStatement可以给方法里面写一些具体的实现
addComment则是添加注解
具体来说就是
addStatement(“this.target = target”) 等价与

private void method(){
	this.target = target
}

addComment(“this.target = target”) 等价与

private void method(){
	//this.target = target
}

addReturns添加返回值
等等等等
其他的一些东西,我在代码的注解里面也写的比较清楚了,这里我们成功实现了利用编译时注解实现findViewById这样的功能,当然我们也不是自己独立写的,而是借鉴ButterKnife和其他大佬写的文章。
此外,我们还可以利用注解去实现SetOnClickListener这样的功能。当然,如果你去看过ButterKnife的源码,就会发现,没那么简单了。

11.耍流氓的方式实现点击事件注解

注解类

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] onClick();
}
//解析OnClick
        Set<? extends Element> clickElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
        //解析属性
        Map<Element, List<Element>> clickElementListMap = new HashMap<>();
        for (Element element : clickElements) {
            Element enclosingElement = element.getEnclosingElement();
            List<Element> list = clickElementListMap.get(enclosingElement);
            if (list == null) {
                list = new ArrayList<>();
                clickElementListMap.put(enclosingElement, list);
            }
            list.add(element);
        }

        //生成
        for (Map.Entry<Element, List<Element>> entry : clickElementListMap.entrySet()) {
            Element key = entry.getKey();
            List<Element> value = entry.getValue();
            String activityName = key.getSimpleName().toString();
            ClassName activityClassName = ClassName.bestGuess(activityName);
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName + "ViewBinding")
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                    .addField(activityClassName, "target", Modifier.PRIVATE);

            MethodSpec.Builder methodbuild = MethodSpec.methodBuilder("setOnClick").addModifiers(Modifier.PUBLIC);
            //classBuilder.addMethod(methodbuild.build());
//构造函数
            MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                    .addParameter(activityClassName, "target");
            constructor.addStatement("this.target = target");
            constructor.addStatement("setOnClick()");
            classBuilder.addMethod(constructor.build()).addModifiers(Modifier.PUBLIC);
            //Utils.findView(source ,r.id);
            for (Element element : value) {
                String fieldName = element.getSimpleName().toString();
                ClassName utils = ClassName.get("com.example.knife", "Utils");
                OnClick annotation = element.getAnnotation(OnClick.class);
                int[] resIds = annotation.onClick();
                for (int i : resIds)
                    methodbuild.addStatement("$T.setClick(target,$L,$S)", utils, i,element.getSimpleName().toString());
            }
            classBuilder.addMethod(methodbuild.build()).addModifiers(Modifier.PUBLIC);


            try {
                String packname = elementsUtil.getPackageOf(key).getQualifiedName().toString();
                JavaFile.builder(packname, classBuilder.build()).build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("error");
            }
        }

回到utils类中,我们在这个类里面找到对应的方法,然后利用反射去实现点击事件

 public static void setClick(Object activity, int viewId, String method) {

        Class<?> aClass = activity.getClass();
        Method[] declaredMethods = aClass.getDeclaredMethods();
        for (Method method1:declaredMethods) {
            if (method1.getName().equals(method)){
                View view = findView((Activity) activity, viewId);
                if (view != null) {
                    view.setOnClickListener(new onDeclaredClick(method1, activity));

                }
            }
        }
    }

    private static class onDeclaredClick implements View.OnClickListener {
        private Method method;
        private Object o;

        public onDeclaredClick(Method method, Object o) {
            this.method = method;
            this.o = o;
        }

        @Override
        public void onClick(View v) {
            try {
                method.setAccessible(true);
                method.invoke(o);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

这里方式很耍流氓,而且这里利用了反射,反射是消耗性能的,通常不会这样写。看过butterknife源码的都知道,它只在bind的时候利用了反射,其他地方都是加载获取类等方式去实现的。
不过,重要的是,我们利用上一篇讲到的运行时注解的方式,在这里按照自己的思路实现了它。所以,事实证明,不是做不了,只是你愿不愿意去做

另外,如果在utils里面完全按照运行时注解去写的话,会发现获取OnClick的注解时为null,这就是编译时注解和运行时注解的差距了,上一篇最后有张图(我copy来的),可以去看看

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值