DataBinding实现原理探析

作者: 工程师milter | 来源:发表于2016-07-24 17:31 被阅读10170次

    声明:本篇文章已授权微信公众号 guolin_blog(郭霖)独家发布!
    关于DataBinding技术,网上教程可谓多矣,但大都没能摆脱简单拷贝、翻译修改官方文档的嫌疑,个别的翻译的还不准确,初学者很容易被误导。鉴于此,本文结合本人项目实践中的经验与思考,为广大Android 开发者提供一篇有观点、有思考的DataBinding讲解文章。
    声明:本文为作者原创,如需转载请自取,仅需在文章开头注明本人简书账号milter、原文链接并确保文章内容不被修改即可

    DataBinding技术能解决什么问题?


    DataBinding技术的出现,肯定是为了解决我们在开发中的一些痛点问题。所以,了解DataBinding要解决的问题,能够使我们更深刻地理解DataBinding技术的设计实现。

    从开发角度看,简言之,DataBinding主要解决了两个问题:

    • 需要多次使用findViewById,损害了应用性能且令人厌烦

    • 更新UI数据需切换至UI线程,将数据分解映射到各个view比较麻烦

    应该说,针对上述问题,都有第三方解决方案。第一个问题可以使用Jake Wharton 的ButterKnife;对于第二个问题,谷歌提供了Loop-Handler方案,你还可以使用RxJava,EventBus等方案,但它们只是解决了线程切换的问题,却没有解决将数据分解映射到各个view的问题,这正是DataBinding的魅力所在!同时,DataBinding的线程切换也是透明的,这是指,当你的Activity需要展示新的数据时,你可以在后台线程中获取数据,然后直接交给DataBinding就可以了,完全不需要关心线程切换的问题。

    DataBinding如何解决这些问题?


    总体思路


    DataBinding解决这些问题的思路非常简单。就是针对每个Activity的布局,在编译阶段,生成一个ViewDataBinding类的对象,该对象持有Activity要展示的数据和布局中的各个view的引用(这里已经解决了令人厌烦的findViewById问题)。同时该对象还有如下可喜的功能:

    • 将数据分解到各个view

    • 在UI线程上更新数据

    • 监控数据的变化,实时更新

    有了这些功能,你会感觉到,你要展示的数据已经和展示它的布局紧紧绑定在了一起,这就是该技术叫做DataBinding的原因。

    实现细节


    下面,我们深入DataBinding的内部,看看它是如何实现以上所说的功能的。
    如何设置使用DataBinding在此就不赘述了,网上大把大把的资料。
    示范项目基本情况:

    • 项目名称为 DataBindingTest
    • 项目包名 com.like4hub.www.databindingtest
    • 项目只有一个主Activity,名称为MainActivity,其布局文件为activity_main.xml
    • 项目用到的图片资源有两个,如下:
    avatar_pure.jpg avatar_sexy.jpg
    • 项目中要展示的数据是User, 其代码如下:
    package com.like4hub.www.databindingtest;
    public class User {    
            private String firstName ;  
            private String  lastName ;   
             private String    avatar ;    
            public User(String avatar, String firstName, String lastName) {  
                      this.avatar = avatar;       
                      this.firstName = firstName;    
                       this.lastName = lastName;   
             }   
    
              public String getAvatar() {    return avatar;    }   
              public void setAvatar(String avatar) {     this.avatar = avatar;    }    
    
              public String getFirstName() {        return firstName;    }  
              public void setFirstName(String firstName) {  this.firstName =firstName;    }  
    
              public String getLastName() {        return lastName;    }  
              public void setLastName(String lastName) {  this.lastName = lastName;    }
    }
    

    有了以上的准备工作,我们可以开始了。
    首先创建如下一个布局:

    <layout  xmlns:android="http://schemas.android.com/apk/res/android"    >   
             <data>    
                <variable   name="user"      
                            type="com.like4hub.www.databindingtest.User"/>    
             </data>
    <LinearLayout    android:layout_width="match_parent" 
               android:layout_height="match_parent"  
                android:orientation="vertical"   
               android:paddingBottom="16dp"  
                android:paddingLeft="16dp"  
                android:paddingRight="16dp"   
               android:paddingTop="16dp"    >  
          <TextView android:id="@+id/firstname"       
                 android:layout_width="wrap_content"   
                 android:layout_height="wrap_content"    
                  android:text="@{user.firstName}" />  
          <TextView android:id="@+id/lastname"        
                  android:layout_width="wrap_content"   
                   android:layout_height="wrap_content"      
                  android:layout_marginTop="16dp"     
                   android:text="@{user.lastName}" />      
          <ImageView  android:layout_width="match_parent"        
                    android:layout_height="wrap_content"        
                    android:layout_marginTop="16dp"        
                  android:src="@{@drawable/avatar_pure}"  />   
           <Button android:id="@+id/button"       
                  android:layout_width="match_parent"      
                  android:layout_height="wrap_content"     
                 android:layout_marginTop="16dp"        
                android:background="@color/orange"  
                android:text="Test" />   
         </LinearLayout>
    </layout>
    

    我们看到,使用DataBinding需要遵照一定的模板去写布局文件,这个模板如下:

    <layout  xmlns:android="http://schemas.android.com/apk/res/android"    >   
             <data>    
                    <!--此处定义该布局要用到的数据的名称及类型-->
             </data>
             <!--此处按照常规方式定义要使用的布局,其中可以使用binding表达式代表属性值,所谓binding表达式,指形如"@{user.firstName}"的表达式-->
    </layout>
    

    我们的Activity onCreate()方法是这样的:

    
    protected void onCreate(Bundle savedInstanceState) {    
            super.onCreate(savedInstanceState);    
    
      ActivityMainBinding binding =  DataBindingUtil.setContentView(this,R.layout.activity_main);  
      User user = new User(null, "万","人迷" );    
      binding.setUser(user);
    }
    

    然后运行我们的程序,结果如下:

    程序截图.png

    那么问题来了,DataBinding究竟在背后做了什么?下面,我们就分步骤进行讲解。

    • 一、对布局文件进行预处理

    首先,DataBinding会对根元素为<layout>的布局文件进行预处理(本例中即activity_main.xml),处理后,原布局文件会变成这个样子:

    <LinearLayout    android:layout_width="match_parent" 
               android:layout_height="match_parent"  
                android:orientation="vertical"   
               android:paddingBottom="16dp"  
                android:paddingLeft="16dp"  
                android:paddingRight="16dp"   
               android:paddingTop="16dp" 
              android:tag="layout/activity_main_0"
                xmlns:android="http://schemas.android.com/apk/res/android"  >  
          <TextView android:id="@+id/firstname"       
                 android:layout_width="wrap_content"   
                 android:layout_height="wrap_content"    
                 android:tag="binding_1"  />  
          <TextView android:id="@+id/lastname"        
                  android:layout_width="wrap_content"   
                   android:layout_height="wrap_content"      
                  android:layout_marginTop="16dp"     
                 android:tag="binding_2" />      
          <ImageView  android:layout_width="match_parent"        
                    android:layout_height="wrap_content"        
                    android:layout_marginTop="16dp"        
                 android:tag="binding_3"  />   
           <Button android:id="@+id/button"       
                  android:layout_width="match_parent"      
                  android:layout_height="wrap_content"     
                 android:layout_marginTop="16dp"        
                android:background="@color/orange"  
                android:text="Test" />   
         </LinearLayout>
    

    我们看到,根元素LinearLayout和那些在属性中使用了binding表达式的view都被设置了Tag,而原有的<layout>标签、data标签以及里面的variable标签,还有各个view中的binding表达式都不见了!!

    DataBinding将它们藏在哪儿了呢?答案是:DataBinding把最初布局文件中的<data>以及各个view中的binding表达式内容抽取出来,生成了一个名为activtiy_main-layout.xml文件,该文件主要内容如下:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <Layout layout="activity_main" 
    modulePackage="com.like4hub.www.databindingtest" 
    >
    
    <Variables declared="true" type="com.like4hub.www.databindingtest.User" name="user">
    </Variables>
    
    <Targets>
    
        <Target tag="layout/activity_main_0" view="LinearLayout">
        </Target>
    
        <Target id="@+id/firstname" tag="binding_1" view="TextView">      
                <Expression text="user.firstName" attribute="android:text"/>
        </Target>
    
        <Target id="@+id/lastname" tag="binding_2" view="TextView">    
                  <Expression text="user.lastName" attribute="android:text"/>
         </Target>
    
         <Target tag="binding_3" view="ImageView">
                <Expression text="@drawable/avatar_pure" attribute="android:src"/>
        </Target>
    </Targets>
    </Layout>
    

    通过给原有布局文件中的view设置Tag和在生成的文件中(本例中即activtiy_main-layout.xml)使用Tag,使得抽取出来的内容能够与其原先所在的位置对应起来。如下图所示:

    映射图.png

    这里有几点需要注意:

    1、LinearLayout设置的Tag是以layout开头的,表示它是根布局。
    2、最初布局文件<data>标签中的内容几乎原封不动的挪到了新生成的文件中。

    • ** 二、生成ActivityMainBinding与BR类**

    现在,DataBinding将会依据上面两个xml文件(即activtiy_main.xml和activtiy_main-layout.xml)生成两个类,一个类是ActivityMainBinding,它继承自ViewDataBinding,里面包含如下fields:

    // views
    public final android.widget.Button button;
    public final android.widget.TextView firstname;
    public final android.widget.TextView lastname;
    private final android.widget.LinearLayout mboundView0;
    private final android.widget.ImageView mboundView3;
    // variables
    private com.like4hub.www.databindingtest.User mUser;
    

    观察这些fields,我们可以发现:
    对应每个variable标签,ActivityMainBinding都有一个相应的变量,在本例中就是上面的mUser变量。

    对应每一个有id的View,都会有一个以其id为名的public final变量,其类型正是该View的类型(如button,firstname)。

    对应每一个没有id但是处理中添加了Tag 的View,都会有一个private final的变量与其对应,名字没有什么特殊的含义(如mboundView0,mboundView3)。

    生成的BR类的内容非常简单,如下:

    package com.like4hub.www.databindingtest;
    public class BR {     
       public static final int _all = 0;     
       public static final int user = 1;
    }
    

    其中的_all变量是默认生成的,user变量是对应ActivityMainBinding类中的mUser变量的。举例来讲,假如我们有一个ActivityMainBinding类的实例对象amb,我们可以调用amb.setVariable(BR.user, userInstance),该调用将会把userInstance赋值给amb的mUser变量。下面是setVariable方法的代码:

    public boolean setVariable(int variableId, Object variable) {    
              switch(variableId) {     
                   case BR.user :  setUser((com.like4hub.www.databindingtest.User)
                   variable);          
              return true;   
               }   
              return false;
    }
    

    那么,DataBinding是否仅仅只给<data>标签中的每一个variable生成对应的BR常量,答案是:NO。
    如果你在User类中的getAvatar方法上添加@Bindable注解,并且让User类继承BaserObservable那么,DataBinding生成的BR类中将会是这样:

    public class BR {        
        public static final int _all = 0;    
        public static final int avatar = 1;      
        public static final int user = 2;
    }
    

    实际上,BR中的常量是一种标识符,它对应一个会发生变化的数据,当数据改变后,你可以用该标识符通知DataBinding,很快,DataBinding就会用新的数据去更新UI。

    那么,DataBinding如何知道哪些数据会变化呢?目前,我们可以确定,<data>中的每一个variable是会变化的,所以DataBinding会为它们生成BR标识符。用@Bindable 注解的类中的getXXX方法(该类父类为BaseObservable或者实现Observable接口)对应一个会变化的数据,DataBinding也会为它们生成BR标识符。实际上,还有第三种,暂且按下不表。

    • 三、生成ActivityMainBinding实例并绑定

    在这一步中,主要有三个过程:

    第一步就是Inflate 处理后的布局文件,由于现在activity_main.xml文件与普通的layout文件一样。现在DataBindingUtil将会Inflate activity_main.xml文件,得到一个ViewGroup变量root。

    第二步就是生成ActivityMainBinding实例对象,DataBindingUtil会将这个变量root传递给ActivityMainBinding的构造方法,生成一个ActivityMainBinding的实例,就是我们在onCreate方法中获取的binding对象。下面看看ActivityMainBinding的构造过程,它的构造方法签名如下:
    public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root)

    其中第二个参数就是刚刚生成的ViewGroup root。你可能想知道第一个参数bindingComponent哪来的,简单一句话,是从DataBindingUtil的getDefaultComponent调用中得来的。如果你之前学习过DataBinding,并且使用过BindingAdapter的话,你应该会比较熟悉它,这里不展开讲。

    好,让我们继续构造我们的ActivityMainBinding对象。
    在构造方法中,ActivityMainBinding会首先遍历root,根据各个View的Tag或者id,初始化自己的fields,就是下面这些:

    public final android.widget.Button button;
    public final android.widget.TextView firstname;
    public final android.widget.TextView lastname;
    private final android.widget.LinearLayout mboundView0;
    private final android.widget.ImageView mboundView3;
    

    至此,Tag们的历史使命完成了,ActivityMainBinding将会把之前加到各个View上的Tags清空。
    最后,构造方法调用invalidateAll引发数据绑定。

    第三步就是进行数据绑定
    在这一步中,ActivityMainBinding将会计算各个view上的binding表达式,然后赋值给view相应的属性。绑定的主要代码如下(省略部分细节):

    @Override
    protected void executeBindings() {   
    
     java.lang.String firstNameUser = null;  
      java.lang.String lastNameUser = null;   
     
    com.like4hub.www.databindingtest.User user = mUser;  
    
    
           if (user != null) {          
              // read user.firstName       
               firstNameUser = user.getFirstName();         
               // read user.lastName           
               lastNameUser = user.getLastName();    
            }  
         
            TextViewBindingAdapter.setText(this.firstname, firstNameUser);        
            TextViewBindingAdapter.setText(this.lastname, lastNameUser);   
    
            ImageViewBindingAdapter.setImageDrawable(this.mboundView3, 
            getDrawableFromResource(R.drawable.avatar_pure));   
    
    }
    

    下面我们来分析上面的数据绑定过程。首先,针对两个binding表达式
    user.firstname 和 user.lastname ,ActivityMainBinding生成了两个临时变量,即:

    java.lang.String firstNameUser = null;
    java.lang.String lastNameUser = null;

    从中我们可以看出这两个变量的命名的规律。这两个变量就代表了两个binding表达式的值,为它们赋值的过程实际上就是binding表达式求值的过程

    ActivityMainBinding通过调用mUser的getFirstName和getLastName方法为上面两个变量赋值。

    请思考,ActivityMainBinding是怎么知道调用mUser的getXXX方法为binding表达式求值的?
    这个问题可以分成两步:

    首先,在构建ActivityMainBinding类时,会对activtiy_main-layout.xml中的数据进行分析,我们再次贴出该文件的内容,以便继续:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <Layout layout="activity_main" 
    modulePackage="com.like4hub.www.databindingtest" 
    >
    
    <Variables declared="true" type="com.like4hub.www.databindingtest.User" name="user">
    </Variables>
    
    <Targets>
    
        <Target tag="layout/activity_main_0" view="LinearLayout">
        </Target>
    
        <Target id="@+id/firstname" tag="binding_1" view="TextView">      
                <Expression text="user.firstName" attribute="android:text"/>
        </Target>
    
        <Target id="@+id/lastname" tag="binding_2" view="TextView">    
                  <Expression text="user.lastName" attribute="android:text"/>
         </Target>
    
         <Target tag="binding_3" view="ImageView">
                <Expression text="@drawable/avatar_pure" attribute="android:src"/>
        </Target>
    </Targets>
    </Layout>
    

    DataBinding发现,有一个variable名为user,所以它为ActivityMainBinding生成了一个mUser变量,DataBinding进一步检查该文件发现,两个binding表达式user.firstName和user.lastName圆点前面的字符串也是user,由此知道,这两个表达式的值来自mUser。

    接着,DataBinding再次进行分析,两个binding表达式圆点后的字符串分别是firstName和lastName,所以DataBinding决定调用mUser的getFirstName和getLastName方法。

    请注意,让User类中包含这两个方法是我们开发者的责任。
    求出值之后就是设置了,比较简单。

    在这里我们可以清楚地看到,binding表达式user.firstName和user.lastName并不是对应着User类中的两个fields,它们实际对应的是User类的两个get方法。

    至此,你可以大胆猜测一下,如果我们给User类添加一个如下方法:

    public String getAlias(){
    return "Alias";
    }
    

    但是我们并不给它添加一个String 类型的alias field,我们是否可以在binding表达式中这样写:@{user.alias}。

    答案是:YES YOU CAN!

    进一步你可以理解,上文中,我们为什么要将@Bindable注解加到一个get方法上面而不是一个field上面了。

    最后,由于ImagView中的binding表达式本身就是一个值,我们不需要再求值了,直接赋值就是。本文这样做,仅仅是为了说明,DataBinding为View添加tag的规则是该View的属性中有没有使用binding表达式。

    好了,至此,我们分析DataBinding工作的核心原理,还有三个内容没有涉及,一个是数据更新(仅略提了一下),另一个是BindingAdapter(其实在executeBindings方法中已经看到它们的身影了),最后一个是事件监听绑定(这个很简单)。其实,掌握了这些核心原理,剩下的内容你可以很轻松地掌握。

    相关文章

      网友评论

      • 望北8261:我只想知道 对布局文件进行预处理 是怎么实现的,我自己能不能实现这个过程,怎么获取layout文件
      • 码途有道:写的很详细,学习了!想问作者一个问题,如果我使用BindingAdapter来给一个view设置宽度,这个view是不是会被绘制两次?
      • 冉桓彬:标题党
      • 7380545803bf:请问一下,如果需要控件注册onTouchListener,在databinding里面应该怎么写?onclick可以在xml里面加上 android:onclick:{xxx.方法名} 来实现,但是onTouchListener呢,有个需要需要监控控件被按下和抬起时分别有一些其他调用,望解答,谢谢
      • 小伙子_d115:不需要Build.Gradle中加以下代码?
        dataBinding {
        enabled = true
        }
      • 逐风的少年:大神,应该用过glide加载图片吧,glide内部会使用tag,而用binding表达式也用了tag,用glide设置图片就会报错,如果设置之前调用settag(null)可以避免,但是glide加载图片就会闪烁一下,请问如果同时使用这两者,该怎么避免这个问题?
        工程师milter:@逐风的少年 binding只在编译时用tag
      • 情天孽海:请问怎么通过动态给ImageView赋值,我发现databingdingTextView好用,但到imgview就尴尬了,比如后台给我返回的imgurl,这个时候是不是还是要通过findviewbyid获取控件操作?
        工程师milter: @情天孽海 我文章里都讲了,你可以再细看一下。
        情天孽海:@milter 能详细点吗?谢谢!
        工程师milter: @情天孽海 不用,直接用ActivityDataBinding类
      • iceIC:说实话BindingAdapter这个注解现在还是似懂非懂,谷歌了很多,感觉都不能getKey
        工程师milter:@iceIC 我已经写了BindingAdapter的文章了,欢迎阅读讨论。
        工程师milter:@iceIC 回头我写一篇文章,大家交流探讨一下
      • KeiHongChan:android新兵来学习了!

      本文标题:DataBinding实现原理探析

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