【實戰】Android Data Binding從牴觸到愛不釋手

2021-03-02 架構師必備
1 引入

如何高效地實現以下界面?


登錄/未登錄

有好幾年findViewById實戰經驗的我,感覺並不難啊。一般會

1.先定義一個User的Model類,數據來自JSON解析;

2.創建一個xml,隨後在xml中布局完所有View,對頭像、標題、積分、登錄按鈕一個id;

3.在Activity中通過findViewById獲取到頭像ImageView、標題TextView、積分TextView、登錄Button,然後給Button設置監聽器,再根據登陸狀態展示對應數據;

實現如下:

User.java

activity_detail.xml


DetailActivity


2 去掉煩人的findViewById(View注入)

可以看到,在Activity中View的定義、find、判空佔據了大量篇幅,我們需要更優雅的實現。

2.1 ButterKnife

你可能聽說過Jake Wharton的ButterKnife,這個庫只需要在定義View變量的時候通過註解傳入對應id,隨後在onCreate時調用ButterKnife.bind(this)即可完成view的注入,示例如下:

2.2 Android Data Binding

如果使用了Android Data Binding,那麼View的定義、find、判空這些都不用寫了,如何做呢?

2.2.1 準備工作

首先,你需要滿足一個條件:你的Android Plugin for Gradle版本必須等於或高於1.5.0-alpha1版本,這個版本位於根目錄build.gradle中,示例如下:

接著,你必須告訴編譯器開啟Data Binding,一般位於app:build.gradle的android標籤中,示例如下:

2.2.2 修改layout.xml

以activity_detail.xml為例,原來的根節點為LinearLayout,如下所示:

我們拷一份activity_detail.xml,改為activity_detail2.xml,並且需要在外面wrap一層layout標籤,修改後的activity_detail2.xml為:

2.2.3 開始享受樂趣吧!

在上述操作完成後,編譯器會自動為我們生成
com.asha.demo.databinding.ActivityDetail2Binding.java類,這個類的命令方式為:包名 + databinding + activity_detail2駝峰命名方式 + Binding.java。隨後,使用這個activity_detail2的DetailActivity2.java的代碼可以簡化為:

是的,所有View的定義、find、判空都不見了,所有的這些操作都在編譯器為我們生成的ActivityDetail2Binding.java中完成,只需要在onCreate時調用如下代碼進行setContentView即可實現,


我的天哪

2.2.4 ActivityDetail2Binding中注入View相關的代碼分析

可以在as中方便的查看編譯器自動生成的類,這個類位於/app/build/intermediates/classes/debug/com/asha/demo/databinding/ActivityDetail2Binding.class中,縮減掉Binding邏輯後的代碼為:

其中全局靜態SparseIntArray數組中存放了4個數字,這個四個數字為R.java中生成的對應View的id,

在ActvityDetail2Binding實例構造的時候調用了mapBindings,一次解決了所有View的查找,mapBindings函數在ActvityDetail2Binding父類ViewDataBinding中實現。

3 使用表達式在layout.xml中填充model數據

在ActivityDetail2.java中還存在大量的View控制、數據填充代碼,如何把這些代碼在交給layout.xml完成呢?

3.1 ModelAdapter類

第2節中已經定義了User.java類作為Model類,但是我們經常會遇到Model類和真正View展示不一致的情況,本例子中定義一個來ModelAdapter類來完整Model數據到展示數據的適配。示例代碼為ActivityDetail3.java的內部類,可以調用ActivityDetail3.java中的函數,代碼定義如下:

3.2 activity_detail3.xml中使用model

同樣複製一份activity_detail2.xml為activity_detail3.xml,在<layout>節點加入<data>節點,並且在裡面定義需要用的model類(比如ModelAdapter adapter),當然也可以是基礎類型變量(比如int visibility);

隨後,就可以在下面的view中使用表達式了,全部布局文件如下:

3.3 DetailActivity3.java中調用填充

