安卓to鸿蒙系列:ButterKnife(二)

系统
想读懂ButterKnife难度还是很大的,一方面自己菜,另一方面代码量大,代码结构复杂。而且很多概念不好理解(比如javapoet引入的各种类,绑定信息相关的几个类BingdingSet、BingdingSet.Builder、xxxBingding等)。

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

本文基于https://gitee.com/openharmony-tpc/butterknife 分析JakeWharton/butterknife的源码,及移植到鸿蒙需要做的工作。

如果对apt的概念及实践不熟悉,请先移步:安卓to鸿蒙系列:ButterKnife(一),然后再来阅读本文,会事半功倍!

butterknife项目结构:

和我们在安卓to鸿蒙系列:ButterKnife(一)写的“乞丐版BufferKnife”一样,主要由三个module组成

  • butterknife_annotations//编译时、运行时都用到

定义了注解

  • butterknife_compiler//编译时用到

apt的主要实现部分。注解的解析、处理,生成模板文件

  • butterknife//运行时用到

对外的工具类,供用户使用,完成注入操作。 butterknife_runtime 定义运行时用到的一些类及工具方法。

移植butterknife_annotations

直接对比openharmony-tpc和JakeWharton的这个module吧:

可以看到有增、删、改。

其中增加和删除的是一一对应的。

  1. BindBitmap vs BindPixelMap//安卓的Bitmap对应鸿蒙的PixelMap 
  2.  
  3. BindView vs BindComponent//安卓的View对应鸿蒙的Component 
  4.  
  5. BindViews vs BindComponents//同上 
  6.  
  7. BindDrawable vs BindElement //安卓的Drawable对应鸿蒙的ohos.agp.components.element.Element 

其它都是修改,以BindingString为例,只是去掉了鸿蒙没有的注解StringRes、修改注释。

ps:对于鸿蒙没有的注解,还有一个办法就是把androidx或support包下相应的文件copy进来,并且包名也保持一致,这样我们就不需要修改BindingString这一类文件了。经过对比发现,可以减少不小的工作量。

还有一些修改稍复杂一些,以OnClick为例。只要写过两个平台的代码,还是很容易理解的,只是做相应的等价替换。

分析butterknife_compiler的源码

优秀资源参考:

静态分析

1.主要的几个类

  • ButterKnifeProcessor//注解入口类,apt程序必须继承AbstractProcessor,没什么好说的。
  • BindingSet//从名字可知,这个类是绑定信息的集合。举例:MainAbilitySlice对应一个BindingSet,也就对应一个xxxx_ViewBinding。

BindingSet的实例存在于编译期,执行它的brewJava()方法生成xxxx_ViewBinding文件。

  • 各种XxxBinding,如:FieldViewBinding、MethodViewBinding、ResourceBinding的各种子类,表示某字段的绑定信息。

