0%

SugarAdapter 注解处理器分析

主要分析一下 sugaradapter—processor 是如何处理 Layout 这个注解的。ID 注解平时不太用(主要感觉不太稳定),同时现在在 kotlin 中,findviewbyId 已然不是什么问题,所以 ID 注解就不分析了,有兴趣的同学可以自己看看。

关于注解处理器 APT 的使用及基础使用在注解处理器APT初探已有过分析,在这里不再展开叙述,只重点描述
process 方法的实现。

SugarAdapter 注解处理器实现分析

注解

Layout

1
2
3
4
5
6
7
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
@Inherited
public @interface Layout {
@LayoutRes
int value() default 0;
}

可以看到 Layout 是支持继承的

思考

1
2
3
4
5
@Layout(R.layout.layout_bar)
public final class BarHolder extends SugarHolder<Bar> {

.... init and bindData stuff...
}

假设你对注解处理器 APT 使用已有一定的了解,那么从上面这个 BarHolder 类中,
如何获取 R.layout.layout_bar 这个资源的完整路径呢?又该如何获取 Bar 这个类的完整路径呢?
可以简单思考一下 🤔🤔🤔🤔🤔🤔🤔🤔🤔

process 的实现

DataClass 的解析

数据类的解析

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

private static final Pattern TYPE_PARAM_PATTERN = Pattern.compile("(.*?)<(.*?)>");

private Map<String, Set<String>> processLayout(@NonNull RoundEnvironment roundEnv) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "processLayout begin");

for (Element element : roundEnv.getElementsAnnotatedWith(Layout.class)) {
if (element instanceof TypeElement) {
String holderClass = ((TypeElement) element).getQualifiedName().toString();

String dataClass = null;
TypeMirror mirror = ((TypeElement) element).getSuperclass();
while (mirror != null && !(mirror instanceof NoType)) {
Matcher matcher = TYPE_PARAM_PATTERN.matcher(mirror.toString());
if (matcher.matches()) {
// remove generic type from dataClass
dataClass = matcher.group(2).trim().replaceAll("<[^>]*>", "");
break;
} else {
mirror = ((TypeElement) processingEnv.getTypeUtils().asElement(mirror)).getSuperclass();
}
}

mMessager.printMessage(Diagnostic.Kind.NOTE, "dataClass==" + dataClass);
}
}
}

为了获取到 SugarHolder 泛型中 Model 类的实际类型

  1. 通过 TypeMirror 获取到了当前 ViewHolder 的父类

以上面的 BarHolder 为例

1
com.zhihu.android.sugaradapter.SugarHolder<com.zhihu.android.sugaradapterdemo.item.Bar>
  1. 用正则匹配的方式,对上述内容按字符串进行查找匹配和匹配,获取到
1
com.zhihu.android.sugaradapterdemo.item.Bar

如果这个类还有泛型,比如 Bar<*> ,则会直接擦除掉(用空字符串替代)

当然,由于 Layout 支持继承,所以这里直接获取当前 ViewHolder 的父类,可能会存在找不泛型的情况,因此需要不断回溯查找父类。

第 2 步,理论上也可以通过 ((Type.ClassType) mirror).getTypeArguments().head 的方式拿到,但这是具体实现细节,用正则匹配也是可以的。

LayoutRes 的解析

资源文件的解析

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
private Map<String, Set<String>> processLayout(@NonNull RoundEnvironment roundEnv) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "processLayout begin");

RParser parser = RParser.builder(processingEnv)
.setSupportedAnnotations(Collections.singleton(Layout.class))
.setSupportedTypes("layout")
.build();

parser.scan(roundEnv);

for (Element element : roundEnv.getElementsAnnotatedWith(Layout.class)) {
if (element instanceof TypeElement) {
String holderClass = ((TypeElement) element).getQualifiedName().toString();
int layoutRes = element.getAnnotation(Layout.class).value();

if (layoutRes == 0 || dataClass == null) {
throw new IllegalStateException("process " + holderClass + " failed!");
}

String layoutResStr = null;
String packageName = null;
for (String path : holderClass.split("\\.")) {
if (packageName == null) {
packageName = path;
} else {
packageName = packageName + "." + path;
}

layoutResStr = parser.parse(packageName, layoutRes);
if (!layoutResStr.equals(String.valueOf(layoutRes))) {
break;
}
}
mMessager.printMessage(Diagnostic.Kind.NOTE, "layoutResStr==" + layoutResStr);
}
}
}

关于资源文件的解析,你可能觉得很简单,但其实不简单

1
int layoutRes = element.getAnnotation(Layout.class).value();

