写一个ButterKnife

来源:互联网 发布:女朋友 文艺青年 知乎 编辑:程序博客网 时间:2024/06/05 00:22

上篇博客写的是源码分析,但是并没有涉及到apt;这次就来看看使用apt如何写一个ButterKnife;

Part1 注解基础

如果没用过注解或者用的少,推荐下看下http://blog.csdn.net/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

原创粉丝点击