DataBinding为null?模块化开发中的DataBinding你需要注意了

Posted open-Xu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DataBinding为null?模块化开发中的DataBinding你需要注意了相关的知识,希望对你有一定的参考价值。

版权声明:本文为openXu原创文章【openXu的博客】,未经博主允许不得以任何形式转载

DataBinding为null?模块化开发中的DataBinding你需要注意了

这几天在新的项目开发中使用DataBinding遇到了一个DataBindingUtil.setContentView()返回null的问题,在解决问题的过程中顺便将DataBinding源码分析了一下,这篇文章中的示例可能有些人看不天明白,因为你没有遇到相同的问题,所以感觉很绕,如果有人遇到相同的问题,希望能帮助到你。如果你只是想了解DataBinding的原理,可以直接看中间的两个内容,分别讲解了DataBinding的使用和源码分析。

1. 使用DataBinding遇到的问题

公司项目在18年就全部重构为基于MVVM的组件化模块化架构,而近期需要开发一个新的项目,使用的就是重构后的框架,在开发过程中遇到一个棘手的问题,某些模块中ViewDataBinding binding = DataBindingUtil.setContentView(this, layoutId)总是返回null,之前开发那么多项目都好好的,怎么就突然出问题了呢?DataBinding相关的辅助类都生成了,为什么就是报空指针呢?

由于DataBinding在出些一些莫名其妙的问题后调试是很麻烦的,最开始排插问题时总是在模块依赖上做了很多尝试,导致浪费了一些时间。后来debug跟踪DataBindingUtil才知道问题所在,由于两个模块的包名相同导致生成的DataBinderMapperImpl类相同,构建时只保留了上级模块的DataBinderMapperImpl,而被依赖模块的DataBinderMapperImpl没有添加到MergedDataBinderMapper的mMappers集合中去,当获取被依赖模块的DataBinding对象时由于mMappers集合没有对应的DataBinderMapperImpl类,所以返回null

上面加粗字体是这个问题的结论,这篇文章就通过这个问题的解决来阐述DataBinding的原理。下面是遇到这个问题的项目框架结构,也就是我们公司使用的框架结构(implementation表示模块依赖关系):


Project
	|
	|--app                  (app文件夹,存放多个app应用模块)
	|   |-- app_operation   (★运营商app,包名为com.hk.operation)
	|   |      implementation project(':template:template_operator')  打包模块依赖应用模板   
	|   |-- app_user        (客户app)
    |
    |--template
    |   |-- template_operator(★运营商模板模块,包名为com.hk.operation)
    |   |         implementation project(':module:module_common')  应用模板依赖业务模块
    |   |-- template_user   (客户模板模块)
    |
    |--module  (业务模块)
    |   |-- module_common    (通用模块,包名com.hk.common)
    |   |-- module_xxx        (其他业务模块....)
    |	
    |--library (组件层)
    |   |--library_core
    |   |--library_xxx
    |   |--...

这个框架一共有4层,可以同时开发多个项目,当然如果要复现上述DataBinding为空的问题只需要3层就可以了,所以你并不需要关心为什么我的框架要这样划分(项目需求)。app文件夹下的模块都是空壳,它是用来打包的,而真正代表应用程序的是template下的模板,比如上面template_operator是运营商的应用模板,它代表代码层真正的应用程序,所以它的包名是应用程序的包名com.hk.operation,而app_operation是用来打包的,它在应用层面上代表运营商的app,所以我给它的包名设成了com.hk.operation,正是由于这两层module的包名一样,导致了module层所有的DataBinding都为null。可能有些同学不太明白,通俗的讲就是A依赖B,B依赖C,A和B的包名相同,导致C中获取DataBinding为null。为什么之前的项目没有这个问题?因为之前项目的template层和app包名就是不一样的,只是时间太长开发新项目时忘记了这个规则。

其实这个问题有两种解决办法:

  • template_operator依赖module_common改用api依赖

这种办法对模块化项目开发来说不太合适,因为app_operation并不关心module_common中的内容,通过api依赖导致module_common暴露给了app_operation,这种方法不推荐

  • template_operator的包名修改一下,不要与app_operation的包名相同

虽然template_operator在代码层面上来说是代表一个完整的app,但是在应用层面来说它不是完整的,它并不能单独打包,所以修改它的包名(把它看作是一个模块而不是应用)是可以理解的,推荐这种方案

接下来我们通过分析DataBinding的源码来看看为什么两个模块包名相同会导致被依赖模块所依赖的其他模块中DataBinding为空的问题。

2. DataBinding库的使用

