写一个ButterKnife

上篇博客写的是源码分析,但是并没有涉及到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

发布了29 篇原创文章 · 获赞 4 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/s122ktyt/article/details/77504754