QQ 音樂 Android 團隊分享 Android DataBinding 數據綁定
引子
幾年前,數據綁定在便已在前端界風生水起,Angular.js、React.js、vue.js等熱門前端框架都具備這種能力。
數據綁定簡單來說,就是通過某種機制,把代碼中的數據和xml(UI)綁定起來,雙方都能對數據進行操作,并且在數據發生變化的時候,自動刷新數據。
數據綁定分單向綁定和雙向綁定兩種。
單向綁定上,數據的流向是單方面的,只能從代碼流向UI;雙向綁定的數據流向是雙向的,當業務代碼中的數據改變時,UI上的數據能夠得到刷新;當用戶通過UI交互編輯了數據時,數據的變化也能自動的更新到業務代碼中的數據上。
Android DataBinding Framework
在2015年的谷歌IO大會上,Android UI Toolkit團隊發布了DataBinding 框架,將數據綁定引入了Android開發,當時還只支持單向綁定,而且需要作為第三方依賴引入,時隔一年,雙向綁定這個特性也得到了支持,同時納入了Android Gradle Plugin(1.5.0+)中,只需要在gradle配置文件里添加短短的三行,就能用上數據綁定。
數據綁定框架
使用數據綁定的優點
-
能有效提高開發效率,減少大量需要手動編寫的膠水代碼(如 findViewById , setOnClickListener );
-
高性能(絕大部分的工作在編譯期完成,避免運行時使用反射);
-
使用靈活(可以使用表達式在布局里進行一定的邏輯運算);
-
具有IDE支持(語法高亮、自動補全,語法錯誤標記)。
舉個簡單的例子
需求:界面上有兩個控件,EditText用于獲取用戶輸入,TextView用于把用戶輸入展示出來。
傳統實現:用傳統的方式來實現,我們需要定義一個布局,設置好這兩個控件,然后在代碼中引用這個布局,把這兩個控件找出來,然后添加監聽器到EditText上,在輸入發生改變的時候,獲取輸入,然后更新到TextView上。
而使用數據綁定,我們的代碼會是這樣:
可以看到,使用了數據綁定,我們的代碼邏輯結構變得清晰,手動編寫的膠水代碼得到了簡化(由數據綁定框架替我們生成),數據綁定框架幫我們做了控件的數據變化監聽,并將數據同步更新到控件上。
數據綁定的使用
布局文件的改造
使用數據綁定的布局文件以 <layout> 標簽作為根節點,表明這是個數據綁定的布局,修改后數據綁定框架會生成對應的*Binding類,如 content_main.xml 會生成 ContentMainBinding 類,即默認規則是:單詞首字母大寫,移除下劃線,并在最后添加上Binding。
數據的聲明和輔助類導入
在 <layout> 標簽內部添加 <data> 標簽,即可聲明數據。給 <data> 標簽添加 class 屬性可以改變生成的*Binding類的名字,如使用 <data class="ContentMain"> 將其改為 ContentMain 。
數據標簽內部通過 <variable> 標簽聲明變量,通過 <import> 標簽導入輔助類,為了避免同名沖突,可以使用 alias 屬性指定一個別名。
數據綁定的使用
變量聲明之后,就可以在布局中使用了,使用的方式和使用Java類似,當表達式使用一個對象內的屬性時,會分別嘗試直接調用、getter、ObservableField.get(),具體的使用這里就不贅述了。
值得一提的是,數據綁定內支持表達式,可以使用表達式來進行一些基本的邏輯運算。
常用的操作有:
-
數學計算符: +、-、*、/、%
-
字符串拼接: +
-
邏輯運算符: &&、||
-
比較運算符: ==、>、<、>=、<=
-
函數調用
-
類型轉換
-
數據存取 [] ,對容器類的操作支持使用這種方式來存取
-
Null合并運算符: ?? ,合并運算符會在變量非空的時候使用左邊的操作,反之使用右邊的,如 data ?? data.defaultVal
事件綁定
嚴格意義上來說,事件綁定也屬于數據綁定的一種。之前我們常在布局內進行的 android:onClick="onBtnClick" 就可以視作是一種數據綁定。但通過使用數據綁定框架,允許我們做更多事情。
可以通過數據綁定,傳入一個變量,調用該變量上的方法用于事件的處理,跟原有的方式比,數據綁定允許我們將處理事件的邏輯和布局所關聯的類解耦,可以方便的替換不同的處理邏輯。
也可以通過表達式,在布局內直接執行一些代碼,不需要我們切換回Java代碼中去實現,對于一些不需要外部處理,僅僅是布局內相關的邏輯來說,這種特性允許我們把UI相關的邏輯進行內聚。
數據綁定框架的另一個特性,在進行數據相關的操作前,會檢查變量是否為空,倘若沒有傳入對應的變量,或者控件為空,在布局上進行的操作并不會執行,因此,假如上述例子中,我們沒有傳入對應的presenter對象,點擊按鈕并不會引發Crash。
還有,由于編譯期會進行檢查,假如對應的數據類型上沒有實現對應的方法,或方法簽名不對(參數類型應為View),那么編譯的時候就會報錯,代碼的穩定性也因此得到了保障。
數據模型
雖然數據綁定支持的POJO(Pure Old Java Object,普通Java類,指僅具有一部分getter/setter方法的類),但對POJO對象的數據更新并不會同步更新UI。為了實現自動更新,可以選擇:
-
繼承自 BaseObservable ,給 getter 加上 @Bindable 注解,并在 setter 中實現域的變動通知。
-
如果數據類無法繼承 BaseObservable ,變動通知可以用 PropertyChangeRegistry 來實現。
-
最后一種是使用 Observable域 ,對數據存取通過 ObservableField<T> 的 get 、 set 方法調用實現。 ObservableField<T> 是泛型類,對于基礎類型,有對應的 ObservableInt 、 ObservableLong 、 ObservableShort 等可供使用;另外對于容器,每次只會更新其中的一個項,而不是整個更新,因此還有對應的 ObservableArrayList 、 ObservableArrayMap 可供使用。
從使用上來說,第三種方式更加直觀和便捷,需要人工介入的地方更少,更不容易出錯,推薦使用。
關于數據綁定的使用,還有很多地方可以說,比如資源的引用、變量動態設置、Lambda表達式的支持等等,限于篇幅,這里就不再多說了,關于數據綁定的詳細介紹和使用,可以查看參考資料中的 Data Binding 指南 進一步學習。
數據綁定的原理
數據綁定的運行機制是怎樣的呢?我稍微修改了布局文件,加了幾個控件,使用了表達式,最終代碼在這: 傳送門
數據綁定相關類的初始化
首先我們需要找一個切入點,最顯而易見的切入點便是 ContentMainBinding.inflate ,這個類是數據綁定框架生成的,生成的文件位于 build/intermediates/classes/debug/<package_name>/databinding/ 目錄下。
方法的實現調用了另一個 inflate 方法,經過幾次輾轉,最終調用到了 ContentMainBinding.bind 方法。
這個方法首先檢查這個view是否是數據綁定相關的布局,不是則會拋出異常,是的話則實例化 ContentMainBinding 。
ContentMainBinding 是怎么實例化的呢?看下生成的代碼。
構造函數內首先調用 mapBindings 把 root 中所有的view找出來,數字8指的是布局中總共有8個view,然后還傳入 sIncludes 和 sViewsWithIds ,前者是布局中include進來的布局的索引,后者是布局中包含id的索引。
這兩個參數是靜態變量,看下它們是怎么初始化的:
由于Demo中的布局不包含include,因此 sIncludes 被值為null,而布局內有一個id為 R.id.fullName 的控件,因此他被加入到 sViewsWithIds 中,7表示它在 bindings 中的索引。
再回到構造函數, mapBindings 查找到的View都放置在 bindings 這個數組中,并通過生成代碼的方式,將它們一一取出來,轉化為對應的數據類型,有設置id的控件,會以id作為變量名,沒有設置id的控件,則以 mboundView + 數字 的方式依次賦值。然后將這個Binding和root關聯起來(通過將Binding設為rootView的tag的方式)。
還實例化了一個 OnClickListener ,用于綁定事件響應。
mapBindings 的方法實現在 ViewDataBinding 這個類里,主要是把root內所有的view給查找出來,并放置到 bindings 對應的索引內,這個索引如何確定呢?原來,數據綁定在處理布局的時候,生成了輔助信息在view的tag里,通過解析這個tag,就能知道對應的索引了。所以,為了避免自己inflate布局文件后,不小心操作了view的tag對解析產生干擾,盡量使用數據綁定來得到inflate之后的view。處理過的布局片段如下,生成位置為 app/build/intermediates/data-binding-layout-out/<build-type>/layout/ 目錄。
mapBindings 方法比較長,里面針對不同情況進行了處理,這里就不貼出源碼了,有興趣的讀者可以自行閱讀。另外,雖然這個方法看似使用到了遞歸,但實際上是通過這種方式實現對root下所有的控件的遍歷,因此整個方法的時間復雜度是O(n),通過一次遍歷,找到所有的控件,整體性能比使用 findViewById 還優秀。
實例化的 OnClickListener 接受兩個參數,一個是 OnClickListener.Listener , ContentMainBinding 實現了這個接口,所以第一個參數傳的值是 ContentMainBinding ,另一個是標識這個listener作用的控件的 sourceId 。這個 OnClickListener 干的事情很簡單,就是把點擊事件,附加上 sourceId ,回傳給了 ContentMainBinding 的 _internalCallbackOnClick 處理,也就是最后我們所有跟布局相關的操作邏輯最終還是內聚到了 ContentMainBinding 這個類中來。
從實現可以看到,這里僅僅實現了我們在布局中寫下的內部處理邏輯 ()-> fullName.setText(firstName + · + lastName) ,由于布局中這樣的處理邏輯僅有一處,所以這里sourceId沒有使用到。如果有多于2處的邏輯,這里會生成一個 switch 塊,通過sourceId執行不同的指令。從實現還可以看到,框架生成的代碼使用本地變量來持有成員變量,以保證對變量的訪問是線程安全的。同樣的,在對訪問控件之前,會進行是否為空的檢查,避免空指針錯誤。這也是使用數據綁定的帶來的好處:通過框架自動生成的代碼中的為空檢查,避免手工編碼容易導致的空指針錯誤。
但是,細心的朋友肯定發現了,構造函數里僅僅是創建了監聽器,但并沒有將它 set 到對應的控件中去,那么這一步是在哪里進行的呢?
數據綁定的Rebind機制
在構造函數的最后,調用了方法 invalidateAll 。
invalidateAll 方法的實現很簡單,將臟標記位 mDirtyFlags 標記為 0x10L ,即在二進制表示上,第5位的值為1,這個臟標記位是一個long的值,也就是最多有64個位可供使用。由于 mDirtyFlags 這個變量是成員變量,且多處會對其進行寫操作,所以對它的寫操作都是同步進行的。更新完了這個值,緊接著就調用了 requestRebind 方法,請求執行rebind操作。
這個方法的實現在 ContentMainBinding 的基類 ViewDataBinding 中。
如果此前沒請求執行rebind操作,那么會將 mPendingRebind 置為 true ,API等級16及以上,會往 mChoreographer 發一個 mFrameCallback ,在系統刷新界面( doFrame )的時候執行rebind操作,API等級16以下,則是往UI線程post一個 mRebindRunnable 任務。 mFrameCallback 的內部實際上調用的是 mRebindRunnable 的 run 方法,因此這兩個任務除了調用時機,干的事情其實沒什么不同。
而如果此前請求過執行rebind操作,即已經post了一個任務到隊列去,而且這個任務還未獲得執行,此時 mPendingRebind 的值為 true ,那么 requestRebind 將直接返回,避免重復、頻繁執行rebind操作帶來的性能損耗。
任務執行的時候干了什么:
當任務獲得執行時,立即將 mPendingRebind 設為 false ,以便后續其他 requestRebind 能往主線程發起rebind的任務。再API 19及以上的版本,檢查下UI控件是否附加到了窗口上,如果沒有附到窗口上,則設置監聽器,以便在UI附加到窗口上的時候立即執行rebind操作,然后返回。當符合執行條件(API 19以下或UI控件已經附加到窗口上)的時候,則調用 executePendingBindings 執行binding邏輯。
然而這里實際上還沒執行具體的binding操作,這里在執行前進行一些判定:
-
如果已經開始執行綁定操作了,即這段代碼正在執行,那么調用一次 requestRebind ,然后返回。
-
如果當前沒有需要進行刷新UI的需要,即臟標記為0,那么直接返回。
-
接下來在執行具體的 executeBindings 操作前,調用下mRebindCallbacks.notifyCallbacks,通知所有回調說即將開始rebind操作,回調可以在執行的過程中,將mRebindHalted置為true,阻止executeBindings的運行,攔截成功同樣通過回調進行通知。
-
如果沒有被攔截,executeBindings方法便得以運行,運行結束后,同樣通過回調進行通知。
executeBindings是個抽象方法,具體的實現在子類中,這樣我們又一次回到了我們的ContentMainBinding類中來。意即跟content_main.xml相關的邏輯依舊內聚到了ContentMainBinding 中。
executeBindings 的實現也是數據綁定框架在編譯期生成的,代碼如下:
實現中,首先把臟標記位存到本地變量中,然后將臟標記位置為0,開始批量處理之前的改動。如何知道需要進行哪些處理呢?根據臟標記位和相關的值進行位與運算來判斷。在構造函數的最后,臟標記位被設為0x10L,即第5位為1,在這種情況下,上述代碼中的每一個分支都為真,都會被執行,即進行了一次全量的綁定操作。
這里做了:
-
創建并設置回調,如
android:onClick="@{presenter::saveUserName} 這種表達式,會在 presenter 不為空的情況下,創建對應的回調,并設置到 mboundView4 上;
-
將數據模型上的值更新到UI上,如將 firstName 設置到 mboundView1 上, lastName 設置到 mboundView2 上。可以看到,每一個 <variable> 標簽聲明的變量都有一個專屬的標記位,當改變量的值被更新時,對應的臟標記位就會置為1, executeBindings 的時候變回將這些變動更新到對應的控件。
-
在設置了雙向綁定的控件上,為其添加對應的監聽器,監聽其變動,如: EditText 上設置 TextWatcher 。具體的設置邏輯放置到了 TextViewBindingAdapter.setTextWatcher 里。源碼如下,也就是創建了一個新的 TextWatcher ,將我們傳進來的監聽器包裹在其中。在這里看到了 @BindingAdapter 注解,這個注解實現了控件屬性和代碼內的方法調用的映射,編譯期,數據綁定框架通過這種方式,為對應的控件生成對應的方法調用。如果需要讓自定義控件支持數據綁定,可以參考實現。
為了監聽代碼改動我們傳入的監聽器是什么呢?
是一個InverseBindingListener,對應 TextViewBindingAdapter.setTextWatcher 的第四個參數,當數據發生變化的時候, TextWatch 在回調 onTextChanged 的最后,會通過 InverseBindingListener 發送通知, InverseBindingListener 的實現中,會去對應的View中取得控件中最新的值,并檢查 *Binding 類是否為空,非空的話則調用對應的方法更新數據。這樣的實現方式,在保證了允許業務自定義監聽器的同時,也保證了數據變動監聽的功能實現。
上面是更新數據的代碼,如之前所屬,更新數據之后,將臟標記位對應的位設置為1,這里是0x8L,即第四位,然后發起一次rebind請求。
回看上面的 executeBindings 實現,可以看到,在下面這個分支里,完成了UI的數據更新:
具體的更新UI的實現放到了 TextViewBindingAdapter.setText 里:
實現中會比對新舊數據是否一致,不一致的情況下才進行更新,這樣也避免了: 設置數據 -> 觸發數據變動回調 -> 更新數據 -> 再次觸發數據變動回調 -> ... 引起的死循環問題。
方法數的問題
data binding框架的jar包有兩個,一個是adapter,一個是baseLibrary,前者方法數為415,后者方法數為502,整體增加的方法數不到一千個。生成的類方法數方面demo中大約是每個布局20個方法,具體跟布局內的變量數量(每個變量對應一個get、set方法)、雙向綁定的數量(每個會多一個 InverseBindingListener 匿名類)有關,會根據這幾個因素有所浮動。
小結
通過上面的一波源碼分析,將數據綁定在應用內的運行機制大致分析了一遍,總結下:
-
通過對root view進行一次遍歷,將view中所有的控件查找出來并進行綁定,查找效率比使用 findViewById 更加高效。
-
查找過程依賴于view的tag標記,盡量避免使用tag標記,以免跟干涉到框架的正常運行
-
對UI的操作都在主線程;對數據的操作可以在任意線程;
-
對數據的操作并不會即時的反應到UI上,通過臟標記,往主線程發起rebind任務,在主線程下次回調的時候批量刷新,避免頻繁操作UI;
-
使用數據綁定操作UI更加安全,操作集中在主線線程,并在操作前進行為空檢查,避免空指針。
-
絕大部分的邏輯在生成的 *Binding 類中,即數據綁定框架在編譯期幫我們做了大量的工作,生成模板代碼,實現綁定邏輯,是否為空檢查,生成代理類,代碼的可靠性也是由編譯期的處理程序保證,有效的降低了人為出錯的可能性。
一些想法
-
使用數據綁定,實現了數據和表現的分離,結合響應式編程框架 RxJava 、 RxAndroid ,編碼體驗和效率能還能進一步提高。
-
由于數據綁定實現了數據和表現的分離,由Data Binding框架對接UI,可以通過自定義Adapter,干預某些屬性的屬性讀取和設置,比如攔截圖片資源的加載(換膚)、動態替換字符(翻譯)等功能。
-
方便UI復用,Android上進行UI組件化的時候,可以在布局的層次上進行復用,業務無關的UI邏輯也能一起打包,同時保持對外接口(數據模型)簡單,學習接入成本更小。
參考資料
-
Data Binding -Write App Faster(Google I/O 2015) https://www.油Tube.com/watch?v=NBbeQMOcnZ0&index=12&list=PLWz5rJ2EKKc_Tt7q77qwyKRgytF1RzRx8
-
Advanced Data Binding(Google I/O 2016) http://v.youku.com/v_show/id_XMTU4NTU4MTAxMg==.html?f=27314446
-
Android Data Binding Library 官方介紹 https://developer.android.com/topic/libraries/data-binding/index.html
-
Data Binding 源碼 https://android.googlesource.com/platform/frameworks/data-binding/
-
Data Binding(Google I/O 2015)的講稿) https://realm.io/news/data-binding-android-boyar-mount/
-
(譯)Data Binding 指南 http://yanghui.name/blog/2016/02/17/data-binding-guide/
-
MasteringAndroidDataBinding https://github.com/LyndonChin/MasteringAndroidDataBinding
-
Data Binding 高級篇 http://blog.zhaiyifan.cn/2016/07/06/android-new-project-from-0-p8/
來自:http://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232170&idx=1&sn=f4d7eb8f35ebf3b13696562ca3172bac&chksm=f1d9eac9c6ae63df357c3a96aa0218b5d66237c5411de5b34cd24ddb7a1d258b34444966d8c6&scene=0#rd