以MainAbilitySlice和FieldViewBinding为例,如下注解代码:

  1. //MainAbilitySlice 
  2. @BindComponent(ResourceTable.Id_viewRoot) 
  3. DirectionalLayout mDlViewRoot; 

 生成一个FieldViewBinding实例,其值为:

  1. final class FieldViewBinding implements MemberViewBinding { 
  2.     private final String name;//"mDlViewRoot" 
  3.     private final TypeName type;//DirectionalLayout 
  4.     private final boolean required;//true 
  • ViewBinding//表示某个控件的绑定信息,其中包括field和method(对于各种事件绑定)。如下代码所示:
  1. final class ViewBinding { 
  2.     private final Id id; 
  3.     private final Map<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>> methodBindings; 
  4.     private final FieldViewBinding fieldBinding; 

2.ButterKnifeProcessor#process()方法相当注解执行的main方法(会进入多次),主要干了两件事findAndParseTargets()和brewJava(),如下图所示:

用到的工具:SequenceDiagram - IntelliJ IDEA插件,直接在插件市场搜索、安装就行,用来生成方法调用时序图

3.其中ButterKnifeProcessor#findAndParseTargets()的主要功能是找到并解析各个targets,如下图所示:

ps:这个图省略了很多parseXXX()方法,只保留了一个parseBindComponent(),因为功能类似。不然图太长了。

由上图可知findAndParseTargets()方法实现了以下三件事:

解析注入对象相关的注解parseXXX()

处理@BindComponent,@BindString 之类的组件或资源

  1. //添加注释、删掉多余代码的parseBindComponent() 
  2. private void parseBindComponent(Element element, Map<TypeElement, BindingSet.Builder> builderMap, 
  3.                                 Set<TypeElement> erasedTargetNames) { 
  4.     //获取当前元素element的类级别的元素,即XXXAbility 
  5.     TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); 
  6.     
  7.     //获取@BindComponent的值,即ResourceTable.Id_xxx 
  8.     int id = element.getAnnotation(BindComponent.class).value(); 
  9.      
  10.     //根据当前元素的类级别的元素先从Map中获取BindingSet的内部类Builder 
  11.     BindingSet.Builder builder = builderMap.get(enclosingElement); 
  12.     Id resourceId = elementToId(element, BindComponent.class, id); 
  13.      
  14.     //builder为空,说明当前类还没有对应的value,需要new一个出来,并放到builderMap中 
  15.     //builder会被BindComponent和OnClick等共用一个(享元模式?),并且它们以XXXAbility分组放在builderMap中。 
  16.     if (builder != null) { 
  17.         String existingBindingName = builder.findExistingBindingName(resourceId); 
  18.         if (existingBindingName != null) { 
  19.             return
  20.         } 
  21.     } else { 
  22.         builder = getOrCreateBindingBuilder(builderMap, enclosingElement); 
  23.     } 
  24.  
  25.     //@BindComponent修饰的字段的简单名称,即变量名比如mTextView 
  26.     String name = simpleName.toString(); 
  27.     //@BindComponent修饰的字段的类型,比如Text 
  28.     TypeName type = TypeName.get(elementType); 
  29.     //是否被@Nullable修饰 
  30.     boolean required = isFieldRequired(element); 
  31.  
  32.     //调用BindingSet的内部类Builder中的addField方法封装解析的信息 
  33.     builder.addField(resourceId, new FieldViewBinding(name, type, required)); 
  34.      
  35.     // Add the type-erased version to the valid binding targets set
  36.     //给所有含有自定义注解的类组成的Set集合中添加元素 
  37.     erasedTargetNames.add(enclosingElement); 
  • 解析事件绑定相关的注解findAndParseListener()

处理@OnClick,@OnItemClick,@OnTextChanged 之类的Listener

通过以上两步,完成了注解信息的扫描收集,并将解析的信息保存到builderMap和erasedTargetNames两个集合中;

  • findAllSupertypeBindings(),findParentType(),及findAndParseTargets()

第三步,对上面提到的builderMap和erasedTargetNames两个集合中的信息进行重新整理,最终返回一个以TypeElement为key,BindingSet为vaule的bindingMap集合。

下面直接帖学习笔记ButterKnife的分析吧:

  1. private Map<TypeElement, BindingSet> findAndParseTargets1(RoundEnvironment env) { 
  2.     //这个不是最后返回的对象,这个只是BindingSet对应的Builder类,保存了BindComponent、BindString、OnClick等等相关的绑定信息 
  3.     Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); 
  4.     Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); 
  5.  
  6.     //隐藏代码 @BindXxx-------------------- 
  7.  
  8.     //绑定界面上的View 
  9.     for (Element element : env.getElementsAnnotatedWith(BindComponent.class)) { 
  10.         parseBindComponent(element, builderMap, erasedTargetNames); 
  11.     } 
  12.  
  13.     //隐藏代码 bindListener.--------------- 
  14.  
  15.     Map<TypeElement, ClasspathBindingSet> classpathBindings = 
  16.             findAllSupertypeBindings(builderMap, erasedTargetNames); 
  17.     //组合所有类的关系  组成 树 
  18.     //这里注释也写了,用队列的方式,将超类与子类绑定,从根开始 
  19.     // Associate superclass binders with their subclass binders. This is a queue-based tree walk 
  20.     // which starts at the roots (superclasses) and walks to the leafs (subclasses). 
  21.     //这个获取的所有的“类”  放入了队列 
  22.     Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries = 
  23.             new ArrayDeque<>(builderMap.entrySet()); 
  24.     //即将返回的对象 
  25.     Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>(); 
  26.     while (!entries.isEmpty()) { 
  27.         Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst(); 
  28.  
  29.         TypeElement type = entry.getKey(); 
  30.         BindingSet.Builder builder = entry.getValue(); 
  31.         //拿队列出来的第一个 在erasedTargetNames中查询,父类是否在这个这个集合里 
  32.         //为什么要这样? 因为可能 你在父类中 绑定了一个String 
  33.         //在子类中使用了这个String,所以必须先初始化父类的String 
  34.         //翻看生成好的代码来看,初始化都是在构造函数中进行绑定的 
  35.         //所以考虑继承情况,必须把所有的类进行一个关联。 
  36.         TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet()); 
  37.         if (parentType == null) { 
  38.             //如果没有父类,则直接 放入 
  39.             bindingMap.put(type, builder.build()); 
  40.         } else { 
  41.             //如果父类有绑定 
  42.             //再从bindingMap(即将返回的对象)中取看看 是否已经放进去了 
  43.             BindingInformationProvider parentBinding = bindingMap.get(parentType); 
  44.             if (parentBinding == null) { 
  45.                 //如果没绑定进去 再从classpathBindings中取 一个父类 
  46.                 parentBinding = classpathBindings.get(parentType); 
  47.             } 
  48.             if (parentBinding != null) { 
  49.                 //如果这个父类不是空的 则和当前循环里的builder  子、父类绑定 
  50.                 builder.setParent(parentBinding); 
  51.                 //放入即将返回的map里 
  52.                 bindingMap.put(type, builder.build()); 
  53.             } else { 
  54.                 //翻译是:有个超类绑定,但还没有构建它,放到后面继续排队 
  55.                 // Has a superclass binding but we haven't built it yet. Re-enqueue for later. 
  56.                 //比如:子类继承父类都有绑定,这时候子类需要关联父类,父类还没初始化, 
  57.                 //把子类放到队尾,等父类初始化完成在进行关联 
  58.                 entries.addLast(entry); 
  59.             } 
  60.         } 
  61.     } 
  62.     //返回,这时候这个map的key 就全是“类信息” 
  63.     //value就是获取好的 当前类里面需要绑定的内容 
  64.     //并且 “类” 也已经做好了继承关系 
  65.     return bindingMap; 

