DataBinding的双向绑定实现原理

Posted 一代小强

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DataBinding的双向绑定实现原理相关的知识,希望对你有一定的参考价值。


“ 悄悄咪咪告诉你,DataBinding是怎么实现双向绑定的“

在讲DataBinding之前,有必要讲讲ViewBinding

1、ViewBinding

1) 配置

要使用ViewBinding,只需要在gradle 添加如下配置即可

android {
        ...
        viewBinding {
            enabled = true
        }
 }

如果需要在生成绑定类时忽略某个布局文件,请将 tools:viewBindingIgnore=“true” 属性添加到相应布局文件的根视图中:

<LinearLayout
        ...
            tools:viewBindingIgnore="true" >
        ...
</LinearLayout>

2)用法

在创建xml文件后,Android Studio会自动创建对应的类,类名格式为:XML 文件的名称转换为驼峰式大小写,并在末尾添加Binding一词。

比如,创建了一个activity_view.xml

<LinearLayout 
           ...>
    <TextView
        android:id="@+id/viewBindingView"
        android:layout_width="match_parent"
        android:layout_height="20dp" />
</LinearLayout>

生成的绑定类就叫做ActivityViewBinding,然后在对应的activity中使用布局绑定


public class ViewBindingActivity extends AppCompatActivity {
    private ActivityViewBinding activityViewBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 初始化布局和view
        activityViewBinding = ActivityViewBinding.inflate(getLayoutInflater());
        // 设置contentView
        setContentView(activityViewBinding.getRoot());
        // 获取到id为viewBindingView的textView,并设置文本
        activityViewBinding.viewBindingView.setText("你好,我是viewBinding");
    }
}

3) 原理

在build之后,会在如下路径生成对应的类

生成如下代码

public final class ActivityViewBinding implements ViewBinding {
    @NonNull
    private final LinearLayout rootView;
    // 通过遍历xml,找到声明id了的view,id作为变量名
    @NonNull
    public final TextView viewBindingView;

    private ActivityViewBinding(@NonNull LinearLayout rootView, @NonNull TextView viewBindingView) {
        this.rootView = rootView;
        this.viewBindingView = viewBindingView;
    }

    @Override
    @NonNull
    public LinearLayout getRoot() {
        return rootView;
    }

    // 传入LayoutInflater,用于获取对应布局的view
    @NonNull
    public static ActivityViewBinding inflate(@NonNull LayoutInflater inflater) {
        return inflate(inflater, null, false);
    }

    @NonNull
    public static ActivityViewBinding inflate(@NonNull LayoutInflater inflater,
                                              @Nullable ViewGroup parent, boolean attachToParent) {
        // 通过指定的布局获取到rootView
        View root = inflater.inflate(R.layout.activity_view, parent, false);
        if (attachToParent) {
            parent.addView(root);
        }
        return bind(root);
    }
    // 核心代码,通过代码模板,找到对应的view
    @NonNull
    public static ActivityViewBinding bind(@NonNull View rootView) {
        int id;
        missingId:
        {
            id = R.id.viewBindingView;
            TextView viewBindingView = rootView.findViewById(id);
            if (viewBindingView == null) {
                break missingId;
            }
            return new ActivityViewBinding((LinearLayout) rootView, viewBindingView);
        }
        String missingId = rootView.getResources().getResourceName(id);
        throw new NullPointerException("Missing required view with ID: ".concat(missingId));
    }
}

生成的类通过LayoutInflater获取布局,然后依次初始化view。

2、DataBinding

1)配置

DataBinding 跟ViewBinding一样,只需要在gradle中添加如下配置即可

android {
        ...
        dataBinding {
            enabled = true
        }
    }

2)用法

我们先看activity_main.xml布局:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="video"
            type="com.example.databinding.VideoBean" />
    </data>
    <LinearLayout xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/bind_text1"
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:gravity="center"
            android:text="@{video.title}" />

        <Button
            android:id="@+id/bind_text2"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:text="@{String.valueOf(video.score + 1)}" />

    </LinearLayout>