首先简单介绍一下DataBinding库的使用,以及在本框架中DataBinding辅助类的生成:

2.1 DataBinding的使用

DataBinding

数据绑定库与android Gradle插件捆绑在一起,如果要使用DataBinding库,我们无需声明对此库的依赖项,只需要在module下build.gradle脚本中开启dataBinding。

★注意:即使某个module不需要使用DataBinding,但是它依赖的别的module使用了,也需要在该module中配置数据绑定

android 
	...
	dataBinding 
		enabled = true
	

开启dataBinding后,Android Gradle插件会做下面两件事:

  • 导入DataBinding的依赖库:
androidx.databinding:databinding-adapter 数据绑定适配器
androidx.databinding:databinding-common  Databingding库用到的注解和接口
androidx.databinding:databinding-runtime Databingding运行时库
  • 执行build任务重新构建,会在build目录中自动为包裹的xml布局生成用于访问布局的变量视图的Binding类。类名称基于布局文件的名称转换为驼峰形式并在末尾添加Binding后缀。

2.2 DataBinding库关键类说明

databinding-runtime库中有4个关键的类,分别是DataBinderMapperMergedDataBinderMapperViewDataBindingDataBindingUtil,其包结构和介绍如下:

# androidx.databinding包

DataBinderMapper                 对应每个module,包括app模块
	  |--MergedDataBinderMapper    对应整个应用程序app模块

ViewDataBinding                  对应每个<layout>布局文件
    |--布局名称驼峰+Binding      
             |--布局名称驼峰+BindingImpl

DataBindingUtil                 DataBinding库工具,用于绑定布局文件到Activity或者Fragment   
  • DataBinderMapper:故名思意Mapper就是映射,DataBinderMapper就是保存了布局layoutId和对应的ViewDataBinding的映射关系

  • MergedDataBinderMapper:它是DataBinderMapper的子类,Merged的意思就是合并,由于项目中有多个模块,就会对应多个DataBinderMapper,MergedDataBinderMapper会将所有的DataBinderMapper对象合并保存到它的List<DataBinderMapper> mMappers集合中

  • ViewDataBinding:对应每个被包裹的布局,Android Gradle插件会为每个布局生成一个ViewDataBinding的子类,DataBinderMapper中的映射就是保存layoutId和ViewDataBinding子类的映射关系

  • DataBindingUtil:提供了绑定布局到到Activity或者Fragment的方法,用于获取布局对应的ViewDataBinding对象

2.3 问题框架中BinderMapper类说明


Project
	|
	|--app                 
	|   |-- app_operation   (★运营商app,包名为com.hk.operation)
	|   |     androidx.databinding.DataBinderMapperImpl   
	|   |	  com.hk.operator.DataBinderMapperImpl	
    |
    |--template
    |   |-- template_operator(★运营商模板模块,包名为com.hk.operation)
    |   |     com.hk.operator.DataBinderMapperImpl     
    |   |     
    |
    |--module  (业务模块)
    |   |-- module_common    (通用模块,包名com.hk.common)
    |         com.fpc.common.DataBinderMapperImpl
    

每个module会生成一个DataBinderMapper的子类DataBinderMapperImpl,如果每个module包名不一样,这些类都会被打包进apk中,而app_operationtemplate_operator的包名相同,导致生成的DataBinderMapperImpl的包路径相同,为了打包是不发生错误,所以在构建时只会保留其中一个,而另一个就被舍弃了。app_operation由于是打包模块,会为它生成一个MergedDataBinderMapper的子类androidx.databinding.DataBinderMapperImpl,它代表合并整合所有依赖模块的DataBinderMapperImpl

3. 运行时DataBindingUtil解析

3.1 DataBindingUtil

binding = DataBindingUtil.setContentView(this, getContentView(savedInstanceState));

binding = DataBindingUtil.inflate(inflater, layoutId, container, false);
public class DataBindingUtil 
	//androidx.databinding.DataBinderMapperImpl对象,继承自MergedDataBinderMapper
    private static DataBinderMapper sMapper = new DataBinderMapperImpl();
	...
    /**
     * 最终都会调用bind方法获取一个DataBinding的实例对象
     */
    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) 
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    


在开发时,我们使用DataBindingUtil来获取ViewDataBinding对象,不管是setContentView()还是inflate(),最终都会调用DataBindingUtil.bind()sMapper中获取布局id对应的ViewDataBinding对象。sMapper则是代表整个应用程序的MergedDataBinderMapper的子类对象,也就是打包模块app_operation生成的androidx.databinding.DataBinderMapperImpl,它的包名是固定的androidx.databinding,当DataBindingUtil被加载到内存时,就会初始化sMapper对象。接下来我们看看androidx.databinding.DataBinderMapperImpl

