(译)深入理解Data Binding原理

作者: 于卫国 | 来源:发表于2017-07-23 15:38 被阅读138次

本文首发:http://yuweiguocn.github.io/

本文介绍了Data Binding的原理。

关于Data Binding的使用请查看Data Binding

原文链接:Understanding Data-Binding's generated code and How does Android Data-Binding compiler work

这篇文章并不是介绍怎样使用Data Binding或了解基本概念。建议你直接查看Google文档,会帮助你轻松集成,有大量的示例代码,当你决定应用它到你的工程你可以实现很多很酷的东西。体验之后,你可能会好奇它是如何实现的,Google是怎样让传统的xml文件和data binding混合的,xml文件是怎样和java代码交互的,编译器是怎样处理的。本文主题旨在揭露data binding的机制,深入底层看看到底发生了什么。因此,开发者可以深入理解帮助他们正确地使用data binding,利用data binding的强大构建完美的应用。

近来,data binding是android很火的趋势,让开发者敲代码时更轻松。由于它的强大大量的开发者已经开始使用data binding。但说实话,它也带给我们一些麻烦。虽然data binding的概念很简单:“Yay!定义一个ViewModel类在 layout.xml 文件中引入并且不需要关注任何UI的东西,我们的数据会直接绑定到UI上用一种神奇的方式”。真的很快,很简单。当出现一些错误,当你的数据突然不能绑定到UI上,当编译器报出大量的错误信息,你真的不想知道这是什么意思么。我们能做些什么!

我不得不去面对这些data binding的问题并且使用不同的办法去解决它。并且我认为,只有通过查看data binding的源码,理解它工作的原理,我就不用再去处理这些问题了。让我们从Google clone下这个仓库一起阅读下它的代码Data-Binding Repository

Part 1:Data Binding 流,Obsevable模式机制和data-binding生成代码的意思

为了能更好的理解代码,我创建了一个简单的使用data-binding的示例。

1.创建一个布局文件R.layout.activity_main

xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="com.example.main.MainViewModel" />

    </data>

    <TextView android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@{ viewModel.text }" />

</layout>

2.创建MainActivity

java
public class MainActivity extends AppCompatActivity {

    @Inject
    MainViewModel mViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding bind = DataBindingUtil.setContentView(this, R.layout.activity_main);
        bind.setViewModel(mViewModel);
    }
}

3.创建MainViewModel

java
public class MainViewModel extends BaseObservable  {

    public final ObservableField<String> text = new ObservableField<>();

    public MainViewModel() {
    }

}

编译工程之后,我们可以看到data-binding生成的新的文件:
1.activity_main-layout.xml(在data-binding-info文件夹中)

xml
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout layout="activity_main" absoluteFilePath="/home/framgia/Projects/harpa-crista/harpacrista/android/app/src/main/res/layout/activity_main.xml"
    directory="layout"
    isMerge="false" modulePackage="com.harpacrista">
    <Variables declared="true" name="viewModel" type="com.example.main.MainViewModel">
        <location endLine="8" endOffset="51" startLine="6" startOffset="8" />
    </Variables>
    <Imports name="View" type="android.view.View">
        <location endLine="10" endOffset="42" startLine="10" startOffset="8" />
    </Imports>
    <Targets>
        <Target tag="layout/activity_main_0" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text=" viewModel.text ">
                    <Location endLine="16" endOffset="41" startLine="16" startOffset="8" />
                    <TwoWay>false</TwoWay>
                    <ValueLocation endLine="16" endOffset="39" startLine="16" startOffset="24" />
                </Expression>
            </Expressions>
            <location endLine="16" endOffset="44" startLine="14" startOffset="4" />
        </Target>
    </Targets>
</Layout>

2.activity_main.xml(正常的布局文件)的简洁版本(在data-binding-layout-out文件夹中)

xml
<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="layout/activity_main_0" />

3.ActivityMainBinding.java文件,这是我们的主角,最神奇的地方。

java
public class ActivityMainBinding extends android.databinding.ViewDataBinding  {

    private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
    private static final android.util.SparseIntArray sViewsWithIds;
    static {
        sIncludes = null;
        sViewsWithIds = null;
    }
    // views
    private final android.widget.TextView mboundView0;
    // variables
    private com.example.main.MainViewModel mViewModel;
    // values
    // listeners
    // Inverse Binding Event Handlers

