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

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

  • @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

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

声明注解

1
2
3
4
5
6
7
8
9
10
11
12
13
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 "";
}
1
2
3
4
5
6
7
8
9
10
11
12
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)注解。
我们需要实现四个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@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;
}
}

处理注解逻辑

声明几个属性

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

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

1
2
3
4
5
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}

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

1
2
3
4
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}

getSupportedAnnotationTypes方法中返回支持的注解

1
2
3
4
5
6
7
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
set.add(BindView.class.getCanonicalName());
set.add(ClickResponder.class.getCanonicalName());
return set;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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中生成辅助类并写入文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 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

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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类,调用者通过调用这个类中的方法,完成调用生成辅助类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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,就可以愉快的使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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文件夹下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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


以上


自定义注解,打造自己的框架-下篇
https://blog.huangyuanlove.com/2019/12/04/自定义注解,打造自己的框架-下篇/
作者
HuangYuan_xuan
发布于
2019年12月4日
许可协议
BY HUANG兄