安卓to鸿蒙系列:ButterKnife(二) 原创 精华
目录
本文基于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吧:
可以看到有增、删、改。
-
其中增加和删除的是一一对应的。
BindBitmap
vsBindPixelMap
//安卓的Bitmap
对应鸿蒙的PixelMap
BindView
vsBindComponent
//安卓的View
对应鸿蒙的Component
BindViews
vsBindComponents
//同上BindDrawable
vsBindElement
//安卓的Drawable
对应鸿蒙的ohos.agp.components.element.Element
-
其它都是修改,以
BindingString
为例,只是去掉了鸿蒙没有的注解StringRes
、修改注释。
ps:对于鸿蒙没有的注解,还有一个办法就是把androidx或support包下相应的文件copy进来,并且包名也保持一致,这样我们就不需要修改
BindingString
这一类文件了。经过对比发现,可以减少不小的工作量。 -
还有一些修改稍复杂一些,以
OnClick
为例。只要写过两个平台的代码,还是很容易理解的,只是做相应的等价替换。
分析butterknife_compiler
的源码
优秀资源参考:
静态分析
-
主要的几个类
-
ButterKnifeProcessor
//注解入口类,apt程序必须继承AbstractProcessor
,没什么好说的。 -
BindingSet
//从名字可知,这个类是绑定信息的集合。举例:MainAbilitySlice
对应一个BindingSet
,也就对应一个xxxx_ViewBinding
。BindingSet
的实例存在于编译期,执行它的brewJava()
方法生成xxxx_ViewBinding
文件。 -
各种
XxxBinding
,如:FieldViewBinding
、MethodViewBinding
、ResourceBinding
的各种子类,表示某字段的绑定信息。以
MainAbilitySlice
和FieldViewBinding
为例,如下注解代码:生成一个
FieldViewBinding
实例,其值为: -
ViewBinding
//表示某个控件的绑定信息,其中包括field和method(对于各种事件绑定)。如下代码所示:
-
-
ButterKnifeProcessor#process()
方法相当注解执行的main方法(会进入多次),主要干了两件事findAndParseTargets()
和brewJava()
,如下图所示:用到的工具:SequenceDiagram - IntelliJ IDEA插件,直接在插件市场搜索、安装就行,用来生成方法调用时序图
-
其中
ButterKnifeProcessor#findAndParseTargets()
的主要功能是找到并解析各个targets,如下图所示:ps:这个图省略了很多parseXXX()方法,只保留了一个
parseBindComponent()
,因为功能类似。不然图太长了。由上图可知
findAndParseTargets()
方法实现了以下三件事:-
解析注入对象相关的注解
parseXXX()
处理
@BindComponent
,@BindString
之类的组件或资源 -
解析事件绑定相关的注解
findAndParseListener()
处理
@OnClick
,@OnItemClick
,@OnTextChanged
之类的Listener通过以上两步,完成了注解信息的扫描收集,并将解析的信息保存到
builderMap
和erasedTargetNames
两个集合中; -
findAllSupertypeBindings()
,findParentType()
,及findAndParseTargets()
第三步,对上面提到的
builderMap
和erasedTargetNames
两个集合中的信息进行重新整理,最终返回一个以TypeElement为key,BindingSet为vaule的bindingMap集合。下面直接帖学习笔记ButterKnife的分析吧:
-
总结一下这个方法,就像它的方法名一 样 “找到并解析各个targets” 。这里有个疑问,targets是什么呢? targets就是待注入的字段、待绑定事件的方法, 比如下面代码中的
mTextView
就是target,即“待注入的字段”
-
动态分析
动态分析主要是验证一下上面静态分析的结论。对于比较复杂的代码,需要debug跟一下代码,查看运行时关键变量的值。
怎么调试butterknife_compiler
?
参考:https://www.w3ma.com/how-to-debug-an-annotation-processor-in-android-studio/
-
新建一个remote debug,比如命名为aptDebug
因为apt过程在编译期,所以需要remote debug。什么是remote debug,可以自己google一下。
-
在
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命令行,它将挂起虚拟机,直到连接了调试器。 -
设置断点,然后点击小虫子debug按钮。
-
以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()方法类似。总结一下: -
debug
Map<TypeElement, BindingSet> findAndParseTargets()
先说上面提到 targets是什么呢?,通过跟代码,可知targets是被注解的Element, 比如MainAblilitySlice中的
mDlViewRoot
方法的返回值是类和它的模板类的一一对应。
日志分析:
debug的效率其实很低。听说10倍程序员都爱打日志。所以,我们也要知道apt的messager的用法及注意事项有哪些?
虽然
System.out.println();
也可以打日志。但是messager会根据日志类型,把Kind.WARNING
和Kind.ERROR
类型的日志做统计,方便我们定位问题。
在ButterKnifeProcessor
中有封装messager的几个方法:
注意: 一定要执行gradlew --no-daemon :entry:clean :entry:compileDebugJavaWithJavac
,compileDebugJavaWithJavac
这个task。而且加上clean。不然看不到日志。
向apt程序传参:
在ButterKnifeProcessor
中覆写了getSupportedOptions()
,这样我们可以向apt传参了。
传参方法:在entry
中
在init()
方法中,我们可以取出传入的值:
移植butterknife_compiler
通过上面的代码分析,我们知道在安卓和鸿蒙上butterknife_compiler
的基本流程没有变,所以移植工作要做的就是因为平台class及api不同,做一些相应的调整。
对比代码后,差异不大。简单列举一下:
findViewById
的修改- Resource相关api的修改
- Font的修改(NORMAL变成REGULAR)
BindAnim
在鸿蒙版上暂不支持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等)。
再次膜拜大神JakeWharton。
喵叔出品,必属精品!
喵叔干货满满
喵叔的集成bufferknife 可以放在GitHub里面大家一起写噢
以前看JakeWharton写的bufferknife,下载源码有看过一部分感触甚深 如果只是单方面的做组件注解的话,简易版的bufferknife完全够用,如果要加入一些事件监听 就需要对反射技术有足够深的理解 我也只能看懂一些皮毛
有问题的话,大家可以一起交流交流~
现在已经有移植了:https://gitee.com/openharmony-tpc/butterknife
嗯嗯 参与+1