    public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
        super(bindingComponent, root, 2);
        final Object[] bindings = mapBindings(bindingComponent, root, 1, sIncludes, sViewsWithIds);
        this.mboundView0 = (android.widget.TextView) bindings[0];
        this.mboundView0.setTag(null);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

    @Override
    public void invalidateAll() {
        synchronized(this) {
                mDirtyFlags = 0x4L;
        }
        requestRebind();
    }

    @Override
    public boolean hasPendingBindings() {
        synchronized(this) {
            if (mDirtyFlags != 0) {
                return true;
            }
        }
        return false;
    }

    public boolean setVariable(int variableId, Object variable) {
        switch(variableId) {
            case BR.viewModel :
                setViewModel((com.example.main.MainViewModel) variable);
                return true;
        }
        return false;
    }

    public void setViewModel(com.example.main.MainViewModel viewModel) {
        updateRegistration(0, viewModel);
        this.mViewModel = viewModel;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.viewModel);
        super.requestRebind();
    }
    public com.example.main.MainViewModel getViewModel() {
        return mViewModel;
    }

    @Override
    protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
        switch (localFieldId) {
            case 0 :
                return onChangeViewModel((com.example.main.MainViewModel) object, fieldId);
            case 1 :
                return onChangeTextViewMode((android.databinding.ObservableField<java.lang.String>) object, fieldId);
        }
        return false;
    }
    private boolean onChangeViewModel(com.example.main.MainViewModel viewModel, int fieldId) {
        switch (fieldId) {
            case BR._all: {
                synchronized(this) {
                        mDirtyFlags |= 0x1L;
                }
                return true;
            }
        }
        return false;
    }
    private boolean onChangeTextViewMode(android.databinding.ObservableField<java.lang.String> textViewModel, int fieldId) {
        switch (fieldId) {
            case BR._all: {
                synchronized(this) {
                        mDirtyFlags |= 0x2L;
                }
                return true;
            }
        }
        return false;
    }

    @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        com.example.main.MainViewModel viewModel = mViewModel;
        java.lang.String textViewModel = null;
        android.databinding.ObservableField<java.lang.String> textViewModel1 = null;

        if ((dirtyFlags & 0x7L) != 0) {



                if (viewModel != null) {
                    // read viewModel.text
                    textViewModel1 = viewModel.text;
                }
                updateRegistration(1, textViewModel1);


                if (textViewModel1 != null) {
                    // read viewModel.text.get()
                    textViewModel = textViewModel1.get();
                }
        }
        // batch finished
        if ((dirtyFlags & 0x7L) != 0) {
            // api target 1

            android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView0, textViewModel);
        }
    }
    // Listener Stub Implementations
    // callback impls
    // dirty flag
    private  long mDirtyFlags = 0xffffffffffffffffL;

    public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot) {
        return inflate(inflater, root, attachToRoot, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot, android.databinding.DataBindingComponent bindingComponent) {
        return android.databinding.DataBindingUtil.<ActivityMainBinding>inflate(inflater, com.harpacrista.R.layout.activity_main, root, attachToRoot, bindingComponent);
    }
    public static ActivityMainBinding inflate(android.view.LayoutInflater inflater) {
        return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.databinding.DataBindingComponent bindingComponent) {
        return bind(inflater.inflate(com.harpacrista.R.layout.activity_main, null, false), bindingComponent);
    }
    public static ActivityMainBinding bind(android.view.View view) {
        return bind(view, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityMainBinding bind(android.view.View view, android.databinding.DataBindingComponent bindingComponent) {
        if (!"layout/activity_main_0".equals(view.getTag())) {
            throw new RuntimeException("view tag isn't correct on view:" + view.getTag());
        }
        return new ActivityMainBinding(bindingComponent, view);
    }
    /* flag mapping
        flag 0 (0x1L): viewModel
        flag 1 (0x2L): viewModel.text
        flag 2 (0x3L): null
    flag mapping end*/
    //end
}

正如我们所看到的,data-binding从activity_main.xml文件帮我们生成了3个额外的文件:activity_main-layout.xmlactivity_main.xml的简洁版本和ActivityMainBinding.java。至此,我们对xml布局生成的代码有了一个初步的了解。

基本上,一个xml布局文件最外层的<layout></layout>标签表示和正常布局文件之间的区别。如果一个正常的布局文件是直接用于android应用,放置在apk包中的res/layout文件夹中,那布局文件中最外层的<layout></layout>标签则是被间接使用。编译器通过搜索应用的layout文件夹编译所有最外层为<layout></layout>标签的布局文件为activity_main.xml(正常的布局文件)的简洁版本。这个版本的xml看起来就像我们没有使用data-binding的正常的布局文件。

因此,基本上,使用data-binding的布局文件就是正常布局文件的一个特殊的版本,data-binding给我们一些词汇和语法,通过这种方式强制它们重写正常的布局文件。Xml文件不能被Android框架理解,它只能被data-binding编译器理解,这让编译器知道什么地方从ViewModel有怎样的数据映射到View上。最后编译器把xml文件转换成可以打包到apk中的正常的布局文件,以及activity_main-layout.xml 和 ActivityMainBinding.java。

我们可以想象原始的布局文件包含两部分:正常部分和绑定部分。正常部分就像我们没有使用data-binding时写的布局文件。绑定部分用于帮助编译器生成java代码,它是一个在UI和数据之间很好的桥梁。

activity_main-layout.xml (在data-binding-info文件夹中)包含绑定部分。就像它的名字,这个文件是布局的绑定信息。我们再来回顾下这个文件。外层仍是<Layout>标签,但和之前的不一样。

xml
<Layout layout="activity_main" absoluteFilePath="/home/framgia/android/app/src/main/res/layout/activity_main.xml"
    directory="layout" isMerge="false" modulePackage="com.harpacrista">
    ...
</Layout>

layout="activity_main"属性表示这个文件是acitivity_main.xml的绑定部分,当前存放在"/home/framgia/android/app/src/main/res/layout/activity_main.xml"。

<Layout>标签的里面,毫无疑问,有<Variables>标签和<Imports>标签。因为我们前面我们用到setViewModel,并且在布局文件中引用了一些类。

xml
<Variables declared="true" name="viewModel" type="com.example.main.MainViewModel">
        <location endLine="8" endOffset="51" startLine="6" startOffset="8" />
</Variables>
<Imports name="View" type="android.view.View">
        <location endLine="10" endOffset="42" startLine="10" startOffset="8" />
</Imports>

它们也包含位置location信息。我不知道为什么编译器需要存储Variable/Import的位置信息,可能用于追溯或其它什么作用。

接下来是这个文件中最重要的部分,<Target>标签告诉ViewModel应该映射到哪个View,绑定类型为单向还是双向,View的位置及View值的位置。

xml
 <Targets>
        <Target tag="layout/activity_main_0" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text=" viewModel.text ">
                    <Location endLine="16" endOffset="41" startLine="16" startOffset="8" />
                    <TwoWay>false</TwoWay>
                    <ValueLocation endLine="16" endOffset="39" startLine="16" startOffset="24" />
                </Expression>
            </Expressions>
            <location endLine="16" endOffset="44" startLine="14" startOffset="4" />
        </Target>
    </Targets>

我们有一个<Target>标签的列表。在布局文件中我们可以有多个View/ViewGroup,但只有包含data-binding表达式的View/ViewGroup才会出现在这里。<Target>标签里面的<Expressions>标签表示View的data-binding表达式。例如:

  • Data-binding表达式android:visibility="@{ viewModel.isVisible ? View.VISIBLE : View.INVISIBLE }"会被编译成
<Expression attribute="android:visibility"
            text=" viewModel.isVisible ? View.VISIBLE : View.INVISIBLE ">
        <Location endLine="29" endOffset="88" startLine="29" startOffset="12" />
        <TwoWay>false</TwoWay>
        <ValueLocation endLine="29" endOffset="86" startLine="29" startOffset="34" />
</Expression>
  • Data-binding表达式android:text="@{ viewModel.text }"会被编译成
<Expression attribute="android:text" text=" viewModel.text ">
             <Location endLine="23" endOffset="45" startLine="23" startOffset="12" />
             <TwoWay>false</TwoWay>
             <ValueLocation endLine="23" endOffset="43" startLine="23" startOffset="28" />
     </Expression>

注意<Expression attribute="android:text" text=" viewModel.text ">。它正是我们要找的View和ViewModel之间的桥梁。ViewModel中的text变量会连接到TextView的android:text属性。这正是我们所期待的。但从这个布局文件到java代码仍是一个谜。接下来看下ActivityMainBinding.java,一探究竟。

ActivityMainBinding.java是ViewDataBinding的一个子类,开发者可以setViewModel到布局文件。从我们分析的过程来看它就像xml的java版本。布局文件中的每个View/ViewGroup标签在这个类是一个变量。例如:

xml
<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@{ viewModel.text }"
            />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{ viewModel.isVisible ? View.VISIBLE : View.INVISIBLE }"
            />
    </LinearLayout>

将会生成:

java
public class ActivityMainBinding extends android.databinding.ViewDataBinding  {

    private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
    private static final android.util.SparseIntArray sViewsWithIds;
    static {
        sIncludes = null;
        sViewsWithIds = null;
    }
    // views
    private final android.widget.LinearLayout mboundView0;
    private final android.widget.TextView mboundView1;
    private final android.widget.Button mboundView2;
    ...
}

并且如果你在布局文件中为View指定一个ID,这个private field将变成 public field,并且你可以直接使用它不用调用findViewById()。

java
// views
    private final android.widget.LinearLayout mboundView0;
    private final android.widget.Button mboundView2;
    public final android.widget.TextView tvTest;

这用起来很方便!访问布局文件中的任意View比之前更为容易。变量的名称和android:id的名称一致。如果android:id 带有下划线,名称将转换为驼峰式,例如android:id="@+id/tv_test"会转换为public final android.widget.TextView tvTest;

<variable>标签定义了java类中的变量,我们通过setViewModel方法设置的变量。顺便说一句,当我们讨论ViewModel时我想提到Obsevable模式。Obsevable模式是一个很关键的东西,无论ViewModel何时被修改都可以让View得到更新,并且ViewModel会自动接收一个新数据当用户和View交互时。这是所有的Obsevable类型覆盖了所有java数据类型。

ObservableArrayList
ObservableArrayMap
ObservableBoolean
ObservableByte
ObservableChar
ObservableDouble
ObservableField
ObservableFloat
ObservableInt
ObservableLong
ObservableParcelable
ObservableShort

这些类的集合很相似也很简单。全部继承自BaseObservable,只包含一个变量,一个getter和一个setter。setter会检查新值和老值是否不同,如果是新值会调用notifyChange(),并且View会得到更新。但notifyChange()是怎样影响到View的?

为了回答上面的问题,我会使用这个例子尽可能简单地解释它:

  • 期望: 我们修改ViewModel中的isShowView为false,actiivty_main.xml中的View将不可见。
  • 里面发生了什么: 当我们setViewModel到ActivityMainBinding,updateRegistration被调用用于在ViewModel 和View之间创建一个“桥”。

我们来看下“桥”到底长什么样子:

java
    /**
     * Method object extracted out to attach a listener to a bound Observable object.
     */
    private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
        @Override
        public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
            return new WeakPropertyListener(viewDataBinding, localFieldId).getListener();
        }
    };

    ....

     private static class WeakPropertyListener extends Observable.OnPropertyChangedCallback
            implements ObservableReference<Observable> {
        final WeakListener<Observable> mListener;

        public WeakPropertyListener(ViewDataBinding binder, int localFieldId) {
            mListener = new WeakListener<Observable>(binder, localFieldId, this);
        }

        @Override
        public WeakListener<Observable> getListener() {
            return mListener;
        }

        @Override
        public void addListener(Observable target) {
            target.addOnPropertyChangedCallback(this);
        }

        @Override
        public void removeListener(Observable target) {
            target.removeOnPropertyChangedCallback(this);
        }

        @Override
        public void onPropertyChanged(Observable sender, int propertyId) {
            ViewDataBinding binder = mListener.getBinder();
            if (binder == null) {
                return;
            }
            Observable obj = mListener.getTarget();
            if (obj != sender) {
                return; // notification from the wrong object?
            }
            binder.handleFieldChange(mListener.mLocalFieldId, sender, propertyId);
        }
    }

    ...

     private static class WeakListener<T> extends WeakReference<ViewDataBinding> {
        private final ObservableReference<T> mObservable;
        protected final int mLocalFieldId;
        private T mTarget;

        public WeakListener(ViewDataBinding binder, int localFieldId,
                ObservableReference<T> observable) {
            super(binder);
            mLocalFieldId = localFieldId;
            mObservable = observable;
        }

        public void setTarget(T object) {
            unregister();
            mTarget = object;
            if (mTarget != null) {
                mObservable.addListener(mTarget);
            }
        }
    }
  • WeakPropertyListener中的mListener变量持有MainDataBinding的一个弱引用,WeakListener中的setTarget方法被调用绑定ViewModelMainDataBinding
  • ViewModel修改isShowView变量会调用mNotifier的notifyCallback():