如下代碼所示,只需要在登錄狀態改變的時候,給viewDataBinding設置所需要的adatper、visibility值,即可完成數據的填充

3.4 ActivityDetail3Binding中填充相關的代碼分析

同樣,ActivityDetail3Binding中,編譯器根據activity_detail3.xml中的<data>標籤,自動生成了諸如setAdapter、setVisibility的代碼,setAdapter相關代碼如下:

非常簡單,自動生成了getter和setter,在完成set操作後,調用執行notifyPropertyChanged和super.requestRebind()

notifyPropertyChanged
ViewDataBinding本身就是一個BaseObservable, 在往ViewDataBinding註冊觀察某個屬性的變化,如果註冊了mAdapter的變化,對應的觀察器就會接收到回調。相關邏輯與反向Binding相關,谷歌官方還沒給出相關使用文檔,不再深入分析;

super.requestRebind()
1.此函數為ViewDataBinding中的函數,具體實現為判斷現在是否有Rebind請求,如果有則return;如果沒有則根據運行時sdk版本交給handler或者choreographer插入到下一幀中執行mRebindRunnable。
2.在mRebindRunnable中會根據當前sdk版本,如果大於等於KITKAT,則需要在onAttachToWindow後執行executePendingBindings;否則直接執行executePendingBindings。

3.在父類ViewDataBinding中經過一些的判斷,調用到ActivityDetail3Binding中的executeBindings,在executeBindings中根據dirtyFlags執行不同的View屬性賦值,以下所有ActivityDetail3Binding相關代碼都是編譯器自動生成的至此,完成了View數據的填充分析。

4 Binding

自動生成的ViewDataBinding類(例如ActivityDetail3Binding)內包含了Model + View,是MVVM中的MV的概念。

第2章的View注入,第3章的View賦值都是鋪墊,他們最後都是為Binding操作進行服務。目前谷歌已經支持雙向Binding,但上文已經提到,目前資料比較少。本文只關注單向的Binding,即:Model的變化,自動同步到View上。

4.1 使用ObservableField

目前所提供的ObservableField有:

Observable類型

對應原類型



ObservableArrayList

ArrayList

ObservableArrayMap

ArrayMap

ObservableBoolean

boolean

ObservableByte

byte

ObservableChar

char

ObservableFloat

float

ObservableDouble

double

ObservableLong

long

ObservableInt

int

ObservableParcelable<T extends Parcelable>

<T extends Parcelable>

ObservableField<T>

<T>

本文使用簡單的ObservableInt作為示例,解決visibility的單項綁定問題。

改造activity_detail4.xml:定義類型為ObservableInt的variable,name為visibility,隨後賦值給ImageView的android:visibility,示例如下:

改造DetailActivity4.java,只需要在onCreate時把visibility賦值給binding(ActivityDetail4Binding)即可,後面對visibility的操作,就會更新到view上,示例代碼如下:

4.2 ActivityDetail4Binding中單向綁定相關的代碼分析

與給ActivityDetail4Binding直接set純Model不同,所有的ObservableField都實現了Observable接口,只要實現了Observable接口,都是單向Binding類型,所以ActivityDetail4Binding中的setVisibility多加了一行代碼:this.updateRegistration(1, visibility),其中1為propertyId,目前一共自動生成了2個,0為adatper,1為visibility,代碼如下:

updateRegistration函數為ViewDataBinding中的函數,會根據 Observable、ObservableList、ObservableMap三種類型,分別創建對應的Listener。ObservableInt為Observable,所以會使用CREATE_PROPERTY_LISTENER,在registerTo函數中創建WeakPropertyListener,
代碼如下:

在WeakPropertyListener的mListener有個setTarget函數,這個函數會向mObservable(即外面傳進來的visibility)註冊一個監聽器,如果visibility值發生變化,這個listener就會得到通知,回調到WeakPropertyListener的onPropertyChanged,接著通知到binding(ActivityDetail4Binding)的handleFieldChange,在handleFieldChange中調用了ActivityDetail4Binding的onFieldChange函數,如果返回值為true,則在handleFieldChange中調用requestRebind(),通知View進行賦值更新界面,onFieldChange相關代碼如下:

4.3 Observable Objects

與4.1 ObservableField類似,可以改造一下ModelAdapter:為getter方法增加@Bindable註解,為setter方法增加notifyPropertyChanged(com.asha.demo.BR.name)通知。其中,BR是根據@Bindalbe自動生成的類,給getter方法增加@Bindable註解後,BR文件自動會生成一個整型的name。改造後代碼如下:

隨後,在DetailActivity4.java中調用測試代碼,執行完會在1秒後改變adapter上的name值,並且同步到View上,測試代碼如下:

具體原理與4.1類似,不再贅述。

5 layout.xml中View屬性的setter

在下述示例中,detail_name這個TextView想把adapter.name賦值給自身的text屬性,就需要調用textView.setText(String)方法,這個方法就是View屬性的setter方法。

5.1 @BindingAdapter

上述的setter方法,Data Binding庫幫我們實現了大部分默認方法,具體方法參見android.databinding.adapters包下的類,下圖為ViewBindingAdatper具體實現,


ViewBindingAdatper

其中setter方法都為static方法,第一個參數都為自身的實例,後面為xml中傳入的參數,只要加入@BindingAdapter註解,編譯器就會全局搜索保存在一個temp文件中,並在生成類似ActivityDetail4Binding過程中去查找所需的setter方法的。如果需要自定義,只需要在任意app代碼中定義@BindingAdapter即可,例如:

5.2 DataBindingComponent

很多情況下只是某個Binding文件(例如ActivityDetail4Binding)需要自定義setter方法,這個時候就需要使用DataBindingComponent,

完成後,這個ActivityDetail4Binding範圍內的所有android:alpha="@{foo}"的方式賦值alpha的setter函數都會使用MyComponent#setAlpha。

5.3 @BindingConversion

有時候會遇到類型不匹配的問題,比如R.color.white是int,但是通過Data Binding賦值給android:background屬性後,需要把int轉換為ColorDrawable,實現方式如下:

對應在ActivityDetail4Binding.java中生成的代碼如下所示,其中AvatarAdapterObjectn1為int類型:

5.4 @BindingMethod

例如layout.xml中android:onClick屬性,在Binding中真正使用setter時,就對應到了setOnClickListener方法,

6 Data Binding利用編譯器在背後做的那些事兒

Data Binding相關的jar包由四部分組成,

1.baseLibrary-2.1.0-rc1.jar
作為運行時類庫被打進APK中;

2.DataBinderPlugin(gradle plugin)
在編譯期使用,利用gradle-api(之前叫transform-api,1.5生,2.0改名)處理xml文件,生成DataBindingInfo.java;

3.compiler-2.1.0-rc1.jar
在編譯器使用,入口類繼承自AbstractProcessor,用於處理註解,並生成Binding類,DataBindingCompoent.java,DataBinderMapper.java類;

4.compilerCommon-2.1.0-rc1.jar
被DataBinderPlugin和compiler-2.1.0-rc1.jar所依賴

為了提高運行時的效率,Data Binding在背後做了非常多的工作,下圖是我整理的編譯流程,如圖所示:


Data Binding編譯流程

6.1 相關對象介紹

白色部分為輸入,包括
1.res/layout;
2.原始碼中的註解;

黃色部分為編譯器處理類,包括
1.aapt編譯時處理,入口類名為MakeCopy.java;
2.gradle-api處理,入口類名為DataBinderPlugin.java;
3.AbstractProcessor處理,入口類名為ProcessDataBinding.java;

藍色部分為中間產物,包括
1.data-binding-info文件夾,包含了layout的基本信息,導入的變量,View標籤中的表達式,標籤的位置索引等等,如下所示為data-binding-info/activity_detail3-layout.xml:


2.setter_store.bin,包含所有setter相關信息;
3.layoutinfo.bin,包含所有layout相關信息;
4.br.bin,包含所有BR相關信息;
以上bin文件都以Serializable方式序列化到磁碟上,需要的時候進行反序列化操作;

綠色部分為最終產物,包括
1.data-binding-layout-out(最終輸出到res/layout),即去掉根節點<layout>,去掉節點<data>,與不使用Data Binding時的layout相一致,例如data-binding-layout-out/activity_detail2.xml:

2.DataBindingInfo.class,一個看似空的類,但在SOURCE階段包含了一個@BindingBuildInfo註解,包含了基本DataBinding的基本信息,代碼如下:

3.DataBindingComponent.class,會根據自定義的DataBindingComponent自動生成對應實例化方法,例如:

4.ViewDataBinding.class的子類(ActivityDetail2Binding.class等)
5.BR.class,Bindable屬性索引表,例如:

6.DataBindingMapper.class,Mapper,用於尋找某個layout.xml對應的ViewDataBinding類,例如:

6.2 相關編譯流程

STEP1 資源處理
aapt或者gradle執行時,都會觸發資源處理,在資源處理過程中,DataBinding都會掃描一遍現有的資源,生成不包含<layout>的data-binding-layout-out以及DataBinding所需要的data-binding-info;

STEP2 DataBindingInfo.class生成
在完成資源處理後,aapt或者gradle-api都會去執行DataBindingInfo.class生成操作,把相關的信息寫入DataBindingInfo.class的@BindingBuildInfo註解中;

STEP3 監聽到註解變化
生成@BindingBuildInfo註解,或者code中發現有新的註解寫入,AbstractProcessor註解處理器就開始執行註解處理。DataBinding中有一個ProcessDataBinding.java類專門來處理DataBinding相關的註解;

STEP4 ProcessDataBinding處理註解,生成bin
ProcessDataBinding中處理註解永遠會按順執行3步,ProcessMethodAdapter,ProcessExpressions,ProcessBindable。每次執行都會從磁碟反序列化對應的bin文件,然後忘bin中寫入新的,完成後再序列化到磁碟;

STEP5 生成最終產物
執行ProcessMethodAdapter生成DataBindingComponents.class;執行ProcessExpressions生成ViewDataBinding.class子類(ActivityDetail2Binding.class),並觸發DataBindingMapper.class更新;執行ProcessBindable生成BR.class,並觸發DataBindingMapper.class更新;

7 細節補充-View Tag的使用

第二章有講到View是如何注入的,其實需要分兩種情況:

1.如果這個View標籤屬性中只有id,沒有其他"@{表達式}"形式,則按照第2章提到的方式直接通過id查找;

2.如果這個View標籤屬性中有"@{表達式}"形式的值,則編譯器會自動給這個View加個android:tag="binding_{N}", 其中{N}按順序從0開始遞增,如android:tag="binding_0"。當執行ViewDataBinding#mapBindings去注入View時,會找tag為binding_開頭的View,隨後執行View注入;

另外,如果View標籤原來就有android:tag值,則編譯器會先保存原有值信息,寫入android:tag="binding_{N}"。當執行完view注入後,再把原來的值賦值給android:tag。注意如果原來的android:tag值為"binding_0",那麼在View注入時將會發生錯亂。

在完成View注入後,ActivityDetail3Binding會執行this.setRootTag(root),代碼如下:

這與ListView中的ViewHoloder實現方式相似,所以如果把DataBinding運用到ListView的ViewHolder中,就不需要多生成一個ViewHolder,直接使用這個ViewDataBinding類即可,例如ListAdapter實現:

8 總結

DataBinding 庫非常小
目前Android Data Binding在運行類庫只有632個方法數,算上每個layout.xml自動生成的ViewDataBinding子類(demo中每個類不超過20個方法數),方法數總和也非常有限。