3.2 MergedDataBinderMapper

app模块下的androidx.databinding.DataBinderMapperImpl继承自androidx.databinding.MergedDataBinderMapper,Merged的意思就是整合,它代表是顶级模块,整个应用程序中只有一个类是继承自它的,它对应整个应用程序,它的构造方法只有一行代码,添加当前app模块的DataBinderMapperImpl对象。而父类中的addMapper()方法是一个递归方法,它里面会调用DataBinderMapper.collectDependencies()获取当前Mapper所依赖的其他Mapper添加到mMappers中,这样整个项目所有模块的Mapper对象都会保存在MergedMapper的mMappers集合中。

同时MergedDataBinderMapper也是DataBinderMapper的子类,它有点类似于装饰者模式,保存了所有模块的DataBinderMapper对象,getDataBinder()就是遍历所有模块的DataBinderMapper对象去获取对应的Binding类。

//文件位置:app_xxx/build/generated/source/kapt/debug/androidx.databinding.DataBinderMapperImpl
public class DataBinderMapperImpl extends MergedDataBinderMapper 
  DataBinderMapperImpl() 
    addMapper(new com.hk.operator.DataBinderMapperImpl());
  

  //addMapper()、getDataBinder()方法是父类MergedDataBinderMapper中的,贴到这里方便查看
  private List<DataBinderMapper> mMappers = new CopyOnWriteArrayList<>();
  public void addMapper(DataBinderMapper mapper) 
    Class<? extends DataBinderMapper> mapperClass = mapper.getClass();
    if (mExistingMappers.add(mapperClass)) 
        mMappers.add(mapper);
        //获取模块所依赖的其他模块的Mapper对象
        final List<DataBinderMapper> dependencies = mapper.collectDependencies();
        for(DataBinderMapper dependency : dependencies) 
            addMapper(dependency);
        
    
    @Override
    public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId) 
    	//重写了DataBinderMapper,遍历从所有mMappers中查找对应的Binding对象
        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;
    

3.3 DataBinderMapper

每个模块都会生成一个DataBinderMapper的子类,它对应单个的模块。它有两个作用:

  • 维护了当前module所依赖的module对应的DataBinderMapper集合
  • 重写getDataBinder()方法获取当前模块中的DataBinding对象
//文件位置:xxx/build/generated/source/kapt/debug/module包名.DataBinderMapperImpl
public class DataBinderMapperImpl extends DataBinderMapper 
  private static final int LAYOUT_ACTIVITYMAINFRAGMENT = 1;
  private static final int LAYOUT_OPERATORACTIVITYMAIN = 2;
  ...

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

  static 
    INTERNAL_LAYOUT_ID_LOOKUP.put(com.hk.operator.R.layout.activity_main_fragment, LAYOUT_ACTIVITYMAINFRAGMENT);
    INTERNAL_LAYOUT_ID_LOOKUP.put(com.hk.operator.R.layout.operator_activity_main, LAYOUT_OPERATORACTIVITYMAIN);
    ...
  
  //★ 从当前模块中获取布局id对应的DataBinding对象
  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) 
  	//根据layoutId获取DataBinding的key
    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) 
      	//创建一个layoutId对应的ViewDataBinding对象,并返回
        case  LAYOUT_ACTIVITYMAINFRAGMENT: 
          if ("layout/activity_main_fragment_0".equals(tag)) 
            return new ActivityMainFragmentBindingImpl(component, view);
          
        
        case  LAYOUT_OPERATORACTIVITYMAIN: 
          if ("layout/operator_activity_main_0".equals(tag)) 
            return new OperatorActivityMainBindingImpl(component, view);
          
        
       ...
      
    
    return null;
  
  ...

  //★ 获取当前模块依赖的其他模块对应的DataBinderMapper对象集合
  @Override
  public List<DataBinderMapper> collectDependencies() 
    ArrayList<DataBinderMapper> result = new ArrayList<DataBinderMapper>(9);
    //获取当前模块所依赖的模块对应的DataBinderMapper对象
    result.add(new androidx.databinding.library.baseAdapters.DataBinderMapperImpl());
    result.add(new com.fpc.core.DataBinderMapperImpl());
    result.add(new com.fpc.libs.DataBinderMapperImpl());
    ...
    result.add(new com.hk.user.home.DataBinderMapperImpl());
    return result;
  



3.4 ViewDataBinding