</layout>

与正常的xml布局不一样的是,DataBinding 以layout作为第一层。第二层分别是 data和view的根布局。

其中data下的name作为对象,type为类路径名

package com.example.databinding;
public class VideoBean {
    public String title;
    public int score;
    public VideoBean(String title, int score) {
        this.title = title;
        this.score = score;
    }
}

然后在布局中就可以把对象的属性值显示到TextView上

<TextView
     ...
     android:text="@{video.title}" />

在对应的activity中,使用的方式与ViewBinding类似

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding mainBinding;
    private VideoBean video;

    @Override
    protected void onCreate(@Nullable  Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 获取对应的binding类
        mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(mainBinding.getRoot());
        video = new VideoBean("小黄人", 8);
        // 设置对应的数据
        mainBinding.setVideo(video);
        // 设置周期监听,可选
        mainBinding.setLifecycleOwner(this);
    }
}

全程我们没有初始化对应的view,只是获取bean,然后更新一下即可

当然,我们也可以在布局中添加对应的事件响应

      <Button
            android:id="@+id/bind_text2"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:onClick="@{video::increaseScore}"
            android:text="@{String.valueOf(video.score + 1)}" />

在类中添加 increaseScore方法,如下所示,即可实现点击button修改score的值,并更新ui。

public class VideoBean extends BaseObservable {
    public String title;
    // 声明该属性是可绑定监听的
    @Bindable
    public int score;
    public VideoBean(String title, int score) {
        this.title = title;
        this.score = score;
    }

    public void increaseScore(View view) {
        score++;
        // 通知score属性变化了
        notifyPropertyChanged(BR.score);
    }
}

我们只需要修改少量代码即可实现双向绑定,接下来将会讲解对应的原理。

更多见布局和绑定表达式

3)原理

view的初始化

生成的类路径与ViewBinding一样,直接看看对应的类

// 抽象类
public abstract class ActivityMainBinding extends ViewDataBinding {
  @NonNull
  public final Button bindText2;
  @Bindable
  protected VideoBean mVideo;
  protected ActivityMainBinding(Object _bindingComponent, View _root, int _localFieldCount,
                                TextView bindText1, Button bindText2) {
    super(_bindingComponent, _root, _localFieldCount);
    this.bindText1 = bindText1;
    this.bindText2 = bindText2;
  }

  // 把data下的参数都声明了get/set方法
  public abstract void setVideo(@Nullable VideoBean video);

  @Nullable
  public VideoBean getVideo() {
    return mVideo;
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, DataBindingUtil.getDefaultComponent());
  }

  @NonNull
  @Deprecated
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
                                            @Nullable Object component) {
    return ViewDataBinding.<ActivityMainBinding>inflateInternal(inflater, R.layout.activity_main, null, false, component);
  }
  ...
}

这里的ActivityMainBinding 继承了ViewDataBinding,但是内部却没有类似ViewBinding那样,直接通过findViewById方式初始化我们需要的view,那这些view是怎么来的?

这个问题我们先放一放,上面的inflate方法调用了ViewDataBinding的inflateInternal 方法

public abstract class ViewDataBinding extends BaseObservable implements ViewBinding {
    ...
    protected static <T extends ViewDataBinding> T inflateInternal(
            @NonNull LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent,
            boolean attachToParent, @Nullable Object bindingComponent) {
        return DataBindingUtil.inflate(
                inflater,
                layoutId,
                parent,
                attachToParent,
                checkAndCastToBindingComponent(bindingComponent)
        );
    }
}

来到DataBindingUtil 这个工具类

public class DataBindingUtil {
   private static DataBinderMapper sMapper = new DataBinderMapperImpl();
    