java
private void notifyCallbacks(T sender, int arg, A arg2, int startIndex, int endIndex, long bits) {
        long bitMask = 1L;

        for(int i = startIndex; i < endIndex; ++i) {
            if((bits & bitMask) == 0L) {
                this.mNotifier.onNotifyCallback(this.mCallbacks.get(i), sender, arg, arg2);
            }

            bitMask <<= 1;
        }

    }

  • 并且mNotifier也是WeakPropertyListener。因此在ViewModel上修改isShowView将会通知WeakPropertyListener,这是isShowView影响View让它不可见的地方。ActivityMainBinding中的executeBindings()拿到值映射到UI上相应的View。
java
@Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        com.example.main.MainViewModel viewModel = mViewModel;
        int isVisibleViewModelVI = 0;
        boolean isVisibleViewModel = false;
        java.lang.String textViewModel = null;
        android.databinding.ObservableBoolean isVisibleViewModel1 = null;
        android.databinding.ObservableField<java.lang.String> textViewModel1 = null;

        if ((dirtyFlags & 0xfL) != 0) {


            if ((dirtyFlags & 0xbL) != 0) {

                    if (viewModel != null) {
                        // read viewModel.isVisible
                        isVisibleViewModel1 = viewModel.isVisible;
                    }
                    updateRegistration(1, isVisibleViewModel1);


                    if (isVisibleViewModel1 != null) {
                        // read viewModel.isVisible.get()
                        isVisibleViewModel = isVisibleViewModel1.get();
                    }
                    if((dirtyFlags & 0xbL) != 0) {
                        if (isVisibleViewModel) {
                            dirtyFlags |= 0x20L;
                        } else {
                            dirtyFlags |= 0x10L;
                        }}


                    // read viewModel.isVisible.get() ? View.VISIBLE : View.INVISIBLE
                    isVisibleViewModelVI = (isVisibleViewModel) ? (android.view.View.VISIBLE) : (android.view.View.INVISIBLE);
            }
            if ((dirtyFlags & 0xdL) != 0) {

                    if (viewModel != null) {
                        // read viewModel.text
                        textViewModel1 = viewModel.text;
                    }
                    updateRegistration(2, textViewModel1);


                    if (textViewModel1 != null) {
                        // read viewModel.text.get()
                        textViewModel = textViewModel1.get();
                    }
            }
        }
        // batch finished
        if ((dirtyFlags & 0xbL) != 0) {
            // api target 1

            this.mboundView2.setVisibility(isVisibleViewModelVI);
        }
        if ((dirtyFlags & 0xdL) != 0) {
            // api target 1

            android.databinding.adapters.TextViewBindingAdapter.setText(this.tvTest, textViewModel);
        }
    }
  • 我们可以看到上面的代码,无论ViewModel何时修改它们的值,它都会通知executeBindings()方法,这个方法依赖于这个值并且会在View上执行动作,例如:
this.mboundView2.setVisibility(isVisibleViewModelVI);
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvTest, textViewModel);
  • 这是为啥我们需要写@BindingAdapter和@BindingMethod用于在View上执行一个动作,例如:recyclerView.setAdapter(), viewPager.setOnPageChange(), ...。
  • 然而,Android已经创建了大量的内置BindingAdapters和BindingMethod,因此开发者大多数情况下只需要使用它。我们只需要在特殊情况下实现我们自己的代码或Android现在还不支持它。这种情况下,isShowView为false对应View.INVISIBLE会执行View.setVisibility(View.INVISIBLE)。That's all。
  • 进行下一部分之前,我想让你知道data-binding内置的adapters。记得使用这些并且不要重复发明轮子。我已经看到有很多开发者滥用@BindingAdapter重写已存在的东西。真是浪费!

Part 2:data-binding编译器是怎样生成代码的?

你有没有把我上面所给出的官方git仓库clone下来?Data-Binding Repository

在理解了生成的代码是怎样在View和ViewModel之间绑定之后,在这一部分,我们会找出编译生成神奇代码的方法。

注意这两个模块:compilercompilerCommon。我们看的最多的地方。

  • 编译器的核心为compiler.android.databinding.annotationprocessor包下的ProcessDataBinding类。这个类的职责是一步一步执行处理列表。
