DataBinding學習筆記(一)原始碼分析
DataBinding整體使用流程
開發階段
UserModel.java
public class UserModel {
public String name;
public String nickName;
public int age;
public UserModel(String name, String nickName, int age) {
this.name = name;
this.age = age;
this.nickName = nickName;
}
public boolean isAge18() {
return age >= 18;
}
}
activity_main.xml
在xml中使用”@{}”識別符號
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="user" type="com.listen.test_databinding.UserModel" />
<variable name="testClick" type="android.view.View.OnClickListener"/>
<import type="android.view.View"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.listen.test_databinding.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"名字" + user.name}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.nickName}'
android:visibility="@{null == user.nickName ? View.VISIBLE : View.GONE}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.isAge18() ? "man" : "boy"}'/>
<Button
android:id="@+id/btn_test"
android:layout_width="match_parent"
android:layout_height="50dp"
android:onClick="@{testClick}" android:text="測試"/>
</LinearLayout>
</layout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding mBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
final UserModel user = new UserModel("listen", "ls", 18);
mBinding.setUser(user);
mBinding.setTestClick(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "testClick", Toast.LENGTH_SHORT).show();
}
});
}
}
編譯階段
1.Databinding會自動解析識別xml中的”@{}”識別符號,並在以下目錄生成2個xml檔案
1.build/intermediates/data-binding-layout-out/activity_main.xml
2.build/intermediates/data-binding-info/debug/activity_main-layout.xml
activity_main.xml
帶“@{}”的xml檔案是android系統無法識別的,為了向後相容,需要在編譯期統一轉換成系統能識別的標準xml佈局,而原先在佈局中新增的”@{}”,”@{三目運算子}”等資訊,則會儲存在activity_main-layout.xml中。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:tag="layout/activity_main_0"
tools:context="com.listen.test_databinding.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_2"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_3"/>
<Button
android:id="@+id/btn_test"
android:layout_width="match_parent"
android:layout_height="50dp"
android:tag="binding_4" android:text="測試"/>
</LinearLayout>
activity_main-layout.xml(xml描述檔案)
1.任何view只要用到了”@{}”標識,就會在activity_main-layout.xml中生成target描述,並根據該view在parent中的位置生成”binding_[index]”標識,並設定在tag中。
2.如果一個view即沒有設定”android:id”,也沒有使用”@{}”標識,則不會在activity_main-layout.xml中生成這個view的target描述。
3.LinearLayout比較特殊,並沒有設定”android:id”,也沒有使用”@{}”,但還是會生成一個預設的tag=”layout/activity_main_0”,表示它是根佈局,在ViewDataBinding.java例項化時,需要判斷根佈局的tag,後面原始碼會分析到。
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout absoluteFilePath="/Users/lisong/Documents/AndroidStudioWorkSpace/Test_Databinding/app/src/main/res/layout/activity_main.xml" directory="layout"
isMerge="false"
layout="activity_main" modulePackage="com.listen.test_databinding">
<Variables name="user" declared="true" type="com.listen.test_databinding.UserModel">
...
</Variables>
<Variables name="testClick" declared="true" type="android.view.View.OnClickListener">
...
</Variables>
<Imports name="View" type="android.view.View">
...
</Imports>
<Targets>
<Target tag="layout/activity_main_0" view="LinearLayout">
<Expressions/>
...
</Target>
<Target tag="binding_1" view="TextView">
<Expressions>
<Expression attribute="android:text" text=""名字" + user.name">
...
</Expression>
</Expressions>
</Target>
<Target tag="binding_2" view="TextView">
<Expressions>
<Expression attribute="android:text" text="user.nickName">
...
</Expression>
<Expression attribute="android:visibility"
text="null == user.nickName ? View.VISIBLE : View.GONE">
...
</Expression>
</Expressions>
</Target>
<Target tag="binding_3" view="TextView">
<Expressions>
<Expression attribute="android:text"
text="user.isAge18() ? "man" : "boy"">
...
</Expression>
</Expressions>
</Target>
<Target id="@+id/btn_test" tag="binding_4" view="Button">
<Expressions>
<Expression attribute="android:onClick" text="testClick">
...
</Expression>
</Expressions>
</Target>
</Targets>
</Layout>
2.生成ActivityMainBinding.java和BR.java
DataBinding根據解析後的activity_main-layout.xml,和layout下的activity_main.xml檔案,生成build/intermediates/classes/debug/[專案路徑]/databinding/
ActivityMainBinding.java和BR.java
ActivityMainBinding主要具備以下功能
1.作為view和model的聯結器,持有需要展示的資料和views的成員變數
2.將資料對映到view(就是setText,setOnClick等)
3.在UI執行緒更新資料
BR.java就是一個常量類
可以通過binding.setVariable(BRuser, new User())進行資料更新
public class BR {
public static final int _all = 0;
public static final int testClick = 1;
public static final int user = 2;
public BR() {
}
}
public boolean setVariable(int variableId, Object variable) {
switch(variableId) {
case BR.testClick :
setTestClick((android.view.View.OnClickListener) variable);
return true;
case BR.user :
setUser((com.listen.test_databinding.UserModel) variable);
return true;
}
return false;
}
執行階段
Databinding框架最主要做的事,就是以上2步,接下來就是在程式碼中呼叫生成的ViewDataBinding,並進行資料繫結操作。
DataBindingUtil是一切的入口
ActivityMainBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId, DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);// 最終呼叫的還是activity.setContentView(),不過這裡的layoutId是已經去掉"@{}"的標準xml佈局
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);//獲取根頂級容器view
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
取出佈局的rootView,呼叫ActivityMainBinding.bind()
private static <T extends ViewDataBinding> T bindToAddedViews(android.databinding.DataBindingComponent component, ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
final int childrenAdded = endChildren - startChildren;
if (childrenAdded == 1) {
// 從頂級容器view中獲取當前佈局的rootView,呼叫bind方法
final View childView = parent.getChildAt(endChildren - 1);
return bind(component, childView, layoutId);
} else {
...
}
}
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
// sMapper = DataBinderMapper.java
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
/** DataBinderMapper.java */
public android.databinding.ViewDataBinding getDataBinder(android.databinding.DataBindingComponent bindingComponent, android.view.View view, int layoutId) {
switch(layoutId) {
case com.listen.test_databinding.R.layout.activity_main:
// 將rootView傳遞給ActivityMainBinding.bind()
return com.listen.test_databinding.databinding.ActivityMainBinding.bind(view, bindingComponent);
}
return null;
}
此處做了rootView的判斷,如果傳遞過來的不是當前ViewDataBinding繫結的佈局,則拋異常。所以即使rootView沒有設定id,及”@{}”,在info-layout.xml中也會生成相應的target描述。
public static ActivityMainBinding bind(View view, DataBindingComponent bindingComponent) {
if(!"layout/activity_main_0".equals(view.getTag())) {
throw new RuntimeException("view tag isn\'t correct on view:" + view.getTag());
} else {
return new ActivityMainBinding(bindingComponent, view); // ActivityMainBinding在此處初始化
}
}
這裡需要特別注意的是在編譯期自動生成的activity_main.xml檔案中自動添加了tag=”binding_1”,”binding_2”等,其實在初始化完這些view後,都已經清空,是不影響我們在程式碼中設定tag的;不過rootView並沒有清除tag(就是xml佈局最外層的layout),如果>=14以上版本,在程式碼裡設定setTag(R.id.databinding,”anything”),或,<14版本,在程式碼裡設定setTag(“anything”),則會報錯,so,這個tag是由DataBinding佔著的,使用上得小心。
public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
super(bindingComponent, root, 0);
// 遍歷佈局,找到所有views,並存儲在bindings[]中,5表示佈局一共有5個view,sIncludes儲存被include進 // 來的佈局,sViewsWithIds儲存設定了"android:id",但是沒有用到"@{}"的view
Object[] bindings = mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds);
// 將bindings[]中的view取出,賦值給當前各個view的成員變數,並清除tag,避免衝突
this.btnTest = (Button)bindings[4];
this.btnTest.setTag((Object)null);
this.mboundView0 = (LinearLayout)bindings[0];
this.mboundView0.setTag((Object)null);
this.mboundView1 = (TextView)bindings[1];
this.mboundView1.setTag((Object)null);
this.mboundView2 = (TextView)bindings[2];
this.mboundView2.setTag((Object)null);
this.mboundView3 = (TextView)bindings[3];
this.mboundView3.setTag((Object)null);
this.setRootTag(root);
/**
ViewDatabBinding.java
protected void setRootTag(View view) {
//private static final boolean USE_TAG_ID = DataBinderMapper.TARGET_MIN_SDK >= 14;
if (USE_TAG_ID) {
view.setTag(R.id.dataBinding, this);
} else {
view.setTag(this);
}
}
*/
// 請求重新整理,實現資料與view的繫結
this.invalidateAll();
}
mapBindings(),其實就是遞迴遍歷view樹的過程,不過不是byId,而是byTag,尋找以”binding_”開頭的view,並取出”binding_[索引]”中的索引,賦值給binding[]陣列。所有的view只在一次遍歷中獲得,而如果是用findViewById的方式,每次呼叫都需要遍歷一次view樹[效能對比]。需要特別注意的是binding陣列的元素不一定都是view或viewGroup,如果有include佈局的時候binding陣列儲存的可能是include佈局的viewDataBinding物件。
private static void mapBindings(DataBindingComponent bindingComponent, View view, Object[] bindings, ViewDataBinding.IncludedLayouts includes, SparseIntArray viewsWithIds, boolean isRoot) {
ViewDataBinding existingBinding = getBinding(view);
if(existingBinding == null) {
Object objTag = view.getTag();
String tag = objTag instanceof String?(String)objTag:null;
if(isRoot && tag != null && tag.startsWith("layout")) {
// 如果是rootView,則從"layout/activity_main_0"中取出索引"0",設定到bindings[0]中
viewGroup = tag.lastIndexOf(95);
count = parseTagInt(tag, viewGroup + 1);
if(bindings[count] == null) {
bindings[count] = view;
}
...
} else if(tag != null && tag.startsWith("binding_")) {
// 同樣判斷tag,取出"binding_1","bingding_2"中的索引,賦值到bindings[]中
viewGroup = parseTagInt(tag, BINDING_NUMBER_START);
if(bindings[viewGroup] == null) {
bindings[viewGroup] = view;
}
...
}
// isBound=false,說明當前的view既不是根佈局,也沒有用到"@{}"(如果有用到就會生成"binding_"
// 的tag);則通過id獲取該view,並設定到bingding[]
// 如果存在設定了id,但是沒有“@{}”的view會被新增到sViewsWithIds中,如果
// "binding_[index]"的index最大為3,則view的起始index設定為4。
// static {
// sIncludes = null;
// sViewsWithIds = new android.util.SparseIntArray();
// sViewsWithIds.put(R.id.btn_test, 4);
//}
if(!isBound) {
viewGroup = view.getId();
if(viewGroup > 0 && viewsWithIds != null && (count = viewsWithIds.get(viewGroup, -1)) >= 0 && bindings[count] == null) {
bindings[count] = view;
}
}
if(view instanceof ViewGroup) {
// 如果是view是個viewGroup,則遍歷子view
ViewGroup var25 = (ViewGroup)view;
count = var25.getChildCount();
...
for(int i = 0; i < count; ++i) {
View child = var25.getChildAt(i);
boolean isInclude = false;
if(indexInIncludes >= 0 && child.getTag() instanceof String) {
String childTag = (String)child.getTag();
if(childTag.endsWith("_0") && childTag.startsWith("layout") && childTag.indexOf(47) > 0) {
// 如果當前view也是一個rootView,則判斷tag中是否有include標識資訊
// 如果包含include標籤,生成的info-layout檔案應該是以下樣式:
// <Target include="include_main" tag="layout/activity_main_0">
// </Target>
int includeIndex = findIncludeIndex(childTag, minInclude, includes, indexInIncludes);
if(includeIndex >= 0) {
isInclude = true;
...
// 如果包含include資訊,則重新呼叫DataBindingUtil.bind()生成ViewDataBinding,重複當前流程,
// 不過當前的bindings[index]就不是一個view,而是一個viewDataBinding
bindings[index] = DataBindingUtil.bind(bindingComponent, child, layoutId);
...
}
}
}
if(!isInclude) {
// 如果只是一個viewGroup,不是include進來的佈局,則重新呼叫mapBindings,只是isRoot=false,則會上面進入"binding_"的判斷邏輯
mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
}
}
}
}
}
view遍歷流程圖
View都找到了,現在該是時候設定listener,data的時候了。這時候會通過invalidateAll()請求資料更新,層層呼叫後,還是回到了ActivityMainBinding的executeBindings(),在這個方法裡將更新後的model資料,onclick等重新設定到Textview,Button上,完成了model->view的單向繫結。
// 子類:xxxViewDataBinding extends ViewDataBinding
public void invalidateAll() {
synchronized(this) {
this.mDirtyFlags = 4L;
}
this.requestRebind();
}
// 父類:ViewDataBinding.java
// 通過handler.post()執行mRebindRunnable
protected void requestRebind() {
...
mUIThreadHandler.post(mRebindRunnable);
}
// mRebindRunnable呼叫了executePendingBindings()
private final Runnable mRebindRunnable = new Runnable() {
@Override
public void run() {
...
executePendingBindings();
}
};
// executePendingBindings呼叫了executeBindings()
public void executePendingBindings() {
...
executeBindings();
...
}
// 子類:xxxViewDataBinding extends ViewDataBinding
protected void executeBindings() {
...
// 當呼叫ViewDataBinding.setUser(new User())時,就是給成員變數mUser賦值,在這裡獲取this.mUser
UserModel user = this.mUser;
if((dirtyFlags & 6L) != 0L) {
// 獲取並構建資料,所以model中的欄位要麼為public,要麼提供一個getter方法,不然這裡無法獲取
// 可以看到,在xml中的@{}表示式,此時已經解析成對應的方法isAge18,user.name等
if(user != null) {
userIsAge18User = user.isAge18();
nameUser = user.name;
nickNameUser = user.nickName;
}
...
// 獲取並構建資料
userIsAge18UserStrin = userIsAge18User?"man":"boy";
stringNameUser = "名字" + nameUser;
ObjectnullNickNameUs1 = null == nickNameUser;
...
objectnullNickNameUs = ObjectnullNickNameUs1?0:8;
}
// 設定listener
if((dirtyFlags & 5L) != 0L) {
this.btnTest.setOnClickListener(testClick);
}
// 通過TextViewBindingAdapter將資料設定到TextView上
if((dirtyFlags & 6L) != 0L) {
TextViewBindingAdapter.setText(this.mboundView1, stringNameUser);
TextViewBindingAdapter.setText(this.mboundView2, nickNameUser);
this.mboundView2.setVisibility(objectnullNickNameUs);
TextViewBindingAdapter.setText(this.mboundView3, userIsAge18UserStrin);
}
}