Android JetPack深入分析DataBinding源码

news2024/12/24 22:02:06

前言

数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。

DataBinding支持双向绑定,数据变化的时候界面跟着变化,界面变化也同步给数据;

DataBinding在MVVM模式中使用比较多,双向绑定机制实现了View和Model的同步更新。

简单使用

DataBinding一般配合LiveDataViewModel一起使用,这里就简单使用下,便于后续源码分析;

  • build.gradle配置
    buildFeatures {
        dataBinding true
    }
  • 定义数据源
data class User(@Bindable var username: String = "", @Bindable var pwd: String = "") : BaseObservable()
  • 定义数据源绑定的布局文件activity_data_binding.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>
        <!--data标签内定义数据源User-->
        <variable
            name="User"
            type="com.xixu.jetpack.User" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tvName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{User.username}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


        <TextView
            android:id="@+id/tvPwd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{User.pwd}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tvName" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
  • 定义Activity使用
class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val dataBinding = DataBindingUtil.setContentView<ActivityDataBindingBinding>(
            this,
            R.layout.activity_data_binding
        )

        lifecycleScope.launch{
            delay(2000)
            dataBinding.user = User("XiXu", "123456")
        }

    }
}

界面效果:延迟2s后,分别将XiXu123456数据绑定到tvNametvPwd控件上;

源码分析

DataBinding是如何实现将数据绑定到具体视图上的呢?

DataBinding为我们生成了哪些布局文件

首先,DataBinding会使用APT(Annotation Processing Too,注解解析器),在编译器为我们生成如下布局文件
由于使用DataBinding,布局文件中引入了layout标签,我们先看下布局文件变化;

1.build/intermediates/incremental/packageDebugResources/stripped.dir/layout/activity_data_binding.xml,为每个控件都新增了tag属性;
activity_data_binding
2.build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_data_binding-layout.xml
定义了多组Target与布局文件中的tag标签控件对应,并标注了每个tag对应控件的view类型,Expressions标签中定义了控件属性对应绑定数据,其中TwoWay标签表示是否是双向绑定;
activity_data_binding-layout

小结

使用DataBinding会在编译期生成辅助布局文件,为每个控件新增tag标签,并记录控件类型id属性等信息;

DataBindingUtil.setContentView()做了什么

    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);
    }

调用setContentView方法主要做了如下几件事:

  1. 调用activity对应的setContentView方法绑定布局;
  2. 获取activity对应的R.id.content控件,我们知道即FrameLayout控件;
  3. 调用bindToAddedViews绑定布局中的控件;

这里我们重点看下bindToAddedViews是如何实现布局控件绑定的;

   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;
        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);
        }
    }

这里parent即指FrameLayout,因此parent.getChildCount()获取的便是根布局个数,上述例子对应的为1,即ConstraintLayout;最终会调用bind(component, childView, layoutId)方法如下:

    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }

其中sMapper初始化代码如下:

    private static DataBinderMapper sMapper = new DataBinderMapperImpl();
    
	public class DataBinderMapperImpl extends MergedDataBinderMapper {
    DataBinderMapperImpl() {
    addMapper(new com.crystal.maniu.DataBinderMapperImpl());
  }
}

因此sMapper.getDataBinder最终调用的为DataBinderMapperImpl【注意包名,与androidx.databinding区分】.getDataBinder方法:

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

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

  static {
    INTERNAL_LAYOUT_ID_LOOKUP.put(com.crystal.maniu.R.layout.activity_data_binding, LAYOUT_ACTIVITYDATABINDING);
  }

  @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_ACTIVITYDATABINDING: {
          if ("layout/activity_data_binding_0".equals(tag)) {
            return new ActivityDataBindingBindingImpl(component, view);
          }
          throw new IllegalArgumentException("The tag for activity_data_binding is invalid. Received: " + tag);
        }
      }
    }
    return null;
  }
 }

可见getDataBinder方法其实就是直接new了一个ActivityDataBindingBindingImpl【APT生成的类】对象,并把根布局ConstraintLayout入参;我们看看ActivityDataBindingBindingImpl构造方法里做了什么;

    public ActivityDataBindingBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
    }
    private ActivityDataBindingBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        super(bindingComponent, root, 1
            , (android.widget.TextView) bindings[1]
            , (android.widget.TextView) bindings[2]
            );
        this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
        this.mboundView0.setTag(null);
        this.tvName.setTag(null);
        this.tvPwd.setTag(null);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