java
         mProcessingSteps = Arrays.asList(
                new ProcessMethodAdapters(),
                new ProcessExpressions(),
                new ProcessBindable()
        );
  • 我们先看第一个处理步骤——ProcessMethodAdapters。这个类提供搜索工程中所有的类,哪一个类哪一个方法添加了下面的注解:@BindingAdapter, @Untaggable, @BindingMethods, @BindingConversion, @InverseBindingAdapter, @InverseBindingMethods。并且把它们保存在SetterStore,后面应该在executeBinding用到正如我们上面所说。在编译期间,注解处理器拿到的这些信息会被存放在setter_store.bin文件中。
java
@Override
    public boolean onHandleStep(RoundEnvironment roundEnv,
            ProcessingEnvironment processingEnvironment, BindingBuildInfo buildInfo) {
        L.d("processing adapters");
        final ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
        Preconditions.checkNotNull(modelAnalyzer, "Model analyzer should be"
                + " initialized first");
        SetterStore store = SetterStore.get(modelAnalyzer);
        clearIncrementalClasses(roundEnv, store);

        addBindingAdapters(roundEnv, processingEnvironment, store);
        addRenamed(roundEnv, store);
        addConversions(roundEnv, store);
        addUntaggable(roundEnv, store);
        addInverseAdapters(roundEnv, processingEnvironment, store);
        addInverseMethods(roundEnv, store);

        try {
            store.write(buildInfo.modulePackage(), processingEnvironment);
        } catch (IOException e) {
            L.e(e, "Could not write BindingAdapter intermediate file.");
        }
        return true;
    }

    ...

    public void write(String projectPackage, ProcessingEnvironment processingEnvironment)
            throws IOException {
        GenerationalClassUtil.writeIntermediateFile(processingEnvironment,
                projectPackage, projectPackage +
                        GenerationalClassUtil.ExtensionFilter.SETTER_STORE.getExtension(), mStore);
    }

    ...

    public enum ExtensionFilter {
        BR("-br.bin"),
        LAYOUT("-layoutinfo.bin"),
        SETTER_STORE("-setter_store.bin");
        private final String mExtension;
        ExtensionFilter(String extension) {
            mExtension = extension;
        }

        public boolean accept(String entryName) {
            return entryName.endsWith(mExtension);
        }

        public String getExtension() {
            return mExtension;
        }
    }
  • 第二步是ProcessExpressions处理表达式。在这一步中会搜索工程中所有xml文件并且会转换最外层为<layout></layout>标签支持data-binding的xml文件。会把这个文件拆分为2个文件正如第一部分所提到的:activity_main.xml(正常的布局文件)和activity_main-layout.xml(包含绑定信息)。LayoutBinder是最有意思的类,它使用(XmlParser中)layoutBundle在activity_main-layout.xml中计算表达式,位置和目标。
