源码分析Android-Data Bindingdata binding

DataBinding实现原理探析

2016-07-24  本文已影响10170人  工程师milter

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

DataBinding技术能解决什么问题?


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

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

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

DataBinding如何解决这些问题?


总体思路


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

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

实现细节


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

avatar_pure.jpg avatar_sexy.jpg
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>标签中的内容几乎原封不动的挪到了新生成的文件中。

现在,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标识符。实际上,还有第三种,暂且按下不表。

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

第一步就是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方法中已经看到它们的身影了),最后一个是事件监听绑定(这个很简单)。其实,掌握了这些核心原理,剩下的内容你可以很轻松地掌握。

上一篇下一篇

猜你喜欢

热点阅读