其中mapBindings方法会对根据tag对布局文件进行xml解析得到控件数组 Object[],super会调用父类ActivityDataBindingBinding【APT生成的类】的构造方法,完成View的初始化工作 invalidateAll()会进行数据的初始化绑定,具体如何绑定我们下一步在分析!

小结

调用DataBindingUtil.setContentView方法会调用setContentView方法并完成控件的初始化工作,从而代替FindViewById的工作;

DataBinding如何实现数据更新

我们再看下调用dataBinding.user = User("XiXu", "123456")是如何实现将数据更新到控件上去的,具体实现交给ActivityDataBindingBindingImpl.setUser方法;

  public void setUser(@Nullable com.xixu.jetpack.User User) {
        updateRegistration(0, User);
        this.mUser = User;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.User);
        super.requestRebind();
    }

其中BR文件如下,定义一系列常量用于区分更新字段:

public class BR {
  public static final int User = 1;

  public static final int _all = 0;

  public static final int pwd = 2;

  public static final int username = 3;
}

可以看到调用setUser主要做了如下几件事:

  1. 调用updateRegistration(0, User)方法;User作为被观察者,会先判断User是否为null,如果为null解除注册,否则会将User绑定到WeakListener虚引用对象上,并包装成WeakPropertyListener对象,用于后续处理数据更新操作;

  2. 调用notifyPropertyChanged(BR.User)方法;经过层层处理会执行WeakPropertyListener.onPropertyChanged方法如下:

    ### WeakPropertyListener.onPropertyChanged
   public void onPropertyChanged(Observable sender, int propertyId) {
   			//1.先判断绑定的ViewDataBinding是否为null
            ViewDataBinding binder = mListener.getBinder();
            if (binder == null) {
                return;
            }
            //2.obj即为传入的User对象,判断和之前绑定的是否一致
            Observable obj = mListener.getTarget();
            if (obj != sender) {
                return; 
            }
            //3.调用ViewDataBinding.onFieldChange方法;
            binder.handleFieldChange(mListener.mLocalFieldId, sender, propertyId);
        }

ViewDataBinding.onFieldChange代码如下:

    protected void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
        if (mInLiveDataRegisterObserver || mInStateFlowRegisterObserver) {
            return;
        }
        boolean result = onFieldChange(mLocalFieldId, object, fieldId);
        if (result) {
        	//内部会进行Lifecycle生命周期判断,从而实现更新逻辑与生命周期绑定
            requestRebind();
        }
    }
	
    protected void requestRebind() {
        if (mContainingBinding != null) {
        	//设置包含绑定
            mContainingBinding.requestRebind();
        } else {
        	//这里会进行组件活跃判断,如果不是STARTED、RESUMED状态,不执行更新数据操作
            final LifecycleOwner owner = this.mLifecycleOwner;
            if (owner != null) {
                Lifecycle.State state = owner.getLifecycle().getCurrentState();
                if (!state.isAtLeast(Lifecycle.State.STARTED)) {
                    return;
                }
            }
            synchronized (this) {
                if (mPendingRebind) {
                    return;
                }
                mPendingRebind = true;
            }
            if (USE_CHOREOGRAPHER) {
            	//SDK_INT >= 16 使用编舞者处理,最终还是调用mRebindRunnable.run方法
                mChoreographer.postFrameCallback(mFrameCallback);
            } else {
                mUIThreadHandler.post(mRebindRunnable);
            }
        }
    }

其中onFieldChange调用的是ActivityDataBindingBindingImpl.onFieldChange方法根据mLocalFieldId以及fieldId匹配BR文件中的常量,判断能否更新;

如果返回true,则调用requestRebind()方法,最终又会回到 ActivityDataBindingBindingImpl.executeBindings方法

  protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        com.xixu.jetpack.User user = mUser;
        java.lang.String userUsername = null;
        java.lang.String userPwd = null;

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


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

                    if (user != null) {
                        // read User.username
                        userUsername = user.getUsername();
                    }
            }
            if ((dirtyFlags & 0xdL) != 0) {

                    if (user != null) {
                        // read User.pwd
                        userPwd = user.getPwd();
                    }
            }
        }
        // batch finished
        if ((dirtyFlags & 0xbL) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvName, userUsername);
        }
        if ((dirtyFlags & 0xdL) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvPwd, userPwd);
        }
    }