总结一下这个方法,就像它的方法名一 样 “找到并解析各个targets” 。这里有个疑问,targets是什么呢? targets就是待注入的字段、待绑定事件的方法, 比如下面代码中的mTextView就是target,即“待注入的字段”

  1. @BindComponent(ResourceTable.Id_tv_hello) 
  2. Text mTextView; 

动态分析

动态分析主要是验证一下上面静态分析的结论。对于比较复杂的代码,需要debug跟一下代码,查看运行时关键变量的值。

怎么调试butterknife_compiler?

参考:https://www.w3ma.com/how-to-debug-an-annotation-processor-in-android-studio/

1.新建一个remote debug,比如命名为aptDebug

因为apt过程在编译期,所以需要remote debug。什么是remote debug,可以自己google一下。

2.在butterknife根目录的命令行中运行gradlew --no-daemon -Dorg.gradle.debug=true :entry:clean :entry:compileDebugJavaWithJavac,编译过程处于等待调试的状态,如下如:

:entry:clean加上它是表示重新构建。

-Dorg.gradle.debug=true设置为true时,Gradle将在启用远程调试的情况下运行构建,侦听端口5005。这等效于将-agentlib:jdwp = transport = dt_socket,server = y,suspend = y,address = 5005添加到 JVM命令行,它将挂起虚拟机,直到连接了调试器。

3.设置断点,然后点击小虫子debug按钮。

4.以debug parseBindComponent()为例:

小技巧:条件断点在这里会提高调试的效率。自己google一下。

通过debug跟代码,可知void parseBindComponent(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames)方法的输入element为@BindComponent注解的java元素,输出builderMap和erasedTargetNames。

其中BinderingSet$Builder为BinderingSet的构建者,这里用到了Builder构建者设计模式。

其中BinderingSet记录了模板类XXX_ViewBinding的生成规则。

在parseBindComponent方法中,调用Builder的addField添加了一条生成java文件的规则,即注入View。其它的parseXXX()方法类似。总结一下:

  1. parseBindComponent()//调用Builder的`addField`,注入View。 
  2. parseResourceXXX()//调用Builder的`addResource`,注入各种资源,如String、intfloat、Dimen、Color、Array、PixelMap等。 
  3. findAndParseListener()//调用Builder的`addMethod`,绑定各种事件。 

5.debug Map<TypeElement, BindingSet> findAndParseTargets()

先说上面提到 targets是什么呢?,通过跟代码,可知targets是被注解的Element, 比如MainAblilitySlice中的mDlViewRoot

  1. @BindComponent(ResourceTable.Id_viewRoot) 
  2. DirectionalLayout mDlViewRoot; 

方法的返回值是类和它的模板类的一一对应。

  1. "com.example.butterknife.slice.MainAbilitySlice" -> "com.example.butterknife.slice.MainAbilitySlice_ViewBinding" 

日志分析:

debug的效率其实很低。听说10倍程序员都爱打日志。所以,我们也要知道apt的messager的用法及注意事项有哪些?

虽然System.out.println();也可以打日志。但是messager会根据日志类型,把Kind.WARNING和Kind.ERROR类型的日志做统计,方便我们定位问题。

