自定义注解,打造自己的框架-下篇

2019-12-04 22:53:52

该系列介绍自定义注解,完成如下功能。

  • @BindView 代替 findViewById
  • @ClickResponder 代替 setOnClickListener
  • @LongClickResponder 代替 setOnLongClickListener
  • @IntentValue 代替 getIntent().getXXX
  • @UriValue 代替 getQueryParameter
  • @BroadcastResponder 代替 registerReceiver
  • @RouterModule、@RouterPath 来进行反依赖传递调用

该系列源码在https://github.com/huangyuanlove/AndroidAnnotation

前两篇介绍了一丢丢自定义注解过程中使用到的东西,现在我们正式开始写框架。

结构

一般来讲,注解类框架(在Android)会分成三个部分,

  • annotation模块(java lib)

    用来存放注解类的,对应gradle引用annotationProcessor xxxx

  • compiler模块(java lib)

    用来存放生成辅助类的,对应gradle引用implementation xxxx

  • api模块(Android lib)

    用来提供给使用者的接口,对应gradle引用implementation xxxx,这个模块中会存在大量的反射调用,主要是调用生成的辅助类中的方法。

  • example(lib)模块和app(application)模块

    用来存放demo的,一般会区分在lib和application中的使用

也有一些框架会把api模块和compiler模块放在一块,无所谓了。。。。

首先来创建新的工程,然后创建对应的模块,注意:annotation和compiler模块是java lib,不要创建成Android lib

我们先来实现一下BindViewClickResponder这两个注解

声明注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int id() default -1;
    //在Android lib中生成的R文件中资源id不是final类型的,所以我们换个思路,
    //通过`getResources().getIdentifier()`来实现
    String idStr() default ""; 
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ClickResponder {
    int[] id() default {};
    String[] idStr() default {""};
}

声明注解处理器

声明一个processer类继承avax.annotation.processing.AbstractProcessor类,对这个类使用@AutoService(Processor.class)注解。
我们需要实现四个方法

