写一个ButterKnife

本文介绍如何使用APT(Annotation Processing Tool)实现ButterKnife的功能,包括创建注解、编写处理器和生成绑定代码的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上篇博客写的是源码分析,但是并没有涉及到apt;这次就来看看使用apt如何写一个ButterKnife;
##Part1 注解基础
如果没用过注解或者用的少,推荐下看下http://blog.youkuaiyun.com/briblue/article/details/73824058
##Part2 apt实现ButterKnife
使用Annotation Processing Tool实现的好处就是没有性能损耗,因为没有用反射,只是在编译时解析注解生成需要的class文件。

步骤:
1.新建名为annotation的Java Library,

annotation的build.gradle:

apply plugin: 'java'

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

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

定义两个注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bind {
    int id() default -1;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    /** View IDs to which the method will be bound. */
    int[] value() default {-1};
}

2.新建名为compiler的Java Library

compiler的build.gradle:

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':annotation')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

配置项目根目录的build.gradle

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

定义一个ButterKnifeProcessor 继承AbstractProcessor;这里参考了最新版本的ButterKnife源码,不过里面的逻辑很复杂容易把人绕晕。所以我根据自己的理解,写了个更简单易懂的。

重写process方法,依据bindingMap 结果遍历生成多个以_ViewBinding为结尾的类文件:

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        Map<ClassName, BindingSet> bindingMap = findAndParseTargets(env);
        for (Map.Entry<ClassName, BindingSet> entry : bindingMap.entrySet()) {//根据ClassName取得BindingSet,生成多个文件;
            ClassName className = entry.getKey();
            BindingSet binding = entry.getValue();

            TypeSpec.Builder
                    result = TypeSpec.classBuilder(binding.bindingClassName.simpleName() + "_ViewBinding")
                    .addModifiers(PUBLIC)
                    .addSuperinterface(UNBINDER)
                    .addField(binding.targetTypeName, "target", PRIVATE)
                    .addMethod(createBindingConstructorForActivity(binding))
                    .addMethod(createBindingConstructorForActivity2(binding))
                    .addMethod(createBindingUnbindMethod(binding));

            for (Element element : binding.elementClicks) {//添加点击view的成员变量;
                int[] ss = element.getAnnotation(OnClick.class).value();
                for (int s : ss) {
                    result.addField(VIEW, "view" + s, PRIVATE);
                }
            }
            JavaFile javaFile = JavaFile.builder(binding.bindingClassName.packageName(), result.build())
                    .addFileComment("Generated code from Butter Knife. Do not modify!")
                    .build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

BindingSet :依据类名,保存需要绑定的view和click集合。

public class BindingSet {
    public TypeName targetTypeName;
    public ClassName bindingClassName;

    public Set<Element> elements = new HashSet<>();
    public Set<Element> elementClicks = new HashSet<>();
}

取出所有的注解,依据所在的类名保存到bindingMap :

private Map<ClassName, BindingSet> findAndParseTargets(RoundEnvironment env) {//以ClassName为key,BindingSet为value保存结果
        LinkedHashMap<ClassName, BindingSet> map = new LinkedHashMap<ClassName, BindingSet>();

        Set<? extends Element> elements = env.getElementsAnnotatedWith(Bind.class);
        Set<? extends Element> elementClicks = env.getElementsAnnotatedWith(OnClick.class);
        for (Element element : elements) {
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            TypeMirror typeMirror = enclosingElement.asType();
            TypeName targetTypeName = TypeName.get(typeMirror);
            if (targetTypeName instanceof ParameterizedTypeName) {
                targetTypeName = ((ParameterizedTypeName) targetTypeName).rawType;
            }
            String packageName = getPackage(enclosingElement).getQualifiedName().toString();
            String className = enclosingElement.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$');
            ClassName bindingClassName = ClassName.get(packageName, className);

            if (map.keySet().contains(bindingClassName)) {
                BindingSet bindingSet = map.get(bindingClassName);
                bindingSet.elements.add(element);
                bindingSet.targetTypeName = targetTypeName;
                bindingSet.bindingClassName = bindingClassName;
            } else {
                BindingSet bindingSet = new BindingSet();
                bindingSet.elements.add(element);
                bindingSet.targetTypeName = targetTypeName;
                bindingSet.bindingClassName = bindingClassName;
                map.put(bindingClassName, bindingSet);
            }
        }

        for (Element click : elementClicks) {
            TypeElement enclosingElement = (TypeElement) click.getEnclosingElement();
            TypeMirror typeMirror = enclosingElement.asType();
            TypeName targetTypeName = TypeName.get(typeMirror);
            if (targetTypeName instanceof ParameterizedTypeName) {
                targetTypeName = ((ParameterizedTypeName) targetTypeName).rawType;
            }
            String packageName = getPackage(enclosingElement).getQualifiedName().toString();
            String className = enclosingElement.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$');
            ClassName bindingClassName = ClassName.get(packageName, className);

            if (map.keySet().contains(bindingClassName)) {
                BindingSet bindingSet = map.get(bindingClassName);
                bindingSet.elementClicks.add(click);
                bindingSet.targetTypeName = targetTypeName;
                bindingSet.bindingClassName = bindingClassName;
            } else {
                BindingSet bindingSet = new BindingSet();
                bindingSet.elementClicks.add(click);
                bindingSet.targetTypeName = targetTypeName;
                bindingSet.bindingClassName = bindingClassName;
                map.put(bindingClassName, bindingSet);
            }
        }
        return map;
    }

使用javapoet生成代码:findView和设置onclick等;

取值替换需要用到以下字符,有点类似于String.format:

  • $L for Literals

  • $S for Strings

  • $T for Types 类型,用了会自动import;

  • $N for Names(我们自己生成的方法名或者变量名等等)

 private MethodSpec createBindingConstructorForActivity2(BindingSet binding) {
        MethodSpec.Builder builderConstructor2 = MethodSpec.constructorBuilder()
                .addModifiers(PUBLIC)
                .addParameter(binding.targetTypeName, "target", Modifier.FINAL)
                .addParameter(VIEW, "source")
                .addStatement("this.target = target")
                .addStatement("View view");
        for (Element element : binding.elements) {
            builderConstructor2.addStatement("target.$L = $T.findRequiredViewAsType(source, R.id.$L, \"field '$L'\", $T.class)", element.getSimpleName(), UTILS, element.getSimpleName(), element.getSimpleName(), getRawType(element));
        }
        for (Element element : binding.elementClicks) {
            int[] ss = element.getAnnotation(OnClick.class).value();
            for (int s : ss) {
                builderConstructor2
                        .addStatement("view = Utils.findRequiredView(source, $L)", s)
                        .addStatement("view$L = view", s)
                        .addStatement("view.setOnClickListener(new View.OnClickListener() {\n" +
                                "            @Override\n" +
                                "            public void onClick(View v) {\n" +
                                "                target.onClick(v);\n" +
                                "            }\n" +
                                "        })");
            }
        }
        return builderConstructor2.build();
    }

主要的apt相关的代码就是上面这些,完成这些后再project build后就会自动生成绑定用到的java代码;

public class ButterKnife {
    public static final String TAG = "ButterKnife";
    public static Unbinder bind(Activity target) {
        View sourceView = target.getWindow().getDecorView();
        return createBinding(target, sourceView);
    }
    private static Unbinder createBinding(Object target, View source) {
        Class<?> targetClass = target.getClass();
        Constructor<? extends Unbinder> constructor = findViewBinder(targetClass);
        try {
            return constructor.newInstance(target, source);//这里调用了2个参数的构造方法。生成实例并且绑定了对象;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
    private static Constructor<? extends Unbinder> findViewBinder(Class<?> cls) {
        String clsName = cls.getName();
        try {
            Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");//查找到apt生成的java文件,加载;
            return (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
        } catch (ClassNotFoundException e) {
            Log.e(TAG, "ClassNotFoundException Not found.");
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
        }
        return null;
    }
}

源码地址:https://github.com/Ulez/AnnotationDemo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值