java
@XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement(name="Layout")
    public static class LayoutFileBundle implements Serializable, FileScopeProvider {
        @XmlAttribute(name="layout", required = true)
        public String mFileName;
        @XmlAttribute(name="modulePackage", required = true)
        public String mModulePackage;
        @XmlAttribute(name="absoluteFilePath", required = true)
        public String mAbsoluteFilePath;
        private String mConfigName;
        // The binding class as given by the user
        @XmlAttribute(name="bindingClass", required = false)
        public String mBindingClass;

        // The location of the name of the generated class, optional
        @XmlElement(name = "ClassNameLocation", required = false)
        private Location mClassNameLocation;
        // The full package and class name as determined from mBindingClass and mModulePackage
        private String mFullBindingClass;

        // The simple binding class name as determined from mBindingClass and mModulePackage
        private String mBindingClassName;

        // The package of the binding class as determined from mBindingClass and mModulePackage
        private String mBindingPackage;

        @XmlAttribute(name="directory", required = true)
        public String mDirectory;
        public boolean mHasVariations;

        @XmlElement(name="Variables")
        public List<VariableDeclaration> mVariables = new ArrayList<VariableDeclaration>();

        @XmlElement(name="Imports")
        public List<NameTypeLocation> mImports = new ArrayList<NameTypeLocation>();

        @XmlElementWrapper(name="Targets")
        @XmlElement(name="Target")
        public List<BindingTargetBundle> mBindingTargetBundles = new ArrayList<BindingTargetBundle>();

        @XmlAttribute(name="isMerge", required = true)
        private boolean mIsMerge;

        ...

    }