databinding插件会在构建时为每个最外层为<layout>的布局文件生成一个java文件存放在module\\build\\generated\\data_binding_base_class_source_out\\debug\\dataBindingGenBaseClassesDebug\\out\\包名\\databinding\\xxxBiding.java,这些java类继承自androidx.databinding.ViewDataBinding,它们分别对应单个布局文件。ViewDataBinding维护了布局中设置了id的所有View的实例,和data的实例。

public abstract class UserFragmentMainHomeBinding extends ViewDataBinding 
  @NonNull
  public final AppBarLayout appBarLayout;

  @NonNull
  public final ImageView ivSetting;

  @NonNull
  public final RelativeLayout rlTitle;

  @NonNull
  public final TabLayout tabLayout;

  @NonNull
  public final LinearLayout tabStatusbar;

  @NonNull
  public final ViewPager viewpager;

  protected UserFragmentMainHomeBinding(DataBindingComponent _bindingComponent, View _root,
      int _localFieldCount, AppBarLayout appBarLayout, ImageView ivSetting, RelativeLayout rlTitle,
      TabLayout tabLayout, LinearLayout tabStatusbar, ViewPager viewpager) 
    super(_bindingComponent, _root, _localFieldCount);
    this.appBarLayout = appBarLayout;
    this.ivSetting = ivSetting;
    this.rlTitle = rlTitle;
    this.tabLayout = tabLayout;
    this.tabStatusbar = tabStatusbar;
    this.viewpager = viewpager;
  

  ...

3.5 BindingImpl

上述ViewDataBinding的子类的实现类,用于实现布局data变量的设置,文件位置:xxx\\build\\generated\\source\\kapt\\debug\\包路径\\databinding\\XxxBindingImpl.java。关于单个布局文件的ViewDataBinding这里就不作过多解析了,非常简单看看其源码就能理解

3.6 总结

DataBindingUtil类被加载到内存中时会完成如下工作:

  1. 初始化顶级MergedDataBinderMapper的子类androidx.databinding.DataBinderMapperImpl对象sMapper
  2. 顶级MergedMapper的构造方法中调用addMapper()添加app模块的DataBinderMapper对象,addMapper()是一个递归方法,它会将当前模块所依赖的其他模块的DataBinderMapper也添加进来,addMapper()方法执行完成后,整个工程所有模块的DataBinderMapper对象都会保存在顶级MergedMapper的mMappers集合中

通过DataBindingUtil获取DataBinding对象:

  1. 通过DataBindingUtil获取DataBinding对象最终都会调用它的bind(int layoutId)方法,而bind方法会调用顶级MergedDataBinderMapper对象sMappergetDataBinder()方法
  2. MergedDataBinderMapper重写了父类DataBinderMappergetDataBinder()方法,遍历调用mMappers集合中所有DataBinderMapper对象的getDataBinder()方法
  3. mMappers集合存放的就是所有模块的DataBinderMapper对象,如果某个模块中存在layoutId对应的DataBinding类,getDataBinder()方法就会new一个其对象并返回
//相关类结构,这里就没画类图了,对着上面的结论看
DataBindingUtil
    private static MergedDataBinderMapper sMapper = new DataBinderMapperImpl();
    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,int layoutId) 

DataBinderMapper
    public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId) 

顶级MergedDataBinderMapper extends DataBinderMapper 
    private List<DataBinderMapper> mMappers = new CopyOnWriteArrayList<>();
    public void addMapper(DataBinderMapper mapper) 
    public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId) 

module下的DataBinderMapperImpl extends DataBinderMapper

4. 模块化开发时,DataBinding为null的问题

上面我们通过分析DataBinding的源码了解了其工作机制,返回到文章开头的问题,为什么会出现DataBinding为null?

如果两个模块的包名相同,生成的DataBinderMapper的类名(带包名)也是一样的,在构建时会自动检测有没有相同的类,如果有就会对其进行合并,怎样进行合并呢?

比如示例中app_operation模块会生成一个com.hk.operator.DataBinderMapperImpl类,由于app_operation依赖了template_operator,所以app_operation的DataBinderMapperImpl伪代码如下:

package com.hk.operator;
public class DataBinderMapperImpl extends DataBinderMapper 
  //由于app_operation中并没有layout布局,所以getDataBinder()方法是空的
  static 
  

  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) 
    return null;
  
  
  @Override
  public List<DataBinderMapper> collectDependencies() 
    ArrayList<DataBinderMapper> result = new ArrayList<DataBinderMapper>(8);
    result.add(new androidx.databinding.library.baseAdapters.DataBinderMapperImpl());
    //app_operation依赖了template_operator,将template_operator的DataBinderMapper实例添加到依赖集合中
    result.add(new com.hk.operator.DataBinderMapperImpl());
    return result;
  