@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor{

     /**
     * 每个Annotation Processor必须有一个空的构造函数。
     * 编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnvironment参数,
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    /**
     * 用于指定该处理器支持哪些注解
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    /**
     * 用于指定支持的java版本,
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    /**
     * Annotation Processor扫描出的结果会存储进roundEnvironment中,可以在这里获取到注解内容,编写你的操作逻辑。
     * 注意:process()函数中不能直接进行异常抛出,并且该方法会执行多次
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

处理注解逻辑

声明几个属性

private Elements elementUtils;
private Map<TypeElement, List<Element>> bindViewMap = new HashMap<>();
private Map<TypeElement, List<Element>> clickResponderMap = new HashMap<>();

init方法中初始化用到的字段

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

getSupportedSourceVersion方法中返回支持的java版本

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

getSupportedAnnotationTypes方法中返回支持的注解

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

这里为了方便以后在辅助类中添加各种方法,定义了TypeSpecWrapper

public class TypeSpecWrapper {

    private TypeSpec.Builder typeSpecBuilder;
    private String packageName;
    private HashMap<String, MethodSpec.Builder> methodBuildMap;

    public TypeSpec build(){
        for(Map.Entry<String,MethodSpec.Builder> entry:methodBuildMap.entrySet()){
            typeSpecBuilder.addMethod(entry.getValue().build());
        }
        return typeSpecBuilder.build();
    }

    public TypeSpec.Builder setTypeSpecBuilder(TypeSpec.Builder builder){
        this.typeSpecBuilder = builder;
        return builder;

    }

    public MethodSpec.Builder putMethodBuilder(MethodSpec.Builder builder){

        return methodBuildMap.put(builder.build().name,builder);
    }

    public MethodSpec.Builder getMethodBuilder(String methodName){
        return methodBuildMap.get(methodName);
    }

    public void writeTo(Filer filer){
        JavaFile javaFile = JavaFile.builder(packageName, build())
                .build();
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public Map<String, MethodSpec.Builder> getMethodBuildMap(){
        return  methodBuildMap;
    }

    public TypeSpec.Builder getTypeSpecBuilder(){
        return typeSpecBuilder;
    }

    public TypeSpecWrapper(TypeSpec.Builder typeSpecBuilder,String packageName){
        this.typeSpecBuilder = typeSpecBuilder;
        this.packageName = packageName;
        methodBuildMap = new HashMap<>();
    }

}

process中生成辅助类并写入文件,

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
bindViewMap.clear();
clickResponderMap.clear();
Set<? extends Element> bindViewSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Set<? extends Element> onClickSet = roundEnvironment.getElementsAnnotatedWith(ClickResponder.class);

//收集BindView对应的信息
collectBindViewInfo(bindViewSet);
//收集ClickResponder对应的信息
collectClickResponderInfo(onClickSet);
//生成辅助类代码
generateCode();
//将生成代码写入文件
for (Map.Entry<TypeElement, TypeSpecWrapper> entry : typeSpecWrapperMap.entrySet()) {
          entry.getValue().writeTo(processingEnv.getFiler());
      }
return true;
}

因为会有多个类使用同一个注解,这里需要根据使用该注解的类名来保存对应的注解信息

private void collectBindViewInfo(Set<? extends Element> elements) {
    for (Element element : elements) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        List<Element> elementList = bindViewMap.get(typeElement);
        if (elementList == null) {
            elementList = new ArrayList<>();
            bindViewMap.put(typeElement, elementList);
        }
        elementList.add(element);
    }
}

private void collectClickResponderInfo(Set<? extends Element> elements) {
    for (Element element : elements) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        List<Element> elementList = clickResponderMap.get(typeElement);
        if (elementList == null) {
            elementList = new ArrayList<>();
            clickResponderMap.put(typeElement, elementList);
        }
        elementList.add(element);
    }
}

生成辅助类的内容,这里为了简单,将BindViewClickResponder以及之后的LongClickResponderCode注解处理都放在了bind方法中

 private void generateCode() {
    generateBindViewCode();
    generateClickResponderCode();
 }

private void generateBindViewCode() {
    for (TypeElement typeElement : bindViewMap.keySet()) {
        MethodSpec.Builder methodBuilder = generateBindMethodBuilder(typeElement);

        List<Element> elements = bindViewMap.get(typeElement);
        for (Element element : elements) {
            processorBindView(element, methodBuilder);
        }
    }
}

private void generateClickResponderCode() {
    for (TypeElement typeElement : clickResponderMap.keySet()) {
        MethodSpec.Builder methodBuilder = generateBindMethodBuilder(typeElement);

        List<Element> elements = clickResponderMap.get(typeElement);
        for (Element element : elements) {
            processorClickResponder(element, methodBuilder);

        }
    }

}

生成对应的辅助类,类名为使用该注解的类名+$ViewInjector

private TypeSpecWrapper generateTypeSpecWrapper(TypeElement typeElement) {
    final String pkgName = getPackageName(typeElement);
    final String clsName = getClassName(typeElement, pkgName) + "$ViewInjector";
    TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(clsName)
            .addModifiers(Modifier.PUBLIC);

    TypeSpecWrapper typeSpecWrapper = typeSpecWrapperMap.get(typeElement);
    if (typeSpecWrapper == null) {
        typeSpecWrapper = new TypeSpecWrapper(typeSpecBuilder, pkgName);
        typeSpecWrapperMap.put(typeElement, typeSpecWrapper);
    }
    return typeSpecWrapper;
}

生成辅助类的bind方法

private MethodSpec.Builder generateBindMethodBuilder(TypeElement typeElement) {
    TypeSpecWrapper typeSpecWrapper = generateTypeSpecWrapper(typeElement);
    MethodSpec.Builder methodBuilder = typeSpecWrapper.getMethodBuilder("bind");
    if (methodBuilder == null) {
        methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addParameter(ClassName.get(typeElement.asType()), "target", Modifier.FINAL)
                .addParameter(ClassName.get("android.view", "View"), "view")
                .addStatement("int resourceID = 0");
        typeSpecWrapper.putMethodBuilder(methodBuilder);
    }
    return methodBuilder;

}

对于BindView来讲,就是通过findViewById来获取到对应的控件,然后赋值给对应的字段。
这里我们为了能在lib中使用,对于没有传入id的属性,通过getIdentifier方法来获取到对应的资源id。

private void processorBindView(Element element, MethodSpec.Builder methodBuilder) {
    VariableElement variableElement = (VariableElement) element;
    String varName = variableElement.getSimpleName().toString();
    String varType = variableElement.asType().toString();
    BindView bindView = variableElement.getAnnotation(BindView.class);

    int params = bindView.id();
    //使用注解没有传入id的属性值,则使用isStr值来获取对应的资源id。
    //严谨来讲,这里需要判断一下isStr是不是空串,如果是空串,则直接抛出异常,终止编译
    if (params <= 0) {
        String idStr = bindView.idStr();
        methodBuilder.addStatement("resourceID = view.getResources().getIdentifier($S,$S, view.getContext().getPackageName())", idStr, "id");

    } else {
        methodBuilder.addStatement("resourceID = ($L)", params);
    }
    methodBuilder.addStatement("target.$L = ($L) view.findViewById(resourceID)", varName, varType);

}

对于ClickResponder来讲,就是通过setOnClickListener对对应的控件设置点击方法。由于可能存在多个控件使用同一个响应点击的方法,这里传入的都是资源数组
同样我们为了能在lib中使用,对于没有传入id的属性,通过getIdentifier方法来获取到对应的资源id。

private void processorClickResponder(Element element, MethodSpec.Builder methodBuilder) {
    ExecutableElement executableElement = (ExecutableElement) element;
    ClickResponder clickView = executableElement.getAnnotation(ClickResponder.class);
    int[] ids = clickView.id();
    String[] idStrs = clickView.idStr();


    if (ids.length > 0) {

        for (int id : ids) {
            if (id == 0) {
                continue;
            }
            MethodSpec innerMethodSpec = MethodSpec.methodBuilder("onClick")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addParameter(ClassName.get("android.view", "View"), "v")
                    .addStatement("target.$L($L)", executableElement.getSimpleName().toString(), "v")
                    .build();
            TypeSpec innerTypeSpec = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(ClassName.bestGuess("View.OnClickListener"))
                    .addMethod(innerMethodSpec)
                    .build();
            methodBuilder.addStatement("view.findViewById($L).setOnClickListener($L)", id, innerTypeSpec);
        }
    }
    if (idStrs.length > 0) {

        for (String idStr : idStrs) {
            if (idStr == null || idStr.length() <= 0) {
                continue;
            }

            MethodSpec innerMethodSpec = MethodSpec.methodBuilder("onClick")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addParameter(ClassName.get("android.view", "View"), "v")
                    .addStatement("target.$L($L)", executableElement.getSimpleName().toString(), "v")
                    .build();
            TypeSpec innerTypeSpec = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(ClassName.bestGuess("View.OnClickListener"))
                    .addMethod(innerMethodSpec)
                    .build();

            methodBuilder.addStatement("resourceID = view.getResources().getIdentifier($S,$S, view.getContext().getPackageName())", idStr, "id");

            methodBuilder.addStatement("view.findViewById($L).setOnClickListener($L)", "resourceID", innerTypeSpec);

        }
    }
}
给使用者提供调用方法

api模块中定义提供给使用者的方法。
新建一个ViewInjector类,调用者通过调用这个类中的方法,完成调用生成辅助类的方法

public class ViewInjector {
    static final Map<Class<?>, Method> BINDINGS = new LinkedHashMap<>();

    public static void bind(Activity activity) {
        bind(activity, activity.getWindow().getDecorView());
    }

    public static void bind(Object target, View view) {
        Method constructor = findBindMethodForClass(target);
        try {
            constructor.invoke(null,target, view);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Method findBindMethodForClass(Object target) {
        Method constructor = BINDINGS.get(target.getClass());
        if (constructor == null) {
            try {
                Class<?> bindingClass = Class.forName(target.getClass().getName() + "$ViewInjector");
                constructor = bindingClass.getMethod("bind",target.getClass(), View.class);
                BINDINGS.put(target.getClass(), constructor);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return constructor;
    }
}
使用

在我们项目的主模块(application模块)中新建一个Activity,就可以愉快的使用了


public class TestViewInjectActivityTwo extends Activity {
    @BindView(id = R.id.test_view_inject_one)
    protected Button buttonOne;
    @BindView(idStr = "test_view_inject_two")
    protected Button buttonTwo;
    。
    。
    。
    @ClickResponder(id = {R.id.test_view_inject_one})
    public void onClickButtonOne(View v) {
        Toast.makeText(TestViewInjectActivity.this, "test_view_inject_one", Toast.LENGTH_SHORT).show();
    }

    @ClickResponder(idStr = {"test_view_inject_two"})
    public void onClickButtonTwo(View v) {
        Toast.makeText(TestViewInjectActivity.this, "test_view_inject_two", Toast.LENGTH_SHORT).show();
    }

执行一下assembleDebug任务,就可以找到TestViewInjectActivityTwo$ViewInjector类了(一般是在对用模块中的build/generated/source/apt/debug)文件夹下,当然,assembleRelease会在build/generated/source/apt/release文件夹下。

public class TestViewInjectActivity$ViewInjector {
  public static void bind(final TestViewInjectActivity target, View view) {
    int resourceID = 0;
    resourceID = (2131165388);
    target.buttonOne = (android.widget.Button) view.findViewById(resourceID);
    resourceID = view.getResources().getIdentifier("test_view_inject_two","id", view.getContext().getPackageName());
    target.buttonTwo = (android.widget.Button) view.findViewById(resourceID);

    view.findViewById(2131165388).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        target.onClickButtonOne(v);
      }
    });
    resourceID = view.getResources().getIdentifier("test_view_inject_two","id", view.getContext().getPackageName());
    view.findViewById(resourceID).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        target.onClickButtonTwo(v);
      }
    });
  }

其实生成的代码还是有优化空间的,比如对于一个既用了BindView又用了ClickResponder的控件,对应的findViewById会执行两次,这里可以优化一下.

可以自己写一下LongClickResponder注解呀,代码在https://github.com/huangyuanlove/AndroidAnnotation


以上

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值