这里采用了取巧的方式处理,使用位运算操作计算要更新哪些控件,最终调用TextViewBindingAdapter.setText完成数据更新;

更新数据流程图更新数据流程图

总结

DataBinding通过APT技术于编译期生成相关辅助类,当调用DataBindingUtil.setContentView方法时帮助我们完成布局控件绑定工作,减少FindViewById操作,当调用数据更新操作时,数据作为被观察者,会绑定到WeakListener虚引用对象上,更新过程中会使用Lifecycle进行生命周期判断,最终通过调用androidx.databinding.adapters包下的辅助工具类完成控件更新操作;

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/726066.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为6605AC控制器大型组网wlan pool技术应用(自动漫游)

AC命令行配置&#xff1a; dis current-configuration set memory-usage threshold 0 ssl renegotiation-rate 1 vlan batch 100 vlan pool vlan_pool_test vlan 11 to 12 diffserv domain default radius-server template default free-rule-template name default_free_rule …

win10通过conda安装pytorch gpu

1. 安装anaconda 到官网下载最新版的anaconda&#xff0c;下载对应的windows版本&#xff0c;地址&#xff1a;anaconda官网 下载后直接安装&#xff0c;安装完成后配置环境变量&#xff0c;具体可以百度anaconda安装说明。安装完成后&#xff0c;打开cmd&#xff0c;输入conda…

Leetcode - 352周赛详解

一&#xff0c;最长奇偶子数组 看题可知可以使用暴力求解&#xff0c;从头开始遍历数组&#xff0c;另设一个变量来记录满足条件的子数组长度的最大值&#xff0c;将该变量不断与新得到的子数组长度比较&#xff0c;最终得到子数组长度的最大值。但是这样依次遍历的话&#xff…

git 分支管理

目录 一. 理解分支 1. master分支 二. 分支的&#xff08;创建&#xff0c;切换、合并、删除&#xff09; 1.查看分支 2.创建分支 3. 切换分支 4. 合并分支 5. 删除分支 三. 合并冲突 四. 合并模式 五. bug 分支 六. 强制删除分支 一. 理解分支 1. master分支 在我们的…

省电,AMD 提交 Linux 新技术 FreeSync Panel Replay

导读AMD 近日发布了一系列补丁&#xff0c;为其 AMDGPU Linux 内核显卡驱动程序的显示代码 “DC” 添加了一个新功能&#xff1a;FreeSync Panel Replay。这个功能针对搭载 DCN v3.1.4 显示块或更新版本的 AMD Ryzen 笔记本电脑&#xff0c;可以在屏幕内容不变时降低功耗。 目…

关于https的加密流程简介(图解)

目录 对称加密&#xff1a; 非对称加密 对称加密&#xff1a; 在网络发展的初阶&#xff0c;为了保护数据安全&#xff0c;防止黑客攻击&#xff0c;我们发明了对称加密 即一把秘钥&#xff0c;客户端和服务器通过这把钥匙对数据进行加密/解密 理想情况下&#xff0c;只要没…

第163天:应急响应-后门攻击检测指南Rookit内存马权限维持WINLinux

知识点 #知识点 -网页篡改与后门攻击防范应对指南 主要需了解&#xff1a;异常特征&#xff0c;处置流程&#xff0c;分析报告等 主要需了解&#xff1a;日志存储&#xff0c;Webshell检测&#xff0c;分析思路等 掌握&#xff1a; 中间件日志存储&#xff0c;日志格式内容介绍…

Qt之QMainWindow 自定义标题栏

简述 Qt自带标题栏功能还是很强大的&#xff0c;但是确实不能百分百满足需求&#xff0c;除了丑以外还不能随意更改标题栏字体&#xff0c;也不能更改样式&#xff1b;所以为了满足自己的虚荣心让标题栏变得更加好用看好看&#xff0c;特地花时间做了以下测试&#xff1b; 支持…

MySQL:数据的增删改查

数据的增删改查 前言一、插入数据1、实际问题2、方式1&#xff1a;VALUES的方式添加3、方式2&#xff1a;将查询结果插入到表中 二、删除数据三、更新数据四、查询数据五、MySQL8新特性&#xff1a;计算列 前言 本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知…

