主要分析一下 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()) { dataClass = matcher.group(2 ).trim().replaceAll("<[^>]*>" , "" ); break ; } else { mirror = ((TypeElement) processingEnv.getTypeUtils().asElement(mirror)).getSuperclass(); } } mMessager.printMessage(Diagnostic.Kind.NOTE, "dataClass==" + dataClass); } } }
为了获取到 SugarHolder 泛型中 Model 类的实际类型
通过 TypeMirror 获取到了当前 ViewHolder 的父类
以上面的 BarHolder 为例
1 com.zhihu.android.sugaradapter.SugarHolder<com.zhihu.android.sugaradapterdemo.item.Bar>
用正则匹配的方式,对上述内容按字符串进行查找匹配和匹配,获取到
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 (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 非常灵活的获取各种你所需要的信息,利用这些进行二次的组合,或分解就可以在编译期生成非常有价值的文件,使一些看似无法解决的重复工作变得简单有条理。