在ButterKnifeProcessor中有封装messager的几个方法:

  1. private void error(Element element, String message, Object... args) { 
  2.     printMessage(Kind.ERROR, element, message, args); 
  3.  
  4. private void note(Element element, String message, Object... args) { 
  5.     printMessage(Kind.NOTE, element, message, args); 
  6.  
  7. private void printMessage(Kind kind, Element element, String message, Object[] args) { 
  8.     if (args.length > 0) { 
  9.         message = String.format(message, args); 
  10.     } 
  11.  
  12.     processingEnv.getMessager().printMessage(kind, message, element); 

注意: 一定要执行gradlew --no-daemon :entry:clean :entry:compileDebugJavaWithJavac,compileDebugJavaWithJavac这个task。而且加上clean。不然看不到日志。

向apt程序传参:

在ButterKnifeProcessor中覆写了getSupportedOptions(),这样我们可以向apt传参了。

  1. @Override 
  2. public Set<String> getSupportedOptions() { 
  3.     ImmutableSet.Builder<String> builder = ImmutableSet.builder(); 
  4.     builder.add(OPTION_SDK_INT, OPTION_DEBUGGABLE); 
  5.     if (trees != null) { 
  6.         builder.add(IncrementalAnnotationProcessorType.ISOLATING.getProcessorOption()); 
  7.     } 
  8.     return builder.build(); 

 传参方法:在entry中

  1. ohos { 
  2.     compileSdkVersion 5 
  3.     defaultConfig { 
  4.         compatibleSdkVersion 5 
  5.  
  6.         javaCompileOptions { 
  7.             annotationProcessorOptions { 
  8.                 arguments = ['butterknife.debuggable'"true"
  9.             } 
  10.         } 
  11.     } 

在init()方法中,我们可以取出传入的值:

  1. @Override 
  2. public synchronized void init(ProcessingEnvironment env) { 
  3.     super.init(env); 
  4.     debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE)); 
  5.     env.getMessager().printMessage(Kind.NOTE, "--------------- debuggable = "+debuggable); 

移植butterknife_compiler

通过上面的代码分析,我们知道在安卓和鸿蒙上butterknife_compiler的基本流程没有变,所以移植工作要做的就是因为平台class及api不同,做一些相应的调整。

对比代码后,差异不大。简单列举一下:

  1. findViewById的修改
  2. Resource相关api的修改
  3. Font的修改(NORMAL变成REGULAR)
  4. BindAnim在鸿蒙版上暂不支持
  5. COLOR_STATE_LIST在鸿蒙版上暂不支持

移植butterknife_runtime

butterknife_runtime同时被butterknife_compiler和butterknife两个module依赖,其中大多是一些工具类,同样,做相应的api修改就可以。

对比代码后,差异比较大。但是都集中的对资源Resource的加载差异上,以及androidx.annotation.UiThread之类的注解(直接删掉就好)。

移植butterknife

butterknife这个module只有一个类ButterKnife,这个类的作用就是通过反射实例化XXX_ViewBinding,并提供一系列静态方法如Unbinder bind(Ability target)来实现target中变量的注入和方法的绑定。同样,做相应的api修改就可以。

欢迎有兴趣的朋友可以完善entry中的用例

目前该库有一些bug,比如:Unbinder bind(Ability target)注入Ability会失败。我相信,bug不止这一个。

发现bug,提issue。我们一起将它完善。

总结

距离写完安卓to鸿蒙系列:ButterKnife(一)已经有两个多月,当时,写一个乞丐版ButterKnife觉得还是很easy的。但是,想读懂ButterKnife难度还是很大的,一方面自己菜,另一方面代码量大,代码结构复杂。而且很多概念不好理解(比如javapoet引入的各种类,绑定信息相关的几个类BingdingSet、BingdingSet.Builder、xxxBingding等)。

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-05-11 14:43:16

鸿蒙HarmonyOS应用

2021-04-26 09:46:10

鸿蒙HarmonyOS应用

2021-04-27 09:22:28

鸿蒙HarmonyOS应用

2019-06-20 16:07:12

鸿蒙安卓操作系统

2020-09-10 09:30:03

鸿蒙安卓操作系统

2013-12-12 16:51:43

安卓进化AndroidGoogle

2019-07-12 16:00:25

华为禁令开发

2021-05-18 15:44:13

IOS安卓鸿蒙

2021-06-04 05:13:22

鸿蒙

2019-06-11 13:41:45

华为鸿蒙操作系统

2024-01-31 17:52:04

2022-07-28 12:06:46

HarmonyOS华为

2022-07-04 10:59:59

安卓鸿蒙操作系统

2019-07-19 16:57:00

华为禁令开发

2014-08-04 14:21:22

安卓架构

2019-07-12 10:15:36

华为鸿蒙安卓

2021-05-07 16:01:38

安卓鸿蒙华为

2016-12-14 14:43:11

ButterknifeAndroid

2012-02-06 10:10:40

安卓iOS美国市场

2020-10-29 09:49:32

HarmonyOS鸿蒙安卓应用
点赞
收藏

51CTO技术栈公众号