Dubbo sentinel 哨兵 熔断 慢调用比例、异常比例、异常数

目录 熔断报这个错误 在调用程序中检测即可 页面配置熔断&#xff0c;配置在 consumer 消费端 慢调用比例规则 最大rt 比例阈值 熔断时长 最小请求数 统计时长 让流量一直降低发送 异常数量 设置30个异常数 Tps 异常数量未达到30个正常运行 设置1个效果直接连续…

git bash 命令行反应慢、卡顿

1. 在Windows11的电脑上安装了git 后&#xff0c;鼠标右键打开git bash here&#xff0c;打开窗口缓慢&#xff0c;输入命令也慢的要死&#xff0c;如果安装git的时候选择在桌面创建图标&#xff0c;通过桌面图标打开也是一样的 2. 最简单的ls 命令&#xff0c;都要停顿半秒 3.…

Spring Security OAuth2.0(4):Spring Security集成SpringBoot

文章目录 前言一、创建工程二、spring容器配置三、Servlet Context配置四、安全配置五、创建测试七、启动服务器测试 前言 \qquad Spring Boot 是一套Spring的快速开发框架&#xff0c;基于Spring4.0设计&#xff0c;使用Spring Boot开发可以避免一些繁琐的工程搭建和配置&…

d3dx9_43.dll丢失怎么解决

d3dx9_43.dll丢失的影响 当我们在运行某些需要DirectX 9支持的程序时&#xff0c;如果系统中缺少d3dx9_43.dll文件&#xff0c;就会出现错误提示&#xff0c;导致程序无法正常启动。这个错误提示通常会类似于“找不到d3dx9_43.dll”或“d3dx9_43.dll不存在”。 打开电脑浏览器…

剑指 Offer 37: 序列化二叉树

这道题很复杂&#xff0c;首先需要发现是层序遍历&#xff0c;因为只有层序遍历才是这个顺序&#xff0c;并且new就可以调用&#xff0c;说明这里里面就生成了一个新的String&#xff08;可以new一个String&#xff09;&#xff0c;给StringBuilder初始化一个]&#xff0c;然后…

力扣题库刷题笔记31--下一个排列

1、题目如下&#xff1a; 2、个人Python代码实现如下&#xff1a; 前几次提交错误&#xff0c;主要是在上面截图第19行代码&#xff0c;原先写的是Nums nums[:i] temp&#xff0c;然后本地一直能跑过&#xff0c;这里不做多赘述 3、个人Python代码思路&#xff1a; 首先来讲本…

使用springboot框架Java+vue2开发的智慧班牌系统源码,SaaS云平台前后端分离架构

智慧班牌系统可实现数字化办公&#xff0c;对外向学生家长提供各种服务&#xff0c;如&#xff1a;消息通知、请假管理&#xff0c;校园活动&#xff0c;学生动态&#xff0c;教师通讯录&#xff0c;学生定位等各种服务。对内向教师提供各类服务&#xff0c;如&#xff1a;班级…

SpringMVC-1

学习笔记&#xff1a; SpringMVC 框架 Spring Web MVC 是一种基于 Java 实现了 MVC 设计模式的请求驱动型的轻量级 Web 框架&#xff0c;即使用了 MVC 架构模式的思想&#xff0c;将 web 层进行职责解耦&#xff0c;基于请求驱动指的就是使用请求-响应模型&#xff0c;框架的…

[PG]查看数据库大小

查看库大小 select d."oid" as "对象ID", d."datname" as "实例名", d."owner" as "所属者",pg_catalog.pg_size_pretty(d."size") as "大小" from (select oid, datname, pg_catalog.pg_ge…

小程序根据登录的角色动态设置 tabBar

根据登录的状态来展示tabbar的名称 type1 》 供应商 》 合同 送货单 我的 type2 》 监理 》 合同 抽检单 我的 在登录之后就拿到type的值 以及登陆之后跳转到合同页面 在合同页面可以书写逻辑 onShow(){if (Number(uni.getStorageSync("type")) 2) {console.log(11…

mybatis表达式判断引发的问题

在做条件查询的时候&#xff0c; 看似好像没啥问题&#xff0c;但是在查询的时候&#xff0c;检索条件无效&#xff0c;只要把! 去掉就好了&#xff0c; 完美解决。