DataBinding源码初探

  • 概述

    • 本例分以DataBinding在Java,Kotlin两门语言的使用入手,浅谈了DataBinding运行机制;是如何实现双向绑定?为什么Kotlin中不能使用注解?DataBinding在哪里消耗了内存?等问题

源码分析:DataBinding_Java版本

  • Model层中的BR文件:在其中会为每维护的字段添加整数标记,内部借助此标记完成数据流动

    • 示意图:BR文件源代码

      图片.png

    • 示意图:可以在Model层发送BR.字段时,使用整数标记做替代;不推荐,使用BR.字段可以减少容错性

      图片.png

  • DataBinding是如何完成View层与Model牵手的:作为中间层,充当VM层角色

    • VM层与View层绑定:

        final ActivityMainBinding binding = 
        DataBindingUtil.setContentView(this,R.layout.activity_main);
      复制代码
    • VM 层与Model层绑定

       //模拟Model数据:假设是从网络上过来的json数据,解析成了JavaBean
       User user;
       ​
       user = new User("wasbry","123456");
       ​
       binding.setUser(user);//建立与DataBinding的绑定关系,否则没有任何效果
      复制代码

      细节:user = new User(“wasbry”,”123456″);会为View层提供默认值

       android:text="@{user.name}"
       android:text="@{user.pwd}"
      复制代码
  • 数据驱动UI细节:

    • 当VM层建立起双向绑定后

      • 数据驱动UI:仅需要开启一个线程对Model层数据修改—>UI更新
      • UI修改数据:通过日志形式打印用户输入内容
    • 代码展示:

       //Model--->View
               new Thread(new Runnable() {
                   @Override
                   public void run() {
                       for(int i = 0;i < 10;i++){
                           try {
                               Thread.sleep(1000);
                               user.setName("第"+i+"次改变用户名");
                               user.setPwd("第"+i+"次改变用户名");
                           } catch (InterruptedException e) {
                               e.printStackTrace();
                           }
                       }
                   }
               }).start();
      复制代码
  • 数据流动顺序:为什么不再需要findViewById

    1. 在VM层中更新字段

       user.setName("第"+i+"次改变用户名");
      复制代码
    2. 跳转到Model层

       notifyPropertyChanged(BR.name); // APT又是主接处理器技术 BR文件
      复制代码
    3. 跳转到View层

       android:text="@{user.name}"
      复制代码
  • 布局拆分细节:

    • 流程分析:DataBinding读取交给其管理的布局,通过里面的tag属性找到拆出的另外一半(Android View绘制的)

      • activity_main.xml:交给DataBinding进行管理

         <?xml version="1.0" encoding="utf-8"?>
         <!--DataBinding 管理了整个布局文件,但是将其拆分成了两部分-->
         <!--拿给DataBinding内部使用-->
         <layout 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">
         ​
             <data>
                 <variable
                     name="user"
                     type="com.wasbry.myapplication.User"
                     />
             </data>
         ​
         <!--    拿给Android做View绘制使用-->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">
         ​
                 <TextView
                     android:id="@+id/tv1"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@{user.name}"
                     android:textSize="50sp"
                     />
         ​
                 <TextView
                     android:id="@+id/tv2"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@{user.pwd}"
                     android:textSize="50sp"/>
         ​
            </LinearLayout>
         </layout>
        复制代码
      • activity_main-layout.xml:DataBinding内部管理的布局

        • 路径:build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main-layout.xml

        • 细节:使用Tag,拿到布局中的控件(布局越复杂,拆出给DataBinding管理的布局越复杂)

            <Targets>
                   <Target tag="layout/activity_main_0" view="LinearLayout">
                       <Expressions />
                       <location endLine="35" endOffset="17" startLine="15" startOffset="3" />
                   </Target>
                   <Target id="@+id/tv1" tag="binding_1" view="TextView">
                       <Expressions>
                           <Expression attribute="android:text" text="user.name">
                               <Location endLine="24" endOffset="38" startLine="24" startOffset="12" />
                               <TwoWay>false</TwoWay>
                               <ValueLocation endLine="24" endOffset="36" startLine="24" startOffset="28" />
                           </Expression>
                       </Expressions>
                       <location endLine="26" endOffset="13" startLine="20" startOffset="8" />
                   </Target>
                   <Target id="@+id/tv2" tag="binding_2" view="TextView">
                       <Expressions>
                           <Expression attribute="android:text" text="user.pwd">
                               <Location endLine="32" endOffset="37" startLine="32" startOffset="12" />
                               <TwoWay>false</TwoWay>
                               <ValueLocation endLine="32" endOffset="35" startLine="32" startOffset="28" />
                           </Expression>
                       </Expressions>
                       <location endLine="33" endOffset="36" startLine="28" startOffset="8" />
                   </Target>
               </Targets>
          复制代码
      • activity_main.xml:原布局拆分出,交给Android绘制UI使用的

        • 路径:build/intermediates/incremental/mergeDebugResources/stripped.dir/layout/activity_main.xml

        • 细节:为每个控件增添了tag,与从原布局拆出,交给DataBinding管理的里面的tag属性相呼应

            <TextView
                       ……
                       android:tag="binding_1"    
                       />
           ​
                   <TextView
                       ……
                       android:tag="binding_2"   
                      />
          复制代码
  • 为什么可以废弃setContentView

    • 使用ActivityMainBinding指代布局文件:

       final ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
      复制代码
      • 证明指代:点击它(CTRL+H鼠标左键),发现直接都将绘制的布局文件自动全选

        图片.png

    • 废除原有的setContentView语句

       //        setContentView(R.layout.activity_main);
      复制代码
      • 实际上还是在内部调用了的

        图片.png
        图片.png
        图片.png

    • setContentView分析:调用bindToAddedViews

       public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
               int layoutId, @Nullable DataBindingComponent bindingComponent) {
           //根据布局id,加载布局
           activity.setContentView(layoutId);
           //有Activity得到根布局Window,方便遍历
           View decorView = activity.getWindow().getDecorView();
           ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
           //深层调用
           return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
       }
      复制代码
    • bindToAddedViews:

      • 无论是有多少个子View都会调用到:bind

         //一个孩子
         return bind(component, childView, layoutId);
         //多个孩子
         return bind(component, children, layoutId);
        复制代码
    • bind分析

      • 将布局信息:DataBindingComponent bindingComponent, View root,int layoutId 传入

      • 调用:getDataBinder

         return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
        复制代码
    • getDataBinder:抽象函数

       public abstract ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
                   int layoutId);
      复制代码
      • 点一下左边行号绿色接口标志,选择第一个进行跳转

        • 示意图:

          image-20220306091323522

        • 这里是有一个坑的:提示源码不全,需要下载(这个是不会成功的,因为下载的是一个arr,其包含了jar包)

          image-20220306091420102

        • 由于源码不全,所以不再分析这个代码,需要跳转:

          • 路径:package androidx.databinding.library.baseAdapters;
          • 文件名:DataBinderMapperImpl .class
        • 需要跳转到同名的文件:源码不全

          • 因为aar包裹的代码会有一份APT代码在build/generated/ap_generated_sources/debug/out
          • 路径:build/generated/ap_generated_sources/debug/out/com/wasbry/myapplication/DataBinderMapperImpl.java
          • 文件名:DataBinderMapperImpl .java
        • 所以说抽象函数在跳转后会调用到DataBinderMapperImpl .java

    • DataBinderMapperImpl.java分析

      • 首先会获取到tag对应的布局(原布局拆出拿给Android绘制View的):为了拿到里面的所有控件

         case  LAYOUT_ACTIVITYMAIN: {
           if ("layout/activity_main_0".equals(tag)) {
               //将信息包裹后发送出去
             return new ActivityMainBindingImpl(component, view);
           }
        复制代码
  • ActivityMainBindingImpl分析

    • 构造函数一:深挖到对象数组

      • 为什么是3,因为这个有3个控件的tag,10个控件就是10
       public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
           this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
       }
      复制代码
    • 构造函数二:触发静态代码块

      • ActivityMainBindingImpl会继承ViewDataBinding的,只是说看不到而已
       private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
           //就是这个super
               super(bindingComponent, root, 2
                   , (android.widget.EditText) bindings[1]
                   , (android.widget.EditText) bindings[2]
                   );
      复制代码
    • mapBindings分析:将布局中所有控件打包成对象数组(一个控件就是一个对象),带来内存消耗

      • 其实比findViewById慢

         protected static Object[] mapBindings
        复制代码
      • 寻找layout 开头的,就是找根布局,

    • 静态代码块(第二个非常消耗内存,Model—>View数据驱动UI):为每一个控件添加了监听,十分消耗内存

       static {
           if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
                   ROOT_REATTACHED_LISTENER = null;
               } else {
                //为每一个控件添加了监听,十分消耗内存
                   ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
      复制代码
      • 最终会开启线程,不断回调,最后拿到了tag对应的控件,调用了setText,就是跟离谱的只是说变成了实时监听
      • 调用到excute,这个函数再回调到回去,在里面发现居然是setText
    • 第三个消耗内存的点(View—>Model)

      • 在xml中:就是这个等号

                <EditText
                          ……
                     android:text="@={user.nameF}"
                     />
        复制代码
      • 会生产一个textWatcher监听,每个控件都有一个监听,这个是以控件的tag作区别的

  • 为什么View触发Model,Model触发View不会导致死循环?

    • 里面在user.setName(java.lang.String):就将这个事件消耗了,类似于责任链模式
    • 因为是APT技术,会动态生成代码的
  • DataBinding的核心

    • 使用APT技术,扫描布局文件,生成了两个布局文件的;

源码分析:DataBinding实现数据驱动UI(Kotlin 版本)

  • Model层中为什么不能使用注解@Bindable

    1. 因为注解是拿给方法或者字段使用的

      image-20220306084528154

    2. 而kotlin中的每个字段是有默认的get/set方法的,也可以进行重写字段的get/set

            var name : String ?= null
       ​
           get()  {
               return field
           }
       ​
           set(value : String ?) {
               field  = value
           }
      复制代码
    3. 但是重写出来的get/set方法只是一个表达式而已,根本就不适用

  • Kotlin配合DataBInding使用何种方法维护Model层数据

    • ObservableField+泛型+懒加载

    • 代码:

       class User{
           
           //继承BaseObservable()失效,这个在java里面可以,但是在kotlin中就不行了
           val nameF : ObservableField<String> by lazy { ObservableField<String>() }
           val pwdF : ObservableField<String> by lazy { ObservableField<String>() }
       }
      复制代码
  • Kotlin版本中是如何建立起双向绑定的?

    • Model—>View:

       //模拟Model层数据(json解析后得到的javaBean)
           private val user = User()
               
           val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
               binding.user = user
      复制代码
    • View—>Model:注意玩双向绑定一定是等号

       //xml中
       <EditText
                   ……
                   android:text="@={user.nameF}"
                   />
       ​
               <EditText
                   ……
                   android:text="@={user.pwdF}"
                   />
       ​
       //VM层中
       Log.d("MainActivity","用户输入第一个字段为${user.nameF.get()}," +
                           "用户输入第二个字段为${user.pwdF.get()}")
      复制代码

      所谓数据驱动UI,UI驱动数据;不过是操作绑在DataBinding上的Model层数据

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享