   public static <T extends ViewDataBinding> T inflate(
            @NonNull LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent,
            boolean attachToParent, @Nullable DataBindingComponent bindingComponent) {
        final boolean useChildren = parent != null && attachToParent;
        final int startChildren = useChildren ? parent.getChildCount() : 0;
        final View view = inflater.inflate(layoutId, parent, attachToParent);
        if (useChildren) {
            return bindToAddedViews(bindingComponent, parent, startChildren, layoutId);
        } else {
            // activity里面传的parent为null ,所以会走这里
            return bind(bindingComponent, view, layoutId);
        }
    }
    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }

}

DataBinderMapperImpl 的实现如下

package androidx.databinding;

public class DataBinderMapperImpl extends MergedDataBinderMapper {
  DataBinderMapperImpl() {
    // 将impl添加到mMappers中
    addMapper(new com.example.databinding.DataBinderMapperImpl());
  }
}
// 对应的基类
public class MergedDataBinderMapper extends DataBinderMapper {
    .... 
    @Override
    public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId) {
        // 遍历 mMappers
        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;
    }
}

那就看看com.example.databinding.DataBinderMapperImpl 的实现

public class DataBinderMapperImpl extends DataBinderMapper {
  private static final int LAYOUT_ACTIVITYMAIN = 1;

  private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(1);

  static {
    // 保存我们创建的layout与 index的映射
    INTERNAL_LAYOUT_ID_LOOKUP.put(com.example.databinding.R.layout.activity_main, LAYOUT_ACTIVITYMAIN);
  }

  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
    // 通过layoutId,获取到index,上面传的是R.layout.activity_main,
    // 所以这里localizedLayoutId为LAYOUT_ACTIVITYMAIN
    int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
    if(localizedLayoutId > 0) {
       // 获取到TAG,上面的代码没有设置tag的地方,但是通过debug得知对应的tag为"layout/activity_main_0"
      final Object tag = view.getTag();
      if(tag == null) {
        throw new RuntimeException("view must have a tag");
      }
      switch(localizedLayoutId) {
        case  LAYOUT_ACTIVITYMAIN: {
          if ("layout/activity_main_0".equals(tag)) {
            return new ActivityMainBindingImpl(component, view);
          }
          throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
        }
      }
    }
    return null;
  }
  ...
}

至于view的tag是怎么来的,笔者猜测是Android Studio自动添加的

最终我们获取到了ActivityMainBindingImpl的实例,对应实现如下

public class ActivityMainBindingImpl extends ActivityMainBinding  {
    @Nullable
    private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
    @Nullable
    private static final android.util.SparseIntArray sViewsWithIds;
    static {
        sIncludes = null;
        sViewsWithIds = null;
    }
    // views
    @NonNull
    private final android.widget.LinearLayout mboundView0;
    // listeners
    private OnClickListenerImpl mVideoIncreaseScoreAndroidViewViewOnClickListener;
    // Inverse Binding Event Handlers

    public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        // 通过mapBindings方法获取view的数组
        this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
    }
    private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        // 调用super即ActivityMainBinding的构造函数初始化view
        super(bindingComponent, root, 0
            , (android.widget.TextView) bindings[1]
            , (android.widget.Button) bindings[2]
            );
        this.bindText1.setTag(null);
        this.bindText2.setTag(null);
        this.mboundView0 = (android.widget.LinearLayout) bindings[0];
        this.mboundView0.setTag(null);
        setRootTag(root);
        invalidateAll();
    }
    ...
    public void setVideo(@Nullable com.example.databinding.VideoBean Video) {
        this.mVideo = Video;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        <

以上是关于DataBinding的双向绑定实现原理的主要内容,如果未能解决你的问题,请参考以下文章

DataBinding的双向绑定实现原理

DataBinding的双向绑定实现原理

Android,DataBinding的官方双向绑定

databinding的原理简单分析

Vue3 双向绑定——Proxy

浅谈Vue的双向绑定