这个伪代码在执行时会掉入递归死循环,因为顶级MergedDataBinderMapperaddMapper()方法在添加app_operation的DataBinderMapper也就是com.hk.operator.DataBinderMapperImpl时,会调用其collectDependencies()添加其依赖的DataBinderMapper,上述伪代码中app_operation依赖了template_operator,由于包名相同导致生成的DataBinderMapperImpl是一样的类名,带式apk中只会保留一个DataBinderMapperImpl类。就会产生添加A,添加A依赖的A,添加A依赖的A…无限递归,导致栈溢出。

为了避免这个问题,android gradle插件在构建时,会对其进行检查,如果有相同包名的DataBinderMapperImpl就不添加依赖了,伪代码变成下面的样子:

package com.hk.operator;
public class DataBinderMapperImpl extends DataBinderMapper 
  //由于app_operation中并没有layout布局,所以getDataBinder()方法是空的
  static 
  

  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) 
    return null;
  
  
  @Override
  public List<DataBinderMapper> collectDependencies() 
    ArrayList<DataBinderMapper> result = new ArrayList<DataBinderMapper>(8);
    result.add(new androidx.databinding.library.baseAdapters.DataBinderMapperImpl());
    //避免递归死循环,不要添加与本DataBinderMapperImpl类名相同的DataBinderMapperImpl
    //result.add(new com.hk.operator.DataBinderMapperImpl());
    return result;
  

正是由于这个原因,导致template_operator模块和它依赖的其他模块的DataBinderMapperImpl没有添加进去,当获取template_operator所依赖的其他模块的DataBinding时,由于顶级MergedDataBinderMappersMapper中没有对应的rMapper,所以返回null。

既然template_operator的DataBinderMapperImpl没有添加进去,为什么只有获取template_operator依赖的其他模块的DataBinding为null?而获取template_operator模块中的DataBinding不为null?其实在相同类合并时还有一个细节,由于template_operator中是存在layout布局的,所以其DataBinderMapperImpl的getDataBinder()方法不是空的,在合并时,将该方法相关内容搬到了app_operation的DataBinderMapperImpl中,最终形式如下:

package com.hk.operator;
public class DataBinderMapperImpl extends DataBinderMapper 

	@Override
	public List<DataBinderMapper> collectDependencies() 
		ArrayList<DataBinderMapper> result = new ArrayList<DataBinderMapper>(8);
		result.add(new androidx.databinding.library.baseAdapters.DataBinderMapperImpl());
		//避免递归死循环,不要添加与本DataBinderMapperImpl类名相同的DataBinderMapperImpl
		//result.add(new com.hk.operator.DataBinderMapperImpl());
		return result;
	

	//以下内容来自template_operator模块的com.hk.operator.DataBinderMapperImpl
	private static final int LAYOUT_ACTIVITYMAINFRAGMENT = 1;
	private static final int LAYOUT_OPERATORACTIVITYMAIN = 2;
	...
	static 
	INTERNAL_LAYOUT_ID_LOOKUP.put(com.hk.operator.R.layout.activity_main_fragment, LAYOUT_ACTIVITYMAINFRAGMENT);
	INTERNAL_LAYOUT_ID_LOOKUP.put(com.hk.operator.R.layout.operator_activity_main, LAYOUT_OPERATORACTIVITYMAIN);
	...
	

	@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_ACTIVITYMAINFRAGMENT: 
	      if ("layout/activity_main_fragment_0".equals(tag)) 
	        return new ActivityMainFragmentBindingImpl(component, view);
	      
	    
	    case  LAYOUT_OPERATORACTIVITYMAIN: 
	      if ("layout/operator_activity_main_0".equals(tag)) 
	        return new OperatorActivityMainBindingImpl(component, view);
	      
	    
	   ...
	  
	
	return null;
	

这个合并规则只是作者根据实际情况进行的猜测,与构建时的现象是一致的,不代表百分之百正确,但是解释了为什么模块包名相同会导致某些DataBinding为null的问题

以上是关于DataBinding为null?模块化开发中的DataBinding你需要注意了的主要内容,如果未能解决你的问题,请参考以下文章

DataBinding 访问 3

项目引用Kotlin与databinding,ARouter,Architecture Components冲突解决办法

Android JetPack组件之DataBinding的使用详解

视图绑定(ViewBinding )与数据绑定(Databinding)

ViewBinding和DataBinding的理解和区别

使用 Kotlin Multiplatform Mobile (KMM) 的多平台应用程序中的 DataBinding 错误