DataBinding的使用與原理

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數組來記錄我們佈局文件中的三個控件。分別設置給了類中的三個成員變量mboundView0mboundView1mboundView2

然後執行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下載地址

Android 架構設計模式系列文章索引

MVC架構設計與經典的三層模型

MVVM實現數據雙向綁定

DataBinding的使用與原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章