A man can be destroyed but not defeated. —— Daily English
DataBinding是一種工具,能在編譯時綁定佈局和對象。通過這篇文章,一是要掌握DataBinding的使用,二是我們要弄懂,View層是怎麼改變Model的,而Model層又是如何改變View的。
介紹
APT預編譯方式
我們已經知道,DataBinding裏面的功能類是通過APT的工具來產生的。
APT(Annotation Processing Tool)即註解處理器,是一種處理註解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理註解。註解處理器以Java代碼(或者編譯過的字節碼)作爲輸入,生成.java文件作爲輸出。簡單來說就是在編譯期,通過註解生成.java文件。
當你的xml用DataBinding規定的格式去書寫的時候,DataBinding就能夠通過APT的技術,幫你生成對應的類文件。
Rebuild完成後,會在build
目錄下生成以[佈局名]Binding.java
文件,這裏我們的佈局叫activity_main
,所以生成了一個名爲ActivityMainBinding.Java
類文件,以及繼承自它的ActivityMainBindingImpl.java
類文件。
生成文件的具體位置參考下圖
佈局的格式和處理
我們需要規定的格式來定義我們的xml佈局文件,參考佈局activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding編碼規範 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 定義該View(佈局)需要綁定的數據來源 -->
<data>
<variable
name="user"
type="pers.owen.databinding.model.UserInfo" />
</data>
<!-- 佈局常規編碼 -->
<LinearLayout...>
<EditText ... />
<EditText ... />
</LinearLayout>
</layout>
<data>
標籤中綁定的可以是一個實體類,也可以是一個ViewModel,只要按照規定的格式去寫,就可以。
在編譯時,DataBinding會內部處理佈局文件控件,重新生成兩個全新的佈局文件。一個與原文件同名,叫activity_main.xml
,另一個是名爲activity_main-layout.xml
。
生成文件的具體位置參考下圖
生成後的activity_main.xml
就跟我們普通的xml文件一樣,用於Android os的渲染。但是DataBinding會在每個控件上都加上Tag,這個Tag就是用來快速查找和定位具體控件的。
生成後的activity_main.xml
代碼如下
<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding編碼規範 -->
<!-- 定義該View(佈局)需要綁定的數據來源 -->
<!-- 佈局常規編碼 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" android:tag="layout/activity_main_0" xmlns:android="http://schemas.android.com/apk/res/android">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_1" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:tag="binding_2" />
</LinearLayout>
生成的activity_main-layout.xml
文件,是DataBinding生成的配置文件,它能夠通過文件中的配置信息,快速定位哪一個控件,該顯示什麼信息。
activity_main-layout.xml
代碼如下
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="C:\workspace\adrproject\MyDataBinding\app\src\main\res\layout\activity_main.xml"
isBindingData="true"
isMerge="false" layout="activity_main" modulePackage="pers.owen.databinding"
rootNodeType="android.widget.LinearLayout">
<Variables name="user" declared="true" type="pers.owen.databinding.model.UserInfo">
<location endLine="7" endOffset="57" startLine="5" startOffset="8" />
</Variables>
<Targets>
<Target tag="layout/activity_main_0" view="LinearLayout">
<Expressions />
<location endLine="27" endOffset="18" startLine="11" startOffset="4" />
</Target>
<Target tag="binding_1" view="EditText">
<Expressions>
<Expression attribute="android:text" text="user.name">
<Location endLine="20" endOffset="39" startLine="20" startOffset="12" />
<TwoWay>true</TwoWay>
<ValueLocation endLine="20" endOffset="37" startLine="20" startOffset="29" />
</Expression>
</Expressions>
<location endLine="20" endOffset="42" startLine="17" startOffset="8" />
</Target>
<Target tag="binding_2" view="EditText">
<Expressions>
<Expression attribute="android:text" text="user.pwd">
<Location endLine="26" endOffset="38" startLine="26" startOffset="12" />
<TwoWay>true</TwoWay>
<ValueLocation endLine="26" endOffset="36" startLine="26" startOffset="29" />
</Expression>
</Expressions>
<location endLine="26" endOffset="41" startLine="22" startOffset="8" />
</Target>
</Targets>
</Layout>
佈局這塊,我們需要做的就是按照DataBinding要求的格式,定義好佈局即可,其他都會在Rebuild的時候,自動生成。
關聯Activity組件與佈局
我們通過代碼DataBindingUtil.setContentView(Activity,Layout);
來關聯Activity組件與佈局。有人可能會疑問,爲什麼不直接在Activity中setCentView。那是因爲DataBinding需要Activity來獲取根佈局,到時候View層刷新後會通過根佈局來查找並更新相應的控件。
何時生成設置Model幫助類?
在Rebuild的時候DataBinding會掃描所有Module中的xml文件。只有當你的xml佈局中有符合DataBinding定義規範的<data>
標籤,纔會去生成對應的幫助類。
DataBinding TestActivity.java
<–> activity_ databinding_test.xml
--> ActivityDatabindingTestBinding
--> ViewDataBinding
實戰
我們這邊寫一個小Demo,實現DataBinding的雙向數據綁定即可。
引入
在module的build.gradle文件中引入DataBinding
android {
...
// 添加DataBinding依賴
dataBinding{
enabled = true
}
)
定義實體類
定義實體類,可以定義成原始的屬性格式,也可以定義成被觀察者屬性格式。
我們先試一下定義成原始屬性格式的實體類,也就是我們之前定義實體類的格式。
public class UserInfo extends BaseObservable {
private String name;
private String pwd;
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
@Bindable
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
notifyPropertyChanged(BR.pwd);
}
}
注意三點,1、繼承BaseObservable
類,2、get()
方法上加上@Bindable
註解,3、set()
方法中加入notifyPropertyChanged
。
定義Layout佈局
頁面很簡單就不截圖了,兩個EditText,分別對應UserInfo中的name和pwd。
<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding編碼規範 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 定義該View(佈局)需要綁定的數據來源 -->
<data>
<variable
name="user"
type="pers.owen.databinding.model.UserInfo" />
</data>
<!-- 佈局常規編碼 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.name}" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@={user.pwd}" />
</LinearLayout>
</layout>
Rebuild Project
Rebuild完成後,就會ActivityMainBinding.Java
文件和ActivityMainBindingImpl.java
文件,生成目錄可參看本文開頭部分。
書寫代碼綁定
public class MainActivity extends AppCompatActivity {
private UserInfo userInfo = new UserInfo();
private final static String TAG = "TAG >>>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 嘗試1:單向綁定第一種方式:<Model -- View>
userInfo.setName("Owen");
userInfo.setPwd("123");
binding.setUser(userInfo);
Log.e(TAG, userInfo.getName() + " / " + userInfo.getPwd());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
userInfo.setName("Sara");
userInfo.setPwd("222");
Log.e(TAG, userInfo.getName() + " / " + userInfo.getPwd());
}
}, 2000);
}
}
通過DataBindingUtil.setContentView(this, R.layout.activity_main)
綁定佈局,獲取到ActivityMainBinding
對象binding
,再通過binding.setUser(userInfo)
綁定實體類。
我們做了嘗試1,給UserInfo的Name和Pwd設值,用Handler延時2s後重新改變model的屬性值,來查看與之綁定的View是不是會發生變化。效果圖如下:
我們看到結果是符合我們預期的,驗證了Model層JavaBean中的屬性值改變,會更新到View層的控件上。
定義被觀察屬性的實體類
我們前面說過了,實體類的定義還有第二種方式,格式如下:
public class UserInfo {
public ObservableField<String> name = new ObservableField<>();
public ObservableField<String> pwd = new ObservableField<>();
}
顯然,這種方式直接把屬性定義成ObservableField<T>
,這是DataBinding爲我們提供的,大大減少了實體類定義的複雜度。
定義實體類的時候,我們需要記住兩點規則:一個是有被觀察者屬性,第二個就是有刷新屬性的方法。
在原生的實體類中,我們需要自己去繼承,去加註解,調用notifyPropertyChanged
去實現。而DataBinding爲我們提供的ObservableField<T>
本身就是一個觀察者屬性。
這種定義方式對實體類屬性有特定的取值和賦值方式,我們來看第二個嘗試
public class MainActivity extends AppCompatActivity {
private UserInfo userInfo = new UserInfo();
private final static String TAG = "TAG >>>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 嘗試1:單向綁定第一種方式:<Model -- View>
...
// 嘗試2:單向綁定第二種方式:<Model -- View>
userInfo.name.set("Owen");
userInfo.pwd.set("123");
binding.setUser(userInfo);
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
userInfo.name.set("Sara");
userInfo.pwd.set("222");
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
}
}, 2000);
}
}
運行效果和嘗試1完全一致。
雙向的數據綁定
以上都是Model層的數據改變,更新View層的顯示,也就是所謂的單向綁定。我們來做第三個嘗試,測試數據的雙向綁定,不僅看Model–>View 的更新,也來看一下View層數據的改變,是不是也能更新其綁定Model中屬性的值。
public class MainActivity extends AppCompatActivity {
private UserInfo userInfo = new UserInfo();
private final static String TAG = "TAG >>>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 嘗試1:單向綁定第一種方式:<Model -- View>
...
// 嘗試2:單向綁定第二種方式:<Model -- View>
...
// 嘗試3:雙向綁定(Model --- View View --- Model)
userInfo.name.set("Owen");
userInfo.pwd.set("123");
binding.setUser(userInfo);
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
}
}, 6000);
}
}
我們先給UserInfo,設置Name和Pwd,檢測是否會更新到View上。然後利用Handler延遲6s後重新讀取UserInfo的屬性。在這6s內,我們需要在界面上修改Name和Pwd對應的EditText值,來進行驗證。
效果如下
logcat控制檯輸出
這個結果是符合預期的,藉助DataBinding成功實現了雙向的數據綁定。
如果View到Model更新失敗的同學,可以參考排查xml佈局文件中,android:text="@={user.name}"
處@
後面的=
是否有加上。
文中Demo會在文末給出。
源碼分析
綁定過程
我們從DataBindingUtil.setContentView(this, R.layout.activity_main)
方法開始,跟蹤這個綁定的過程。
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
↓
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId) {
return setContentView(activity, layoutId, sDefaultComponent);
}
↓
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
...
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
↓
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
...
return bind(component, childView, layoutId);
}
↓
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
}
綁定的過程最後跟蹤到了sMapper.getDataBinder(bindingComponent, roots, layoutId)
方法,我們來看這個方法是如何實現的。查找該方法的實現。
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
...
if ("layout/activity_main_0".equals(tag)) {
return new ActivityMainBindingImpl(component, view);
}
...
}
activity_main_0
是不是很熟悉。我們已經知道,在Rebuild項目的時候,我們xml文件會被重新生成兩個xml文件。生成後的activity_main.xml
中所有的控件,都被添加了tag屬性,而最外層layout的tag就是用[佈局名]_0
組成的。可參考本文章前段部分關於生成新xml的代碼。
所以當找到我們常規的佈局文件後,就開始new ActivityMainBindingImpl(component, view)
創建DataBinding的功能類,開始搞事情。
我們來看ActivityMainBindingImpl
類構造方法的實現。
private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 2
);
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
this.mboundView1 = (android.widget.EditText) bindings[1];
this.mboundView1.setTag(null);
this.mboundView2 = (android.widget.EditText) bindings[2];
this.mboundView2.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
標記1。我們可以看到這裏是有一個Object[] bindings
數組來記錄我們佈局文件中的三個控件。分別設置給了類中的三個成員變量mboundView0
,mboundView1
和mboundView2
。
然後執行invalidateAll()
,我們對該方法做一個跟蹤。
@Override
public void invalidateAll() {
....
requestRebind();
}
↓
protected void requestRebind() {
....
mUIThreadHandler.post(mRebindRunnable);
}
標記2。我們看到,DataBinding會定義一個Handler去執行mRebindRunnable
,這個mRebindRunnable
裏面也就是我們V和M雙向綁定的核心核能。我們來看這個mRebindRunnable
是如何實現的。
private final Runnable mRebindRunnable = new Runnable() {
...
executePendingBindings();
};
↓
public void executePendingBindings() {
...
executeBindingsInternal();
}
↓
private void executeBindingsInternal() {
...
executeBindings();
...
}
mRebindRunnable
最終會執行到executeBindings()
方法。executeBindings()
方法是在ActivityMainBindingImpl
中實現的,代碼如下。
@Override
protected void executeBindings() {
...
androidx.databinding.ObservableField<java.lang.String> userName = null;
java.lang.String userPwdGet = null;
pers.owen.databinding.model.UserInfo user = mUser;
java.lang.String userNameGet = null;
androidx.databinding.ObservableField<java.lang.String> userPwd = null;
...
userNameGet = userName.get();
...
userPwdGet = userPwd.get();
...
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, userNameGet);
...
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView1, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView1androidTextAttrChanged);
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView2, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView2androidTextAttrChanged);
...
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, userPwdGet);
...
}
最後這四行代碼,短的這兩行是setText()
,作用是把從JavaBean屬性中讀出來的值,設置到View上。也就是實現了Model到View更新。長的這兩行是setTextWatcher()
,作用可想而知,就是爲View控件設置監聽,以改變對應JavaBean的屬性的值。這樣也就實現了View到Model的更新。
我們來看這兩個監聽中其中一個mboundView1androidTextAttrChanged
是如何定義的。
private androidx.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
@Override
public void onChange() {
...
java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
...
userName.set(((java.lang.String) (callbackArg_0)));
...
};
把多餘的代碼去掉,邏輯不要太清晰。
至此,我們就完全弄懂了DataBinding的綁定過程。知道了,View層是怎麼改變Model的,而Model層又是如何改變View的,這個數據雙向綁定過程。
整個過程中,沒有反射!
那這個mRebindRunnable
又是何時被執行的呢,換句話說,我們這個雙向數據的更新動作是什麼時候執行的呢?
我們找到mRebindRunnable
調用的位置,在ViewDataBinding
的靜態代碼塊,會有一個全局的監聽,這個監聽也就是用來通知數據更新的。
static {
if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
ROOT_REATTACHED_LISTENER = null;
} else {
ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
@TargetApi(VERSION_CODES.KITKAT)
@Override
public void onViewAttachedToWindow(View v) {
// execute the pending bindings.
final ViewDataBinding binding = getBinding(v);
binding.mRebindRunnable.run();
v.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) {
}
};
}
}
標記3。每當View改變的時候,這個監聽中的回調方法就會被執行,通過一個Handler去post我們的mRebindRunnable
,從而更新Model。反之亦然。
至此,綁定過程分析完畢。
內存消耗簡析
我在綁定過程中標記了三個標記,想必大家都注意到了。這三個標記就是需要額外消耗內存的地方。
標記1:額外數組
定義了一個額外的數組來記錄這些控件。數組造成的額外內存開銷。
標記2:Runable
只要當控件發生任何的數據改變,它都有一個監聽。在這個監聽中,Binder會new一個Runable。
每個Activity都會有一個Runable,10個Activity就會創建10個Runable。
標記3:handler的loop一直在等待狀態。
一旦Model的數據刷新,最後都是通過Handler去刷新UI的。我們知道Handler中有一個管道,會一直等待消息進來,然後通過Lopper.loop()方法,去取消息處理。當有消息了,就通過這個Tag找到控件,爲控件賦值。Handler的等待造成的額外內存開銷。
本文完!相信堅持看完本文的同學一定會有收穫。
文中Demo下載地址。