Data Binding方法數

DataBinding 運行時沒有多餘性能損耗
DataBinding所有的View注入、View賦值、Binding都是編譯器自動生成的代碼,這些重複的體力勞動本身就需要去做,只是交給了編譯器來完成,所以運行時沒有多餘的性能損耗。

DataBinding 可以減少錯誤率
既然View注入、View賦值、Binding都是編譯器自動完成的,只要使用正確,100%無低級錯誤保證,可以提高代碼質量,讓開發者心情愉悅。

DataBinding 對編譯時長的影響
還沒實際運用到生產環境,肯定有所延長,具體量級還未知。

9 Reference

官方Data-Binding-Guide
楊輝的個人博客-(譯)Data Binding 指南
LyndonChin/MasteringAndroidDataBinding
googlesource/data-binding

相關焦點

  • 從零開始的Android新項目7 - Data Binding入門篇
    引Data Binding自從去年的Google I/O發布到至今,也有近一年的時間了。這一年來,從Beta到如今比較完善的版本,從Android Studio 1.3到如今2.1.2的支持,可以說Data Binding已經是一個可用度較高,也能帶來實際生產力提升的技術了。然而事實上,真正使用到Data Binding的公司、項目仍然是比較少的。
  • 從零開始的Android新項目8 - Data Binding高級篇
    開源方案及其局限性evant / binding-collection-adapterradzio / android-data-binding-recyclerview均提供了簡化的RV data binding方案。
  • 如何通過Data Binding提升擼碼逼格(進階篇)
    extends RecyclerView.ViewHolder {    private ItemLoveHistoryShowBinding binding;    public ShowLoveHistoryHolder(ItemLoveHistoryShowBinding binding) {        super(binding.getRoot());
  • Android上玩玩Hook:Cydia Substrate實戰
    而「鉤子」的意思,就是在事件傳送到終點前截獲並監控事件的傳輸,像個鉤子勾上事件一樣。並且能夠在勾上事件時,處理一些自己特定的事件。如下圖所示:安裝Cydia Substrate框架Android本地服務首先就是在Android設備中安裝Cydia Substrate框架的本地服務應用substrate.apk,我們可以在其官網下載到。
  • Android MVP 實例
    可以點擊「閱讀原文」到原博客!效果預覽準備MVP流程步驟1:UI實現View方法,引用Presenter步驟2:Presenter調用Model,走Model具體邏輯步驟3:Model邏輯實現,回調Presenter方法步驟4:Presenter回調View,即回到UI,回調View方法gradle文件compile 'com.loopj.android
  • 放蕩不羈SVG講解與實戰——Android高級UI
    老規矩,先上實戰圖。"手寫"掘金地圖查閱器對 「屬性動畫」 源碼興趣的童鞋可以移步小盆友的另一篇博文,帶有活力的屬性動畫源碼分析與實戰https://juejin.im/post/5c595158f265da2d9710cb6e接下來的一個問題就是,屬性動畫反射回調的類是哪個類呢?這裡有兩種情況,一種是針對 Group 標籤,一種是針對 Path 標籤。
  • 最新Android框架排行榜,上百項資源匯總!
    {    compile 'org.greenrobot:greendao:3.2.2' // add library}一句話介紹:一款提供在Chrome開發者工具上調試Android app能力的開源框架上榜理由:上古時期Android程式設計師要調試本地資料庫,需要進入Android Device Monitor找到/data/data/com.xxx.xxx/databases
  • 【Android 原創】實戰分析一個Crackme的過程(超級詳細)
    作者論壇帳號: 佚名RJ[超級詳細]實戰分析一個Crackme的過程一、寫在前面自學這個也有幾個星期了,今天就總結一下近期學習的成果,實戰分析一個Crackme,並寫下了這篇超級詳細的過程,從軟體環境的配置到軟體詳細的使用再到最後的逆向分析出結果,為了文章的貼圖方便和文章美觀展示,用了兩臺電腦+MUMU模擬器+Google6.0.1版真機,相互切換著截圖附在帖子裡,也主要是因為模擬器動態調試so加載好像有問題。
  • Android之Binder底層原理詳解必讀
    {android.os.Parcel _data = android.os.Parcel.obtain();android.os.Parcel _reply = android.os.Parcel.obtain();try {_data.writeInterfaceToken(DESCRIPTOR);_data.writeInt(anInt);_data.writeLong(aLong);_data.writeInt
  • Android Glide庫使用,一句話完成圖片加載、gif、高斯模糊等 && 【菜鳥窩的三個實戰項目推薦】
    首先分享Android方面一些菜鳥窩的實戰課程給大家,課程包含App應用市場、菜鳥商城和直播平臺,3個課程都是精心挑選出來的,大家可以先看看課程簡介和涉及到的技術
  • Android壓力測試Monkey工具
    最近在Android程序測試過程中接觸到了自動化測試方法,對其中的一些工具、方法和框架做了一些簡單的整理,其中包括android測試框架、CTS、Monkey、Monkeyrunner其它test tool等等。因接觸時間很短,很多地方有不足之處,希望能和大家多多交流和指點在這裡簡單對monkey做簡單的介紹吧。
  • Android Schema協議略知一二
    :name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <data android:scheme="artist" android
  • Android Studio 3.6 穩定版發布 - OSCHINA - 中文開源技術交流社區
    /studio/#downloads當在代碼中引用視圖時,View binding 通過提供編譯時(compile-time)安全性,方便開發者更輕鬆地編寫與視圖交互的代碼。啟用此功能後,View binding 將為該 module 中包含的每個 XML 布局文件生成一個綁定類。在大多數情況下,view binding 會替換 findViewById。
  • Android PMS處理APK的複製
    mBound) {                   Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "bindingMCS",                      System.identityHashCode(mHandler));                                    if
  • Android新手入門-Android中文SDK
    (After reading the sections above, the following Getting Started information is also very useful)核心包 (Core Packages)這些是基本包在通過Android SDK來編寫應用程式,這些包作為原始層,列出的是從最底層到最高層。
  • 最新Android框架排行榜,上百項資源匯總不容錯過
    /data/com.xxx.xxx/databases裡面的db文件,導出到PC端,用PC的數據工具查看,現在使用stetho省卻了如此的麻煩;如今的Android程式設計師如果想調試網絡請求響應過程中的報文段,需要在請求中加入Log語句,一個信息一個信息列印出來,相當繁瑣,現在請使用stetho,省卻諸如此類的麻煩把!
  • 來談一下android中的MVVM
    MVC缺點:不適合小型,中等規模的應用程式,花費大量時間將MVC應用到規模並不是很大的應用程式通常會得不償失。視圖與控制器間過於緊密連接,視圖與控制器是相互分離,但卻是聯繫緊密的部件,視圖沒有控制器的存在,其應用是很有限的,反之亦然,這樣就妨礙了他們的獨立重用。
  • Android Hook神器:XPosed入門與登陸劫持演示
    下載完畢後我們需要將Xposed Library複製到lib目錄(注意是lib目錄,不是Android提供的libs目錄),然後將這個jar包添加到Build PATH中。
  • 【Android基礎學習一】Android 常用 adb 命令總結
    and cache directories , -k 選項,卸載時保存數據和緩存目錄adb pull , 將 Android 設備上的文件或者文件夾複製到本地例如複製 Sdcard 下的 pull.txt 文件到 D 盤:adb pull sdcard/pull.txt d:\如果需要重命名為 rename.txt:
  • Android圖片壓縮的幾種方案
    ,並將其上傳到Linux伺服器的某個目錄2、將生成的 libjpeg.a和頭文件導入到我們的項目中>5、聲明權限<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>使用LibJpeg