通过注解直接获取的资源文件 value 是一段类似 34343438943 的数字,这个是系统自动在 R 文件中生成的。因此这里获取的 layoutRes 是一个 int 值,并不能直接拿来使用。然而我们最终需要的是 packagename.R.layout.xxx 这样的文件(在组件化开发或依赖第三方库时,你无法避免别人和你定义了一个同名的资源文件,所以这里需要带包名的资源路径,以做区分)。

因此这里借助HendraAnggrian/r-parser 实现了这个功能。

1
2
3
4
layoutResStr = parser.parse(packageName, layoutRes);
if (!layoutResStr.equals(String.valueOf(layoutRes))) {
break;
}

parser.parse 会根据包名和资源 id 去查找具体的资源路径名。这里通过拼接包名的方式,不断查找这个资源。

通过上述过程,就完成了 DataClass 和 Reslayot 完整路径的获取。以 BarViewHolder 为例

1
2
3
4
5
@Layout(R.layout.layout_bar)
public final class BarHolder extends SugarHolder<Bar> {

.... init and bindData stuff...
}

正常情况下,可以得到如下结果

1
2
3
4
dataClass = com.zhihu.android.sugaradapterdemo.item.Bar.class

layoutResStr = com.zhihu.android.sugaradapterdemo.R.layout.layout_bar

生成编译期文件

有了这两个最总要的内容,就可以用来生成编译期的文件了。

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
private Map<String, Set<String>> processLayout(@NonNull RoundEnvironment roundEnv) {


Map<String, Pair> containerMap = new HashMap<>();

for (Element element : roundEnv.getElementsAnnotatedWith(Layout.class)) {
if (element instanceof TypeElement) {
String holderClass = ((TypeElement) element).getQualifiedName().toString();
int layoutRes = element.getAnnotation(Layout.class).value();

String dataClass = null;

... detail stuff...


if (layoutResStr == null || layoutResStr.equals(String.valueOf(layoutRes))) {
throw new IllegalStateException("process " + holderClass + " failed!");
}

containerMap.put(holderClass, new Pair(layoutResStr, dataClass));
}
}

generateContainerDelegateImpl(containerMap)

.....
}

generateContainerDelegateImpl

关于编译期文件的生成,可以用 javapoet ,当然也可以直接按最终需要的格式,进行字符串拼接,都是可以的。这里假设我们只有 BarViewHolder 这一个类,那么最终生成的文件,就是如下ContainerDelegateImpl.java

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
private void generateContainerDelegateImpl(@NonNull Map<String, Pair> map) throws IOException {
StringBuilder builder = new StringBuilder();
String packageName = "com.zhihu.android.sugaradapter";
builder.append("package ").append(packageName).append(";\n\n");


...import...

// for module project

... 变量声明...

for (String key : map.keySet()) {
String layoutResStr = map.get(key).getFirst();
String dataClass = map.get(key).getSecond();
builder.append(" mLayoutResMap.put(").append(key).append(".class, ")
.append(layoutResStr).append(");\n");
builder.append(" mDataClassMap.put(").append(key).append(".class, ")
.append(dataClass).append(".class);\n");
}

... getxxx method stuff...



JavaFileObject object = processingEnv.getFiler().createSourceFile(packageName + "." + className);
Writer writer = object.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
}

ContainerDelegateImpl.java

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
public final class ContainerDelegateImpl implements ContainerDelegate {
private Map<Class<? extends SugarHolder>, Integer> mLayoutResMap;
private Map<Class<? extends SugarHolder>, Class> mDataClassMap;

public ContainerDelegateImpl() {
mLayoutResMap = new HashMap<>();
mDataClassMap = new HashMap<>();


mLayoutResMap.put(com.zhihu.android.sugaradapterdemo.holder.BarHolder.class, com.zhihu.android.sugaradapterdemo.R.layout.layout_bar);
mDataClassMap.put(com.zhihu.android.sugaradapterdemo.holder.BarHolder.class, com.zhihu.android.sugaradapterdemo.item.Bar.class);
}

@NonNull
public Map<Class<? extends SugarHolder>, Integer> getLayoutResMap() {
return mLayoutResMap;
}

@NonNull
public Map<Class<? extends SugarHolder>, Class> getDataClassMap() {
return mDataClassMap;
}

@Override
@LayoutRes
public int getLayoutRes(@NonNull Class<? extends SugarHolder> holderClass) {
return mLayoutResMap.get(holderClass);
}

@Override
@NonNull
public Class getDataClass(@NonNull Class<? extends SugarHolder> holderClass) {
return mDataClassMap.get(holderClass);
}
}

总结

以上就是 sugaradapter-processor 的分析,可以看到关于注解处理器的使用是非常灵活的,在编译期根据 RoundEnvironment 接口,Element 接口及其子类提供的 API 非常灵活的获取各种你所需要的信息,利用这些进行二次的组合,或分解就可以在编译期生成非常有价值的文件,使一些看似无法解决的重复工作变得简单有条理。

加个鸡腿呗.