QQ 音乐 Android 团队分享 Android DataBinding 数据绑定

airiuytsb 8年前
   <h2><strong>引子</strong></h2>    <p>几年前,数据绑定在便已在前端界风生水起,Angular.js、React.js、vue.js等热门前端框架都具备这种能力。</p>    <p>数据绑定简单来说,就是通过某种机制,把代码中的数据和xml(UI)绑定起来,双方都能对数据进行操作,并且在数据发生变化的时候,自动刷新数据。</p>    <p>数据绑定分单向绑定和双向绑定两种。</p>    <p>单向绑定上,数据的流向是单方面的,只能从代码流向UI;双向绑定的数据流向是双向的,当业务代码中的数据改变时,UI上的数据能够得到刷新;当用户通过UI交互编辑了数据时,数据的变化也能自动的更新到业务代码中的数据上。</p>    <p><img src="https://simg.open-open.com/show/82c13c9334a6012ae934ebf3f17cc8b9.jpg"></p>    <h2><strong>Android DataBinding Framework</strong></h2>    <p>在2015年的谷歌IO大会上,Android UI Toolkit团队发布了DataBinding 框架,将数据绑定引入了Android开发,当时还只支持单向绑定,而且需要作为第三方依赖引入,时隔一年,双向绑定这个特性也得到了支持,同时纳入了Android Gradle Plugin(1.5.0+)中,只需要在gradle配置文件里添加短短的三行,就能用上数据绑定。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/cbe5310e8e69b1679b02d5c656926943.png"></p>    <p>数据绑定框架</p>    <h2><strong>使用数据绑定的优点</strong></h2>    <ol>     <li> <p>能有效提高开发效率,减少大量需要手动编写的胶水代码(如 findViewById , setOnClickListener );</p> </li>     <li> <p>高性能(绝大部分的工作在编译期完成,避免运行时使用反射);</p> </li>     <li> <p>使用灵活(可以使用表达式在布局里进行一定的逻辑运算);</p> </li>     <li> <p>具有IDE支持(语法高亮、自动补全,语法错误标记)。</p> </li>    </ol>    <h2><strong>举个简单的例子</strong></h2>    <p>需求:界面上有两个控件,EditText用于获取用户输入,TextView用于把用户输入展示出来。</p>    <p>传统实现:用传统的方式来实现,我们需要定义一个布局,设置好这两个控件,然后在代码中引用这个布局,把这两个控件找出来,然后添加监听器到EditText上,在输入发生改变的时候,获取输入,然后更新到TextView上。</p>    <p>而使用数据绑定,我们的代码会是这样:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/1f11c5b3e36d5c73eb0fbe6355e24427.jpg"></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/33f02675f63e03d1e561996255a25bef.png"></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/a5e4ecd363081d1586eb504b889c1219.gif"></p>    <p>可以看到,使用了数据绑定,我们的代码逻辑结构变得清晰,手动编写的胶水代码得到了简化(由数据绑定框架替我们生成),数据绑定框架帮我们做了控件的数据变化监听,并将数据同步更新到控件上。</p>    <h2><strong>数据绑定的使用</strong></h2>    <h3><strong>布局文件的改造</strong></h3>    <p>使用数据绑定的布局文件以 <layout> 标签作为根节点,表明这是个数据绑定的布局,修改后数据绑定框架会生成对应的*Binding类,如 content_main.xml 会生成 ContentMainBinding 类,即默认规则是:单词首字母大写,移除下划线,并在最后添加上Binding。</p>    <h3><strong>数据的声明和辅助类导入</strong></h3>    <p>在 <layout> 标签内部添加 <data> 标签,即可声明数据。给 <data> 标签添加 class 属性可以改变生成的*Binding类的名字,如使用 <data class="ContentMain"> 将其改为 ContentMain 。</p>    <p>数据标签内部通过 <variable> 标签声明变量,通过 <import> 标签导入辅助类,为了避免同名冲突,可以使用 alias 属性指定一个别名。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d77cec837d507d87f2401173aa877078.png"></p>    <h3><strong>数据绑定的使用</strong></h3>    <p>变量声明之后,就可以在布局中使用了,使用的方式和使用Java类似,当表达式使用一个对象内的属性时,会分别尝试直接调用、getter、ObservableField.get(),具体的使用这里就不赘述了。</p>    <p>值得一提的是,数据绑定内支持表达式,可以使用表达式来进行一些基本的逻辑运算。</p>    <p>常用的操作有:</p>    <ol>     <li> <p>数学计算符: +、-、*、/、%</p> </li>     <li> <p>字符串拼接: +</p> </li>     <li> <p>逻辑运算符: &&、||</p> </li>     <li> <p>比较运算符: ==、>、<、>=、<=</p> </li>     <li> <p>函数调用</p> </li>     <li> <p>类型转换</p> </li>     <li> <p>数据存取 [] ,对容器类的操作支持使用这种方式来存取</p> </li>     <li> <p>Null合并运算符: ?? ,合并运算符会在变量非空的时候使用左边的操作,反之使用右边的,如 data ?? data.defaultVal</p> </li>    </ol>    <h3><strong>事件绑定</strong></h3>    <p>严格意义上来说,事件绑定也属于数据绑定的一种。之前我们常在布局内进行的 android:onClick="onBtnClick" 就可以视作是一种数据绑定。但通过使用数据绑定框架,允许我们做更多事情。</p>    <p>可以通过数据绑定,传入一个变量,调用该变量上的方法用于事件的处理,跟原有的方式比,数据绑定允许我们将处理事件的逻辑和布局所关联的类解耦,可以方便的替换不同的处理逻辑。</p>    <p>也可以通过表达式,在布局内直接执行一些代码,不需要我们切换回Java代码中去实现,对于一些不需要外部处理,仅仅是布局内相关的逻辑来说,这种特性允许我们把UI相关的逻辑进行内聚。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/91909b932b0d0a2d1e5abd965545a649.jpg"></p>    <p>数据绑定框架的另一个特性,在进行数据相关的操作前,会检查变量是否为空,倘若没有传入对应的变量,或者控件为空,在布局上进行的操作并不会执行,因此,假如上述例子中,我们没有传入对应的presenter对象,点击按钮并不会引发Crash。</p>    <p>还有,由于编译期会进行检查,假如对应的数据类型上没有实现对应的方法,或方法签名不对(参数类型应为View),那么编译的时候就会报错,代码的稳定性也因此得到了保障。</p>    <h2><strong>数据模型</strong></h2>    <p>虽然数据绑定支持的POJO(Pure Old Java Object,普通Java类,指仅具有一部分getter/setter方法的类),但对POJO对象的数据更新并不会同步更新UI。为了实现自动更新,可以选择:</p>    <ol>     <li> <p>继承自 BaseObservable ,给 getter 加上 @Bindable 注解,并在 setter 中实现域的变动通知。</p> </li>     <li> <p>如果数据类无法继承 BaseObservable ,变动通知可以用 PropertyChangeRegistry 来实现。</p> </li>     <li> <p>最后一种是使用 Observable域 ,对数据存取通过 ObservableField<T> 的 get 、 set 方法调用实现。 ObservableField<T> 是泛型类,对于基础类型,有对应的 ObservableInt 、 ObservableLong 、 ObservableShort 等可供使用;另外对于容器,每次只会更新其中的一个项,而不是整个更新,因此还有对应的 ObservableArrayList 、 ObservableArrayMap 可供使用。</p> </li>    </ol>    <p>从使用上来说,第三种方式更加直观和便捷,需要人工介入的地方更少,更不容易出错,推荐使用。</p>    <p>关于数据绑定的使用,还有很多地方可以说,比如资源的引用、变量动态设置、Lambda表达式的支持等等,限于篇幅,这里就不再多说了,关于数据绑定的详细介绍和使用,可以查看参考资料中的 Data Binding 指南 进一步学习。</p>    <h2><strong>数据绑定的原理</strong></h2>    <p>数据绑定的运行机制是怎样的呢?我稍微修改了布局文件,加了几个控件,使用了表达式,最终代码在这: 传送门</p>    <h3><strong>数据绑定相关类的初始化</strong></h3>    <p>首先我们需要找一个切入点,最显而易见的切入点便是 ContentMainBinding.inflate ,这个类是数据绑定框架生成的,生成的文件位于 build/intermediates/classes/debug/<package_name>/databinding/ 目录下。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/9d48a3c6eed0aa6f5cca624a3780c0f9.png"></p>    <p>方法的实现调用了另一个 inflate 方法,经过几次辗转,最终调用到了 ContentMainBinding.bind 方法。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/a5e683b91051ba9a16b1f29223f0f73e.png"></p>    <p>这个方法首先检查这个view是否是数据绑定相关的布局,不是则会抛出异常,是的话则实例化 ContentMainBinding 。</p>    <p>ContentMainBinding 是怎么实例化的呢?看下生成的代码。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/5eae291edb14267856f7f39dbdc32d0d.jpg"></p>    <p>构造函数内首先调用 mapBindings 把 root 中所有的view找出来,数字8指的是布局中总共有8个view,然后还传入 sIncludes 和 sViewsWithIds ,前者是布局中include进来的布局的索引,后者是布局中包含id的索引。</p>    <p>这两个参数是静态变量,看下它们是怎么初始化的:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/00cf21596fe9946df2e33c4e298747fb.png"></p>    <p>由于Demo中的布局不包含include,因此 sIncludes 被值为null,而布局内有一个id为 R.id.fullName 的控件,因此他被加入到 sViewsWithIds 中,7表示它在 bindings 中的索引。</p>    <p>再回到构造函数, mapBindings 查找到的View都放置在 bindings 这个数组中,并通过生成代码的方式,将它们一一取出来,转化为对应的数据类型,有设置id的控件,会以id作为变量名,没有设置id的控件,则以 mboundView + 数字 的方式依次赋值。然后将这个Binding和root关联起来(通过将Binding设为rootView的tag的方式)。</p>    <p>还实例化了一个 OnClickListener ,用于绑定事件响应。</p>    <p>mapBindings 的方法实现在 ViewDataBinding 这个类里,主要是把root内所有的view给查找出来,并放置到 bindings 对应的索引内,这个索引如何确定呢?原来,数据绑定在处理布局的时候,生成了辅助信息在view的tag里,通过解析这个tag,就能知道对应的索引了。所以,为了避免自己inflate布局文件后,不小心操作了view的tag对解析产生干扰,尽量使用数据绑定来得到inflate之后的view。处理过的布局片段如下,生成位置为 app/build/intermediates/data-binding-layout-out/<build-type>/layout/ 目录。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e05c82e586af469853d999ee694ff889.png"></p>    <p>mapBindings 方法比较长,里面针对不同情况进行了处理,这里就不贴出源码了,有兴趣的读者可以自行阅读。另外,虽然这个方法看似使用到了递归,但实际上是通过这种方式实现对root下所有的控件的遍历,因此整个方法的时间复杂度是O(n),通过一次遍历,找到所有的控件,整体性能比使用 findViewById 还优秀。</p>    <p>实例化的 OnClickListener 接受两个参数,一个是 OnClickListener.Listener , ContentMainBinding 实现了这个接口,所以第一个参数传的值是 ContentMainBinding ,另一个是标识这个listener作用的控件的 sourceId 。这个 OnClickListener 干的事情很简单,就是把点击事件,附加上 sourceId ,回传给了 ContentMainBinding 的 _internalCallbackOnClick 处理,也就是最后我们所有跟布局相关的操作逻辑最终还是内聚到了 ContentMainBinding 这个类中来。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/b3b06633b916492095697eb67142903a.jpg"></p>    <p>从实现可以看到,这里仅仅实现了我们在布局中写下的内部处理逻辑 ()-> fullName.setText(firstName + · + lastName) ,由于布局中这样的处理逻辑仅有一处,所以这里sourceId没有使用到。如果有多于2处的逻辑,这里会生成一个 switch 块,通过sourceId执行不同的指令。从实现还可以看到,框架生成的代码使用本地变量来持有成员变量,以保证对变量的访问是线程安全的。同样的,在对访问控件之前,会进行是否为空的检查,避免空指针错误。这也是使用数据绑定的带来的好处:通过框架自动生成的代码中的为空检查,避免手工编码容易导致的空指针错误。</p>    <p>但是,细心的朋友肯定发现了,构造函数里仅仅是创建了监听器,但并没有将它 set 到对应的控件中去,那么这一步是在哪里进行的呢?</p>    <h2><strong>数据绑定的Rebind机制</strong></h2>    <p>在构造函数的最后,调用了方法 invalidateAll 。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e3aadee6564c9bf13dfefcd76e6368f2.png"></p>    <p>invalidateAll 方法的实现很简单,将脏标记位 mDirtyFlags 标记为 0x10L ,即在二进制表示上,第5位的值为1,这个脏标记位是一个long的值,也就是最多有64个位可供使用。由于 mDirtyFlags 这个变量是成员变量,且多处会对其进行写操作,所以对它的写操作都是同步进行的。更新完了这个值,紧接着就调用了 requestRebind 方法,请求执行rebind操作。</p>    <p>这个方法的实现在 ContentMainBinding 的基类 ViewDataBinding 中。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/c1b71004d8ffa6be3397be76eb05bbb1.png"></p>    <p>如果此前没请求执行rebind操作,那么会将 mPendingRebind 置为 true ,API等级16及以上,会往 mChoreographer 发一个 mFrameCallback ,在系统刷新界面( doFrame )的时候执行rebind操作,API等级16以下,则是往UI线程post一个 mRebindRunnable 任务。 mFrameCallback 的内部实际上调用的是 mRebindRunnable 的 run 方法,因此这两个任务除了调用时机,干的事情其实没什么不同。</p>    <p>而如果此前请求过执行rebind操作,即已经post了一个任务到队列去,而且这个任务还未获得执行,此时 mPendingRebind 的值为 true ,那么 requestRebind 将直接返回,避免重复、频繁执行rebind操作带来的性能损耗。</p>    <p>任务执行的时候干了什么:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/21fdde7f3a2d6e35bc8806f9b55ce1c1.jpg"></p>    <p>当任务获得执行时,立即将 mPendingRebind 设为 false ,以便后续其他 requestRebind 能往主线程发起rebind的任务。再API 19及以上的版本,检查下UI控件是否附加到了窗口上,如果没有附到窗口上,则设置监听器,以便在UI附加到窗口上的时候立即执行rebind操作,然后返回。当符合执行条件(API 19以下或UI控件已经附加到窗口上)的时候,则调用 executePendingBindings 执行binding逻辑。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/51a0b6e975f27899054c5761f76c0110.jpg"></p>    <p>然而这里实际上还没执行具体的binding操作,这里在执行前进行一些判定:</p>    <ol>     <li> <p>如果已经开始执行绑定操作了,即这段代码正在执行,那么调用一次 requestRebind ,然后返回。</p> </li>     <li> <p>如果当前没有需要进行刷新UI的需要,即脏标记为0,那么直接返回。</p> </li>     <li> <p>接下来在执行具体的 executeBindings 操作前,调用下mRebindCallbacks.notifyCallbacks,通知所有回调说即将开始rebind操作,回调可以在执行的过程中,将mRebindHalted置为true,阻止executeBindings的运行,拦截成功同样通过回调进行通知。</p> </li>     <li> <p>如果没有被拦截,executeBindings方法便得以运行,运行结束后,同样通过回调进行通知。</p> </li>    </ol>    <p>executeBindings是个抽象方法,具体的实现在子类中,这样我们又一次回到了我们的ContentMainBinding类中来。意即跟content_main.xml相关的逻辑依旧内聚到了ContentMainBinding 中。</p>    <p>executeBindings 的实现也是数据绑定框架在编译期生成的,代码如下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/7996adee599eae502ff660c72ef9c90e.jpg"> <img src="https://simg.open-open.com/show/5ae4589e556408462d4fae1f1836db3c.jpg"></p>    <p>实现中,首先把脏标记位存到本地变量中,然后将脏标记位置为0,开始批量处理之前的改动。如何知道需要进行哪些处理呢?根据脏标记位和相关的值进行位与运算来判断。在构造函数的最后,脏标记位被设为0x10L,即第5位为1,在这种情况下,上述代码中的每一个分支都为真,都会被执行,即进行了一次全量的绑定操作。</p>    <p>这里做了:</p>    <ol>     <li> <p>创建并设置回调,如</p> <p>android:onClick="@{presenter::saveUserName} 这种表达式,会在 presenter 不为空的情况下,创建对应的回调,并设置到 mboundView4 上;</p> </li>     <li> <p>将数据模型上的值更新到UI上,如将 firstName 设置到 mboundView1 上, lastName 设置到 mboundView2 上。可以看到,每一个 <variable> 标签声明的变量都有一个专属的标记位,当改变量的值被更新时,对应的脏标记位就会置为1, executeBindings 的时候变回将这些变动更新到对应的控件。</p> </li>     <li style="text-align: center;"> <p>在设置了双向绑定的控件上,为其添加对应的监听器,监听其变动,如: EditText 上设置 TextWatcher 。具体的设置逻辑放置到了 TextViewBindingAdapter.setTextWatcher 里。源码如下,也就是创建了一个新的 TextWatcher ,将我们传进来的监听器包裹在其中。在这里看到了 @BindingAdapter 注解,这个注解实现了控件属性和代码内的方法调用的映射,编译期,数据绑定框架通过这种方式,为对应的控件生成对应的方法调用。如果需要让自定义控件支持数据绑定,可以参考实现。</p> <img src="https://simg.open-open.com/show/387aa25a24ed6c44376c9a1e7381be85.jpg"> <img src="https://simg.open-open.com/show/2e4c2e55e5c238301d7f0adfd3da24c4.jpg"></li>    </ol>    <p>为了监听代码改动我们传入的监听器是什么呢?</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/014edc544cace7f56a3590f78c615636.jpg"></p>    <p>是一个InverseBindingListener,对应 TextViewBindingAdapter.setTextWatcher 的第四个参数,当数据发生变化的时候, TextWatch 在回调 onTextChanged 的最后,会通过 InverseBindingListener 发送通知, InverseBindingListener 的实现中,会去对应的View中取得控件中最新的值,并检查 *Binding 类是否为空,非空的话则调用对应的方法更新数据。这样的实现方式,在保证了允许业务自定义监听器的同时,也保证了数据变动监听的功能实现。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/4ef76ba226863d431612fcc8f554d3b5.png"></p>    <p>上面是更新数据的代码,如之前所属,更新数据之后,将脏标记位对应的位设置为1,这里是0x8L,即第四位,然后发起一次rebind请求。</p>    <p>回看上面的 executeBindings 实现,可以看到,在下面这个分支里,完成了UI的数据更新:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/7a095b6453a96f24ede55ed16515901c.png"></p>    <p>具体的更新UI的实现放到了 TextViewBindingAdapter.setText 里:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/64617a932a4351930cf98e9d5f436d49.jpg"></p>    <p>实现中会比对新旧数据是否一致,不一致的情况下才进行更新,这样也避免了: 设置数据 -> 触发数据变动回调 -> 更新数据 -> 再次触发数据变动回调 -> ... 引起的死循环问题。</p>    <h2><strong>方法数的问题</strong></h2>    <p>data binding框架的jar包有两个,一个是adapter,一个是baseLibrary,前者方法数为415,后者方法数为502,整体增加的方法数不到一千个。生成的类方法数方面demo中大约是每个布局20个方法,具体跟布局内的变量数量(每个变量对应一个get、set方法)、双向绑定的数量(每个会多一个 InverseBindingListener 匿名类)有关,会根据这几个因素有所浮动。</p>    <h2><strong>小结</strong></h2>    <p>通过上面的一波源码分析,将数据绑定在应用内的运行机制大致分析了一遍,总结下:</p>    <ol>     <li> <p>通过对root view进行一次遍历,将view中所有的控件查找出来并进行绑定,查找效率比使用 findViewById 更加高效。</p> </li>     <li> <p>查找过程依赖于view的tag标记,尽量避免使用tag标记,以免跟干涉到框架的正常运行</p> </li>     <li> <p>对UI的操作都在主线程;对数据的操作可以在任意线程;</p> </li>     <li> <p>对数据的操作并不会即时的反应到UI上,通过脏标记,往主线程发起rebind任务,在主线程下次回调的时候批量刷新,避免频繁操作UI;</p> </li>     <li> <p>使用数据绑定操作UI更加安全,操作集中在主线线程,并在操作前进行为空检查,避免空指针。</p> </li>     <li> <p>绝大部分的逻辑在生成的 *Binding 类中,即数据绑定框架在编译期帮我们做了大量的工作,生成模板代码,实现绑定逻辑,是否为空检查,生成代理类,代码的可靠性也是由编译期的处理程序保证,有效的降低了人为出错的可能性。</p> </li>    </ol>    <h2><strong>一些想法</strong></h2>    <ol>     <li> <p>使用数据绑定,实现了数据和表现的分离,结合响应式编程框架 RxJava 、 RxAndroid ,编码体验和效率能还能进一步提高。</p> </li>     <li> <p>由于数据绑定实现了数据和表现的分离,由Data Binding框架对接UI,可以通过自定义Adapter,干预某些属性的属性读取和设置,比如拦截图片资源的加载(换肤)、动态替换字符(翻译)等功能。</p> </li>     <li> <p>方便UI复用,Android上进行UI组件化的时候,可以在布局的层次上进行复用,业务无关的UI逻辑也能一起打包,同时保持对外接口(数据模型)简单,学习接入成本更小。</p> </li>    </ol>    <h2>参考资料</h2>    <ol>     <li> <p>Data Binding -Write App Faster(Google I/O 2015) https://www.油Tube.com/watch?v=NBbeQMOcnZ0&index=12&list=PLWz5rJ2EKKc_Tt7q77qwyKRgytF1RzRx8</p> </li>     <li> <p>Advanced Data Binding(Google I/O 2016) http://v.youku.com/v_show/id_XMTU4NTU4MTAxMg==.html?f=27314446</p> </li>     <li> <p>Android Data Binding Library 官方介绍 https://developer.android.com/topic/libraries/data-binding/index.html</p> </li>     <li> <p>Data Binding 源码 https://android.googlesource.com/platform/frameworks/data-binding/</p> </li>     <li> <p>Data Binding(Google I/O 2015)的讲稿) https://realm.io/news/data-binding-android-boyar-mount/</p> </li>     <li> <p>(译)Data Binding 指南 http://yanghui.name/blog/2016/02/17/data-binding-guide/</p> </li>     <li> <p>MasteringAndroidDataBinding https://github.com/LyndonChin/MasteringAndroidDataBinding</p> </li>     <li> <p>Data Binding 高级篇 http://blog.zhaiyifan.cn/2016/07/06/android-new-project-from-0-p8/</p> </li>    </ol>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232170&idx=1&sn=f4d7eb8f35ebf3b13696562ca3172bac&chksm=f1d9eac9c6ae63df357c3a96aa0218b5d66237c5411de5b34cd24ddb7a1d258b34444966d8c6&scene=0#rd</p>    <p> </p>