源码版本:Android10
一、DataBinding的使用
1.1 activity_databinding_main.xml源码
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="androidx.databinding.ObservableMap" />
<import type="androidx.databinding.ObservableArrayList" />
<import type="com.study.jetpackstudykotlin.databinding.bean.Fields" />
<import type="com.study.jetpackstudykotlin.databinding.bean.Teacher2" />
<import type="android.text.TextUtils" />
<variable
name="user"
type="com.study.jetpackstudykotlin.databinding.bean.User" />
<!-- "<"要用<代替 -->
<variable
name="studentMap"
type="ObservableMap<String,Object>" />
<variable
name="studentArray"
type="ObservableArrayList<Object>" />
<variable
name="teacher"
type="Teacher2" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--
default用于预览展示
nameFormat进行字符串格式化: <string name="nameFormat">Full Name: %1$s:%2$s</string>
-->
<Button
android:id="@+id/tv_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{@string/nameFormat(user.firstName, user.lastName),default=aaaa}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_tip1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ObservableMap:"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_1" />
<TextView
android:id="@+id/tv_last"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{studentMap.lastName,default=yyy}"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_tip1" />
<TextView
android:id="@+id/tv_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(1 + (Integer)studentMap.age),default=111}"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_last" />
<TextView
android:id="@+id/tv_tip2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ObservableArray:"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_age" />
<TextView
android:id="@+id/tv_array_last"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{studentArray[Fields.LAST_NAME],default=yyy}"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_tip2" />
<TextView
android:id="@+id/tv_array_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(1 + (Integer)studentArray[Fields.AGE]),default=111}"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_array_last" />
<TextView
android:id="@+id/tv_tip3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Observable:"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_array_age" />
<TextView
android:id="@+id/tv_observable_last"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{teacher.firstName,default=yyy}"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_tip3" />
<TextView
android:id="@+id/tv_observable_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="@color/colorAccent"
android:shadowDy="10"
android:shadowRadius="10"
android:text="@{teacher.lastName,default=111}"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_observable_last" />
<Button
android:id="@+id/bt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:onClick="jumpActivity"
android:text="RecyclerView"
app:layout_constraintTop_toBottomOf="@id/tv_observable_age" />
<EditText
android:id="@+id/et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入"
android:text="@{teacher.lastName}"
app:layout_constraintTop_toBottomOf="@id/bt" />
<Button
android:id="@+id/bt_confirm"
android:layout_width="match_parent"
android:layout_height="45dp"
android:text="@{teacher.lastName}"
app:layout_constraintTop_toBottomOf="@id/et" />
<com.study.jetpackstudykotlin.databinding.view.DefineImageView
android:id="@+id/iv"
imgContent="@{teacher.lastName}"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@{TextUtils.isEmpty(teacher.lastName)?@color/color_enable:@color/color_unable}"
android:elevation="2dp"
app:layout_constraintBottom_toBottomOf="@id/bt_confirm"
app:layout_constraintLeft_toLeftOf="@id/bt_confirm"
app:layout_constraintRight_toRightOf="@id/bt_confirm"
app:layout_constraintTop_toTopOf="@id/bt_confirm"
app:showShadow="@{teacher.lastName}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
1.2 DataBindingMainActivity源码
package com.study.jetpackstudykotlin.databinding
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableArrayMap
import com.study.jetpackstudykotlin.R
import com.study.jetpackstudykotlin.databinding.bean.Teacher2
import com.study.jetpackstudykotlin.databinding.bean.User
import kotlinx.android.synthetic.main.activity_databinding_main.*
class DataBindingMainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityDatabindingMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_databinding_main
)
tv_1.setOnClickListener {
//修改binding.teacher?.lastName = "confirm",?不可少,因为binding.teacher是由java转换过来的,可能为null
binding.teacher?.lastName = "confirm"
}
binding.user = User("哈哈", "嘻嘻")
binding.studentMap = ObservableArrayMap<String, Any>().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
binding.studentArray = ObservableArrayList<Any>().apply {
add("Byte Dance")
add("Inc.")
add(888)
}
val teacher2 = Teacher2("李", "老师")
binding.teacher = teacher2
var i = 0
Thread {
for (j in 0..10) {
teacher2.firstName = "${teacher2.firstName}$i"
i++
Thread.sleep(100)
}
}.start()
}
fun jumpActivity(view: View) {
startActivity(Intent(this, RecyclerViewActivity::class.java))
}
}
1.3 DefineImageView源码
package com.study.jetpackstudykotlin.databinding.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import androidx.databinding.BindingMethod
import androidx.databinding.BindingMethods
@BindingMethods(
BindingMethod(
type = AppCompatImageView::class,
attribute = "showShadow",
method = "showShadow"
)
)
class DefineImageView : AppCompatImageView {
constructor(context: Context) : this(context, null)
constructor(context: Context, attributes: AttributeSet?) : this(context, attributes, 0)
constructor(context: Context, attributes: AttributeSet?, defStyleAttr: Int) : super(
context,
attributes,
defStyleAttr
)
fun showShadow(content: String) {
visibility = if (content == "confirm") View.VISIBLE else View.GONE
}
}
1.4 ViewAdapter源码
package com.study.jetpackstudykotlin.databinding.viewAdapter;
import android.graphics.drawable.ColorDrawable;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.databinding.BindingAdapter;
import androidx.databinding.BindingConversion;
import androidx.databinding.InverseBindingAdapter;
import androidx.databinding.InverseBindingListener;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
public class ViewAdapter {
@BindingAdapter(value = {"imgContent"})
public static void loadImage(AppCompatImageView imageView, final String content) {
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), content, Toast.LENGTH_SHORT).show();
}
});
}
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
//===================SwipeRefreshLayout双向绑定的具体实现===================
@InverseBindingAdapter(attribute = "refreshing", event = "refreshingAttrChanged")
public static boolean isRefreshing(SwipeRefreshLayout view) {
return view.isRefreshing();
}
@BindingAdapter(value = {"refreshingAttrChanged"}, requireAll = false)
public static void setOnRefreshListener(final SwipeRefreshLayout view,
final InverseBindingListener refreshingAttrChanged) {
if (refreshingAttrChanged != null) {
view.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
refreshingAttrChanged.onChange();
Log.e("refresh::", "当前binding.refresh值为" + view.isRefreshing());
}
});
}
}
@BindingAdapter("refreshing")
public static void setRefreshing(SwipeRefreshLayout view, boolean refreshing) {
if (refreshing != view.isRefreshing()) {
view.setRefreshing(refreshing);
}
}
}
二、数据绑定的源码解析
2.1 使用DataBinding的布局文件如何转化为标准的布局文件
此处以生成activity_databinding_main.xml为例2.1.1 生成标准activity_databinding_main.xml文件所需要的配置文件
配置文件路径:D:\Android\Space\Mine\JetpackStudyKotlin\app\build\intermediates\data_binding_layout_info_type_merge\debug\mergeDebugResources\out\activity_databinding_main-layout.xml<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout layout="activity_databinding_main" modulePackage="com.study.jetpackstudykotlin" filePath="D:\Android\Space\Mine\JetpackStudyKotlin\app\src\main\res\layout\databinding\layout\activity_databinding_main.xml" directory="layout" isMerge="false">
<Variables declared="true" type="com.study.jetpackstudykotlin.databinding.bean.User" name="user">
<location startLine="15" startOffset="8" endLine="17" endOffset="71"/>
</Variables>
<Variables declared="true" type="ObservableMap<String,Object>" name="studentMap">
<location startLine="19" startOffset="8" endLine="21" endOffset="52"/>
</Variables>
<Variables declared="true" type="ObservableArrayList<Object>" name="studentArray">
<location startLine="23" startOffset="8" endLine="25" endOffset="51"/>
</Variables>
<Variables declared="true" type="Teacher2" name="teacher">
<location startLine="27" startOffset="8" endLine="29" endOffset="29"/>
</Variables>
<Imports type="androidx.databinding.ObservableMap" name="ObservableMap">
<location startLine="5" startOffset="8" endLine="5" endOffset="59"/>
</Imports>
<Imports type="androidx.databinding.ObservableArrayList" name="ObservableArrayList">
<location startLine="7" startOffset="8" endLine="7" endOffset="65"/>
</Imports>
<Imports type="com.study.jetpackstudykotlin.databinding.bean.Fields" name="Fields">
<location startLine="9" startOffset="8" endLine="9" endOffset="77"/>
</Imports>
<Imports type="com.study.jetpackstudykotlin.databinding.bean.Teacher2" name="Teacher2">
<location startLine="11" startOffset="8" endLine="11" endOffset="79"/>
</Imports>
<Imports type="android.text.TextUtils" name="TextUtils">
<location startLine="13" startOffset="8" endLine="13" endOffset="47"/>
</Imports>
<Targets>
<Target tag="layout/activity_databinding_main_0" view="androidx.constraintlayout.widget.ConstraintLayout">
<Expressions/>
<location startLine="32" startOffset="4" endLine="155" endOffset="55"/>
</Target>
<Target id="@+id/tv_1" tag="binding_1" view="Button">
<Expressions>
<Expression text="@string/nameFormat(user.firstName, user.lastName),default=aaaa" attribute="android:text">
<Location startLine="47" startOffset="12" endLine="47" endOffset="91"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="47" startOffset="28" endLine="47" endOffset="89"/>
</Expression>
</Expressions>
<location startLine="42" startOffset="8" endLine="50" endOffset="55"/>
</Target>
<!--include标签布局-->
<Target id="@+id/tv_age" tag="layout/activity_databinding_main_0" include="main_include">
<Expressions>
<Expression text="studentMap" attribute="app:studentMap">
<Location startLine="57" startOffset="12" endLine="57" endOffset="41"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="57" startOffset="30" endLine="57" endOffset="39"/>
</Expression>
</Expressions>
<location startLine="52" startOffset="8" endLine="59" endOffset="60"/>
</Target>
<Target id="@+id/tv_array_last" tag="binding_2" view="TextView">
<Expressions>
<Expression text="studentArray[Fields.LAST_NAME],default=yyy" attribute="android:text">
<Location startLine="74" startOffset="12" endLine="74" endOffset="71"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="74" startOffset="28" endLine="74" endOffset="69"/>
</Expression>
</Expressions>
<location startLine="70" startOffset="8" endLine="77" endOffset="63"/>
</Target>
<Target id="@+id/tv_array_age" tag="binding_3" view="TextView">
<Expressions>
<Expression text="String.valueOf(1 + (Integer)studentArray[Fields.AGE]),default=111" attribute="android:text">
<Location startLine="83" startOffset="12" endLine="83" endOffset="94"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="83" startOffset="28" endLine="83" endOffset="92"/>
</Expression>
</Expressions>
<location startLine="79" startOffset="8" endLine="86" endOffset="69"/>
</Target>
<Target id="@+id/tv_observable_last" tag="binding_4" view="TextView">
<Expressions>
<Expression text="teacher.firstName,default=yyy" attribute="android:text">
<Location startLine="101" startOffset="12" endLine="101" endOffset="58"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="101" startOffset="28" endLine="101" endOffset="56"/>
</Expression>
</Expressions>
<location startLine="97" startOffset="8" endLine="104" endOffset="63"/>
</Target>
<Target id="@+id/tv_observable_age" tag="binding_5" view="TextView">
<Expressions>
<Expression text="teacher.lastName,default=111" attribute="android:text">
<Location startLine="113" startOffset="12" endLine="113" endOffset="57"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="113" startOffset="28" endLine="113" endOffset="55"/>
</Expression>
</Expressions>
<location startLine="106" startOffset="8" endLine="116" endOffset="74"/>
</Target>
<Target id="@+id/et" tag="binding_6" view="EditText">
<Expressions>
<Expression text="teacher.lastName" attribute="android:text">
<Location startLine="132" startOffset="12" endLine="132" endOffset="45"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="132" startOffset="28" endLine="132" endOffset="43"/>
</Expression>
</Expressions>
<location startLine="127" startOffset="8" endLine="133" endOffset="58"/>
</Target>
<Target id="@+id/bt_confirm" tag="binding_7" view="Button">
<Expressions>
<Expression text="teacher.lastName" attribute="android:text">
<Location startLine="139" startOffset="12" endLine="139" endOffset="45"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="139" startOffset="28" endLine="139" endOffset="43"/>
</Expression>
</Expressions>
<location startLine="135" startOffset="8" endLine="140" endOffset="58"/>
</Target>
<Target id="@+id/iv" tag="binding_8" view="com.study.jetpackstudykotlin.databinding.view.DefineImageView">
<Expressions>
<Expression text="teacher.lastName" attribute="imgContent">
<Location startLine="144" startOffset="12" endLine="144" endOffset="43"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="144" startOffset="26" endLine="144" endOffset="41"/>
</Expression>
<Expression text="TextUtils.isEmpty(teacher.lastName)?@color/color_enable:@color/color_unable" attribute="android:background">
<Location startLine="147" startOffset="12" endLine="147" endOffset="110"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="147" startOffset="34" endLine="147" endOffset="108"/>
</Expression>
<Expression text="teacher.lastName" attribute="app:showShadow">
<Location startLine="153" startOffset="12" endLine="153" endOffset="47"/>
<TwoWay>false</TwoWay>
<ValueLocation startLine="153" startOffset="30" endLine="153" endOffset="45"/>
</Expression>
</Expressions>
<location startLine="142" startOffset="8" endLine="153" endOffset="50"/>
</Target>
<Target id="@+id/tv_tip2" view="TextView">
<Expressions/>
<location startLine="61" startOffset="8" endLine="68" endOffset="62"/>
</Target>
<Target id="@+id/tv_tip3" view="TextView">
<Expressions/>
<location startLine="88" startOffset="8" endLine="95" endOffset="68"/>
</Target>
<Target id="@+id/bt" view="Button">
<Expressions/>
<location startLine="118" startOffset="8" endLine="125" endOffset="73"/>
</Target>
</Targets>
</Layout>
2.1.2 标准activity_databinding_main.xml文件
文件路径:D:\Android\Space\Mine\JetpackStudyKotlin\app\build\intermediates\incremental\mergeDebugResources\stripped.dir\layout\activity_databinding_main.xml<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" android:tag="layout/activity_databinding_main_0">
<!--
default用于预览展示
nameFormat进行字符串格式化: <string name="nameFormat">Full Name: %1$s:%2$s</string>
-->
<Button
android:id="@+id/tv_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="aaaa"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" android:tag="binding_1"/>
<!--使用include布局,要想layout的属性生效,必须重写android:layout_width和android:layout_height-->
<include
android:id="@+id/tv_age"
layout="@layout/main_include"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_1" />
<TextView
android:id="@+id/tv_tip2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ObservableArray:"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_age" />
<TextView
android:id="@+id/tv_array_last"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yyy"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_tip2" android:tag="binding_2"/>
<TextView
android:id="@+id/tv_array_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="111"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_array_last" android:tag="binding_3"/>
<TextView
android:id="@+id/tv_tip3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Observable:"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_array_age" />
<TextView
android:id="@+id/tv_observable_last"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yyy"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_tip3" android:tag="binding_4"/>
<TextView
android:id="@+id/tv_observable_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:shadowColor="@color/colorAccent"
android:shadowDy="10"
android:shadowRadius="10"
android:text="111"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_observable_last" android:tag="binding_5"/>
<Button
android:id="@+id/bt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:onClick="jumpActivity"
android:text="RecyclerView"
app:layout_constraintTop_toBottomOf="@id/tv_observable_age" />
<EditText
android:id="@+id/et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入"
android:tag="binding_6"
app:layout_constraintTop_toBottomOf="@id/bt" />
<Button
android:id="@+id/bt_confirm"
android:layout_width="match_parent"
android:layout_height="45dp"
android:tag="binding_7"
app:layout_constraintTop_toBottomOf="@id/et" />
<com.study.jetpackstudykotlin.databinding.view.DefineImageView
android:id="@+id/iv"
android:tag="binding_8"
android:layout_width="match_parent"
android:layout_height="45dp"
android:elevation="2dp"
app:layout_constraintBottom_toBottomOf="@id/bt_confirm"
app:layout_constraintLeft_toLeftOf="@id/bt_confirm"
app:layout_constraintRight_toRightOf="@id/bt_confirm"
app:layout_constraintTop_toTopOf="@id/bt_confirm"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
2.2 DataBindingUtil源码
DataBindingUtil加载时,下列对象将会被加载至内存中: private static DataBinderMapper sMapper = new DataBinderMapperImpl();
此处DataBinderMapperImpl是由编译生成,DataBinderMapperImpl源码路径如下:
D:\Android\Space\Mine\JetpackStudyKotlin\app\build\generated\ap_generated_sources\debug\out\androidx\databinding\DataBinderMapperImpl.java
源码如下所示:
package androidx.databinding;
public class DataBinderMapperImpl extends MergedDataBinderMapper {
DataBinderMapperImpl() {
//添加用户自己报名下面DataBinderMapperImpl类,该类的路径D:\Android\Space\Mine\JetpackStudyKotlin\app\
//build\generated\ap_generated_sources\debug\out\com\study\jetpackstudykotlin\DataBinderMapperImpl.java
addMapper(new com.study.jetpackstudykotlin.DataBinderMapperImpl());
}
}
D:\Android\Space\Mine\JetpackStudyKotlin\app\build\generated\ap_generated_sources\debug\out\com\study\jetpackstudykotlin\DataBinderMapperImpl.java的源码如下所示:
package com.study.jetpackstudykotlin;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.View;
import androidx.databinding.DataBinderMapper;
import androidx.databinding.DataBindingComponent;
import androidx.databinding.ViewDataBinding;
import com.study.jetpackstudykotlin.databinding.ActivityDatabindingMainBindingImpl;
import com.study.jetpackstudykotlin.databinding.ActivityRecyclerViewBindingImpl;
import com.study.jetpackstudykotlin.databinding.ItemRecyclerBindingImpl;
import com.study.jetpackstudykotlin.databinding.MainIncludeBindingImpl;
import java.lang.IllegalArgumentException;
import java.lang.Integer;
import java.lang.Object;
import java.lang.Override;
import java.lang.RuntimeException;
import java.lang.String;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class DataBinderMapperImpl extends DataBinderMapper {
private static final int LAYOUT_ACTIVITYDATABINDINGMAIN = 1;
private static final int LAYOUT_ACTIVITYRECYCLERVIEW = 2;
private static final int LAYOUT_ITEMRECYCLER = 3;
private static final int LAYOUT_MAININCLUDE = 4;
private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(4);
static {
INTERNAL_LAYOUT_ID_LOOKUP.put(com.study.jetpackstudykotlin.R.layout.activity_databinding_main, LAYOUT_ACTIVITYDATABINDINGMAIN);
INTERNAL_LAYOUT_ID_LOOKUP.put(com.study.jetpackstudykotlin.R.layout.activity_recycler_view, LAYOUT_ACTIVITYRECYCLERVIEW);
INTERNAL_LAYOUT_ID_LOOKUP.put(com.study.jetpackstudykotlin.R.layout.item_recycler, LAYOUT_ITEMRECYCLER);
INTERNAL_LAYOUT_ID_LOOKUP.put(com.study.jetpackstudykotlin.R.layout.main_include, LAYOUT_MAININCLUDE);
}
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if (localizedLayoutId > 0) {
final Object tag = view.getTag();
if (tag == null) {
throw new RuntimeException("view must have a tag");
}
//根据不同标记返回不同的Binding实现类
switch (localizedLayoutId) {
case LAYOUT_ACTIVITYDATABINDINGMAIN: {
if ("layout/activity_databinding_main_0".equals(tag)) {
return new ActivityDatabindingMainBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_databinding_main is invalid. Received: " + tag);
}
case LAYOUT_ACTIVITYRECYCLERVIEW: {
if ("layout/activity_recycler_view_0".equals(tag)) {
return new ActivityRecyclerViewBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_recycler_view is invalid. Received: " + tag);
}
case LAYOUT_ITEMRECYCLER: {
if ("layout/item_recycler_0".equals(tag)) {
return new ItemRecyclerBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for item_recycler is invalid. Received: " + tag);
}
case LAYOUT_MAININCLUDE: {
if ("layout/main_include_0".equals(tag)) {
return new MainIncludeBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for main_include is invalid. Received: " + tag);
}
}
}
return null;
}
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View[] views, int layoutId) {
if (views == null || views.length == 0) {
return null;
}
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if (localizedLayoutId > 0) {
final Object tag = views[0].getTag();
if (tag == null) {
throw new RuntimeException("view must have a tag");
}
switch (localizedLayoutId) {
}
}
return null;
}
@Override
public int getLayoutId(String tag) {
if (tag == null) {
return 0;
}
Integer tmpVal = InnerLayoutIdLookup.sKeys.get(tag);
return tmpVal == null ? 0 : tmpVal;
}
@Override
public String convertBrIdToString(int localId) {
String tmpVal = InnerBrLookup.sKeys.get(localId);
return tmpVal;
}
@Override
public List<DataBinderMapper> collectDependencies() {
ArrayList<DataBinderMapper> result = new ArrayList<DataBinderMapper>(1);
result.add(new androidx.databinding.library.baseAdapters.DataBinderMapperImpl());
return result;
}
private static class InnerBrLookup {
static final SparseArray<String> sKeys = new SparseArray<String>(9);
static {
sKeys.put(0, "_all");
sKeys.put(1, "firstName");
sKeys.put(2, "lastName");
sKeys.put(3, "teacher");
sKeys.put(4, "studentArray");
sKeys.put(5, "studentMap");
sKeys.put(6, "refresh");
sKeys.put(7, "user");
}
}
private static class InnerLayoutIdLookup {
static final HashMap<String, Integer> sKeys = new HashMap<String, Integer>(4);
static {
sKeys.put("layout/activity_databinding_main_0", com.study.jetpackstudykotlin.R.layout.activity_databinding_main);
sKeys.put("layout/activity_recycler_view_0", com.study.jetpackstudykotlin.R.layout.activity_recycler_view);
sKeys.put("layout/item_recycler_0", com.study.jetpackstudykotlin.R.layout.item_recycler);
sKeys.put("layout/main_include_0", com.study.jetpackstudykotlin.R.layout.main_include);
}
}
}
上诉中addMapper方法源码如下所示:
//给mMapper添加数据
@SuppressWarnings("WeakerAccess")
public void addMapper(DataBinderMapper mapper) {
Class<? extends DataBinderMapper> mapperClass = mapper.getClass();
if (mExistingMappers.add(mapperClass)) {
mMappers.add(mapper);
final List<DataBinderMapper> dependencies = mapper.collectDependencies();
for(DataBinderMapper dependency : dependencies) {
addMapper(dependency);
}
}
}
DataBindingUtil.setContentView( this, R.layout.activity_databinding_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,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
final int childrenAdded = endChildren - startChildren;
//ActivityDataBindingMain中的childrenAdded=1
if (childrenAdded == 1) {
final View childView = parent.getChildAt(endChildren - 1);
return bind(component, childView, layoutId);
} else {
final View[] children = new View[childrenAdded];
for (int i = 0; i < childrenAdded; i++) {
children[i] = parent.getChildAt(i + startChildren);
}
return bind(component, children, layoutId);
}
}
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
//此处sMapper是D:\Android\Space\Mine\JetpackStudyKotlin\app\build\generated\ap_generated_sources\debug\out\androidx\databinding\DataBinderMapperImpl.java
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
上述sMapper.getDataBinder的源码如下:
@Override
public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
int layoutId) {
//因为mMappers中的元素类型[com.study.jetpackstudykotlin.DataBinderMapperImpl,androidx.databinding.DataBinderMapperImpl]
for(DataBinderMapper mapper : mMappers) {
ViewDataBinding result = mapper.getDataBinder(bindingComponent, view, layoutId);
if (result != null) {
return result;
}
}
if (loadFeatures()) {
return getDataBinder(bindingComponent, view, layoutId);
}
return null;
}
上述mapper.getDataBinder(bindingComponent, view, layoutId)实质调用com.study.jetpackstudykotlin.DataBinderMapperImpl中如下方法:
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if (localizedLayoutId > 0) {
final Object tag = view.getTag();
if (tag == null) {
throw new RuntimeException("view must have a tag");
}
switch (localizedLayoutId) {
case LAYOUT_ACTIVITYDATABINDINGMAIN: {
if ("layout/activity_databinding_main_0".equals(tag)) {
return new ActivityDatabindingMainBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_databinding_main is invalid. Received: " + tag);
}
case LAYOUT_ACTIVITYRECYCLERVIEW: {
if ("layout/activity_recycler_view_0".equals(tag)) {
return new ActivityRecyclerViewBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_recycler_view is invalid. Received: " + tag);
}
case LAYOUT_ITEMRECYCLER: {
if ("layout/item_recycler_0".equals(tag)) {
return new ItemRecyclerBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for item_recycler is invalid. Received: " + tag);
}
case LAYOUT_MAININCLUDE: {
if ("layout/main_include_0".equals(tag)) {
return new MainIncludeBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for main_include is invalid. Received: " + tag);
}
}
}
return null;
}
com.study.jetpackstudykotlin.databinding.ActivityDatabindingMainBindingImpl源码如下:
package com.study.jetpackstudykotlin.databinding;
import com.study.jetpackstudykotlin.R;
import com.study.jetpackstudykotlin.BR;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
@SuppressWarnings("unchecked")
public class ActivityDatabindingMainBindingImpl extends ActivityDatabindingMainBinding {
@Nullable
private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
@Nullable
private static final android.util.SparseIntArray sViewsWithIds;
static {
//这里存储的是布局中使用include标签的布局
sIncludes = new androidx.databinding.ViewDataBinding.IncludedLayouts(13);
sIncludes.setIncludes(0,
new String[] {"main_include"},
new int[] {9},
new int[] {com.study.jetpackstudykotlin.R.layout.main_include});
sViewsWithIds = new android.util.SparseIntArray();
//这里存储的是没有使用DataBinding表达式的控件id
sViewsWithIds.put(R.id.tv_tip2, 10);
sViewsWithIds.put(R.id.tv_tip3, 11);
sViewsWithIds.put(R.id.bt, 12);
}
// views
@NonNull
private final androidx.constraintlayout.widget.ConstraintLayout mboundView0;
public ActivityDatabindingMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
//mapBindings解析布局中的控件
this(bindingComponent, root, mapBindings(bindingComponent, root, 13, sIncludes, sViewsWithIds));
}
private ActivityDatabindingMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 4
, (android.widget.Button) bindings[12]
, (android.widget.Button) bindings[7]
, (android.widget.EditText) bindings[6]
, (com.study.jetpackstudykotlin.databinding.view.DefineImageView) bindings[8]
, (android.widget.Button) bindings[1]
, (com.study.jetpackstudykotlin.databinding.MainIncludeBinding) bindings[9]
, (android.widget.TextView) bindings[3]
, (android.widget.TextView) bindings[2]
, (android.widget.TextView) bindings[5]
, (android.widget.TextView) bindings[4]
, (android.widget.TextView) bindings[10]
, (android.widget.TextView) bindings[11]
);
this.btConfirm.setTag(null);
this.et.setTag(null);
this.iv.setTag(null);
this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
this.mboundView0.setTag(null);
this.tv1.setTag(null);
this.tvArrayAge.setTag(null);
this.tvArrayLast.setTag(null);
this.tvObservableAge.setTag(null);
this.tvObservableLast.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
@Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x80L;
}
tvAge.invalidateAll();
//调用父类方法,发送回调事件
requestRebind();
}
@Override
public boolean hasPendingBindings() {
synchronized(this) {
if (mDirtyFlags != 0) {
return true;
}
}
if (tvAge.hasPendingBindings()) {
return true;
}
return false;
}
@Override
public boolean setVariable(int variableId, @Nullable Object variable) {
boolean variableSet = true;
if (BR.studentMap == variableId) {
setStudentMap((androidx.databinding.ObservableMap<java.lang.String,java.lang.Object>) variable);
}
else if (BR.teacher == variableId) {
setTeacher((com.study.jetpackstudykotlin.databinding.bean.Teacher2) variable);
}
else if (BR.user == variableId) {
setUser((com.study.jetpackstudykotlin.databinding.bean.User) variable);
}
else if (BR.studentArray == variableId) {
setStudentArray((androidx.databinding.ObservableArrayList<java.lang.Object>) variable);
}
else {
variableSet = false;
}
return variableSet;
}
public void setStudentMap(@Nullable androidx.databinding.ObservableMap<java.lang.String,java.lang.Object> StudentMap) {
updateRegistration(0, StudentMap);
this.mStudentMap = StudentMap;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
//通知属性改变
notifyPropertyChanged(BR.studentMap);
super.requestRebind();
}
public void setTeacher(@Nullable com.study.jetpackstudykotlin.databinding.bean.Teacher2 Teacher) {
updateRegistration(1, Teacher);
this.mTeacher = Teacher;
synchronized(this) {
mDirtyFlags |= 0x2L;
}
//通知属性改变
notifyPropertyChanged(BR.teacher);
super.requestRebind();
}
public void setUser(@Nullable com.study.jetpackstudykotlin.databinding.bean.User User) {
//User为什么没有调用updateRegistration方法呢?因为User类是不被观察的
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x10L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
public void setStudentArray(@Nullable androidx.databinding.ObservableArrayList<java.lang.Object> StudentArray) {
updateRegistration(2, StudentArray);
this.mStudentArray = StudentArray;
synchronized(this) {
mDirtyFlags |= 0x4L;
}
//通知属性改变
notifyPropertyChanged(BR.studentArray);
super.requestRebind();
}
@Override
public void setLifecycleOwner(@Nullable androidx.lifecycle.LifecycleOwner lifecycleOwner) {
super.setLifecycleOwner(lifecycleOwner);
tvAge.setLifecycleOwner(lifecycleOwner);
}
//监听字段变化
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
case 0 :
return onChangeStudentMap((androidx.databinding.ObservableMap<java.lang.String,java.lang.Object>) object, fieldId);
case 1 :
return onChangeTeacher((com.study.jetpackstudykotlin.databinding.bean.Teacher2) object, fieldId);
case 2 :
return onChangeStudentArray((androidx.databinding.ObservableArrayList<java.lang.Object>) object, fieldId);
case 3 :
return onChangeTvAge((com.study.jetpackstudykotlin.databinding.MainIncludeBinding) object, fieldId);
}
return false;
}
private boolean onChangeStudentMap(androidx.databinding.ObservableMap<java.lang.String,java.lang.Object> StudentMap, int fieldId) {
if (fieldId == BR._all) {
synchronized(this) {
mDirtyFlags |= 0x1L;
}
return true;
}
return false;
}
private boolean onChangeTeacher(com.study.jetpackstudykotlin.databinding.bean.Teacher2 Teacher, int fieldId) {
if (fieldId == BR._all) {
synchronized(this) {
mDirtyFlags |= 0x2L;
}
return true;
}
else if (fieldId == BR.firstName) {
synchronized(this) {
mDirtyFlags |= 0x20L;
}
return true;
}
else if (fieldId == BR.lastName) {
synchronized(this) {
mDirtyFlags |= 0x40L;
}
return true;
}
return false;
}
private boolean onChangeStudentArray(androidx.databinding.ObservableArrayList<java.lang.Object> StudentArray, int fieldId) {
if (fieldId == BR._all) {
synchronized(this) {
mDirtyFlags |= 0x4L;
}
return true;
}
return false;
}
private boolean onChangeTvAge(com.study.jetpackstudykotlin.databinding.MainIncludeBinding TvAge, int fieldId) {
if (fieldId == BR._all) {
synchronized(this) {
mDirtyFlags |= 0x8L;
}
return true;
}
return false;
}
//最终对控件的赋值实在此处完成的
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
int androidxDatabindingViewDataBindingSafeUnboxIntegerStudentArrayFieldsAGE = 0;
java.lang.Object studentArrayFieldsLASTNAME = null;
java.lang.String teacherFirstName = null;
androidx.databinding.ObservableMap<java.lang.String,java.lang.Object> studentMap = mStudentMap;
java.lang.Object studentArrayFieldsAGE = null;
java.lang.String stringValueOfInt1IntegerStudentArrayFieldsAGE = null;
com.study.jetpackstudykotlin.databinding.bean.Teacher2 teacher = mTeacher;
java.lang.String userFirstName = null;
com.study.jetpackstudykotlin.databinding.bean.User user = mUser;
java.lang.String userLastName = null;
java.lang.Integer integerStudentArrayFieldsAGE = null;
java.lang.String tv1AndroidStringNameFormatUserFirstNameUserLastName = null;
int textUtilsIsEmptyTeacherLastNameIvAndroidColorColorEnableIvAndroidColorColorUnable = 0;
androidx.databinding.ObservableArrayList<java.lang.Object> studentArray = mStudentArray;
java.lang.String teacherLastName = null;
int int1IntegerStudentArrayFieldsAGE = 0;
boolean textUtilsIsEmptyTeacherLastName = false;
if ((dirtyFlags & 0x81L) != 0) {
}
if ((dirtyFlags & 0xe2L) != 0) {
if ((dirtyFlags & 0xa2L) != 0) {
if (teacher != null) {
// read teacher.firstName
teacherFirstName = teacher.getFirstName();
}
}
if ((dirtyFlags & 0xc2L) != 0) {
if (teacher != null) {
// read teacher.lastName
teacherLastName = teacher.getLastName();
}
// read TextUtils.isEmpty(teacher.lastName)
textUtilsIsEmptyTeacherLastName = android.text.TextUtils.isEmpty(teacherLastName);
if((dirtyFlags & 0xc2L) != 0) {
if(textUtilsIsEmptyTeacherLastName) {
dirtyFlags |= 0x200L;
}
else {
dirtyFlags |= 0x100L;
}
}
// read TextUtils.isEmpty(teacher.lastName) ? @android:color/color_enable : @android:color/color_unable
textUtilsIsEmptyTeacherLastNameIvAndroidColorColorEnableIvAndroidColorColorUnable = ((textUtilsIsEmptyTeacherLastName) ? (getColorFromResource(iv, R.color.color_enable)) : (getColorFromResource(iv, R.color.color_unable)));
}
}
if ((dirtyFlags & 0x90L) != 0) {
if (user != null) {
// read user.firstName
userFirstName = user.getFirstName();
// read user.lastName
userLastName = user.getLastName();
}
// read @android:string/nameFormat
tv1AndroidStringNameFormatUserFirstNameUserLastName = tv1.getResources().getString(R.string.nameFormat, userFirstName, userLastName);
}
if ((dirtyFlags & 0x84L) != 0) {
if (studentArray != null) {
// read studentArray[Fields.LAST_NAME]
studentArrayFieldsLASTNAME = getFromList(studentArray, com.study.jetpackstudykotlin.databinding.bean.Fields.LAST_NAME);
// read studentArray[Fields.AGE]
studentArrayFieldsAGE = getFromList(studentArray, com.study.jetpackstudykotlin.databinding.bean.Fields.AGE);
}
if (studentArrayFieldsAGE != null) {
// read (Integer) studentArray[Fields.AGE]
integerStudentArrayFieldsAGE = ((java.lang.Integer) (studentArrayFieldsAGE));
}
// read androidx.databinding.ViewDataBinding.safeUnbox((Integer) studentArray[Fields.AGE])
androidxDatabindingViewDataBindingSafeUnboxIntegerStudentArrayFieldsAGE = androidx.databinding.ViewDataBinding.safeUnbox(integerStudentArrayFieldsAGE);
// read (1) + (androidx.databinding.ViewDataBinding.safeUnbox((Integer) studentArray[Fields.AGE]))
int1IntegerStudentArrayFieldsAGE = (1) + (androidxDatabindingViewDataBindingSafeUnboxIntegerStudentArrayFieldsAGE);
// read String.valueOf((1) + (androidx.databinding.ViewDataBinding.safeUnbox((Integer) studentArray[Fields.AGE])))
stringValueOfInt1IntegerStudentArrayFieldsAGE = java.lang.String.valueOf(int1IntegerStudentArrayFieldsAGE);
}
// batch finished
if ((dirtyFlags & 0xc2L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.btConfirm, teacherLastName);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.et, teacherLastName);
//@BindingAdapter(loadImage)在此处使用
com.study.jetpackstudykotlin.databinding.viewAdapter.ViewAdapter.loadImage(this.iv, teacherLastName);
//@BindingConversion在此处使用
androidx.databinding.adapters.ViewBindingAdapter.setBackground(this.iv, com.study.jetpackstudykotlin.databinding.viewAdapter.ViewAdapter.convertColorToDrawable(textUtilsIsEmptyTeacherLastNameIvAndroidColorColorEnableIvAndroidColorColorUnable));
//@BindingMethod在此使用
this.iv.showShadow(teacherLastName);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvObservableAge, teacherLastName);
}
if ((dirtyFlags & 0x90L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv1, tv1AndroidStringNameFormatUserFirstNameUserLastName);
}
if ((dirtyFlags & 0x81L) != 0) {
// api target 1
this.tvAge.setStudentMap(studentMap);
}
if ((dirtyFlags & 0x84L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvArrayAge, stringValueOfInt1IntegerStudentArrayFieldsAGE);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvArrayLast, (java.lang.CharSequence) studentArrayFieldsLASTNAME);
}
if ((dirtyFlags & 0xa2L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvObservableLast, teacherFirstName);
}
executeBindingsOn(tvAge);
}
// 数据标记值
private long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
flag 0 (0x1L): studentMap
flag 1 (0x2L): teacher
flag 2 (0x3L): studentArray
flag 3 (0x4L): tvAge
flag 4 (0x5L): user
flag 5 (0x6L): teacher.firstName
flag 6 (0x7L): teacher.lastName
flag 7 (0x8L): null
flag 8 (0x9L): TextUtils.isEmpty(teacher.lastName) ? @android:color/color_enable : @android:color/color_unable
flag 9 (0xaL): TextUtils.isEmpty(teacher.lastName) ? @android:color/color_enable : @android:color/color_unable
flag mapping end*/
//end
}
三、总结
这里只介绍了关于DataBinding是如何实现数据绑定,以及被绑定的View是如何被赋值的。关于@InverseBindingAdapter以及如何实现双向绑定将在下一篇文章中介绍。![](https://i-blog.csdnimg.cn/blog_migrate/78a75fdcd083b4ffd0709b7f23c769cf.png#pic_center)