java
 public LayoutBinder(ResourceBundle.LayoutFileBundle layoutBundle) {
        try {
            Scope.enter(this);
            mExprModel = new ExprModel();
            mExpressionParser = new ExpressionParser(mExprModel);
            mBindingTargets = new ArrayList<BindingTarget>();
            mBundle = layoutBundle;
            mModulePackage = layoutBundle.getModulePackage();
            HashSet<String> names = new HashSet<String>();
            // copy over data.
            for (ResourceBundle.VariableDeclaration variable : mBundle.getVariables()) {
                addVariable(variable.name, variable.type, variable.location, variable.declared);
                names.add(variable.name);
            }
            for (ResourceBundle.NameTypeLocation userImport : mBundle.getImports()) {
                mExprModel.addImport(userImport.name, userImport.type, userImport.location);
                names.add(userImport.name);
            }
            if (!names.contains("context")) {
                mExprModel.builtInVariable("context", "android.content.Context",
                        "getRoot().getContext()");
                names.add("context");
            }
            for (String javaLangClass : sJavaLangClasses) {
                mExprModel.addImport(javaLangClass, "java.lang." + javaLangClass, null);
            }
            // First resolve all the View fields
            // Ensure there are no conflicts with variable names
            for (BindingTargetBundle targetBundle : mBundle.getBindingTargetBundles()) {
                try {
                    Scope.enter(targetBundle);
                    final BindingTarget bindingTarget = createBindingTarget(targetBundle);
                    if (bindingTarget.getId() != null) {
                        final String fieldName = LayoutBinderWriterKt.
                                getReadableName(bindingTarget);
                        if (names.contains(fieldName)) {
                            L.w("View field %s collides with a variable or import", fieldName);
                        } else {
                            names.add(fieldName);
                            mExprModel.viewFieldExpr(bindingTarget);
                        }
                    }
                } finally {
                    Scope.exit();
                }
            }
            for (BindingTarget bindingTarget : mBindingTargets) {
                try {
                    Scope.enter(bindingTarget.mBundle);
                    for (BindingTargetBundle.BindingBundle bindingBundle : bindingTarget.mBundle
                            .getBindingBundleList()) {
                        try {
                            Scope.enter(bindingBundle.getValueLocation());
                            bindingTarget.addBinding(bindingBundle.getName(),
                                    parse(bindingBundle.getExpr(), bindingBundle.isTwoWay(),
                                            bindingBundle.getValueLocation()));
                        } finally {
                            Scope.exit();
                        }
                    }
                    bindingTarget.resolveTwoWayExpressions();
                    bindingTarget.resolveMultiSetters();
                    bindingTarget.resolveListeners();
                } finally {
                    Scope.exit();
                }
            }
            mSortedBindingTargets = new ArrayList<BindingTarget>(mBindingTargets);
            Collections.sort(mSortedBindingTargets, COMPARE_FIELD_NAME);
        } finally {
            Scope.exit();
        }
    }
  • 第三步是ProcessBindable。这个处理生成BR类,绑定属性的id,例如:BR.text, BR.item, BR.isShowView, ...
  • 最后,你可能想知道最重要的类MainDataBinding是在哪里创建的。我不知道为啥Google使用Kotlin编写的,相关文件为DataBinderWriter.kt 和 LayoutBinderWriter.kt。你可以自己去看这些文件。

总结

我希望你能读到这里,因为这篇文章有难点我们读起来很难理解。Data-Binding用起来不是很容易甚至很难理解。但我认为当我们真正理解后面的代码,没有什么是秘密,因为我们知道data-binding的原理。在一些工程上使用data-binding之后,我看到一些data-binding相关的bug很难跟踪和解决,开发者在它上面花费了很多时间。通过这篇文章,我们不再害怕深入到生成的代码查找bug原因。了解了编译器的知识可以帮助我们用最好的方式去写代码。

在下面留下评论,告诉我哪一块是你不理解的,我会尽力更新,让这篇文章更有用。Thanks for your time!

相关文章

网友评论

  • evaqin:突然发现我这边在Android 3.1下找不到data-binding-layout-out文件夹,请问一下简化的xml文件在哪里可以看到呢?

本文标题:(译)深入理解Data Binding原理

本文链接:https://www.haomeiwen.com/subject/aldmkxtx.html