ViewBinding 的本质

简介: ViewBinding 的本质

今天我们来深入的了解 ViewBinding 的本质,看看他是怎么生成 ActivityMainBinding 这种文件的。

使用


ViewBinding 目前只支持 as3.6,使用方法很简单,仅仅只需要添加如下代码:


android {
    viewBinding {
        enabled = true
    }
}
复制代码

make project 之后,会在对应的 module 路径:


app/build/generated/data_binding_base_class_source_out/${buildTypes}/out/${包名}/databinding

生成 ViewBinding 文件,为什么我会说 对应的 module ?因为 viewBinding 只对当前设置了 enabled = true 的 module 才会进行处理。


然后来看下处理后的文件:


public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;
  @NonNull
  public final Button tv;
  private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull Button tv) {
    this.rootView = rootView;
    this.tv = tv;
  }
  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }
  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }
  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_main, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }
  @NonNull
  public static ActivityMainBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    String missingId;
    missingId: {
      Button tv = rootView.findViewById(R.id.tv);
      if (tv == null) {
        missingId = "tv";
        break missingId;
      }
      return new ActivityMainBinding((ConstraintLayout) rootView, tv);
    }
    ...
  }
}
复制代码


我们来看看这个文件有哪些信息:


  • R.layout.activity_main 布局文件
  • 布局文件中的 view 控件和 view id
  • 布局文件的 rootView 和类型

接下来,我们会通过源码的方式来跟踪到,这些信息是怎么产生的。

具体使用可以参考文章:[译]深入研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用

准备


由于我们并没有依赖其他 plugin 就可以使用,所以能被直接识别只能是 classpath 依赖的 gradle 了:


classpath 'com.android.tools.build:gradle:3.6.1'

既然 make project 之后就可以看到 ViewBinding 的生成类,那么,我们可以根据 make project 的 build 信息查看做了哪些 task:


> Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
> Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE
> ...
> Task :app:dataBindingGenBaseClassesDebug
复制代码


没有找到 ViewBinding,但找到了 dataBinding,但可以肯定的是,这个 dataBinding 就是生成 ViewBinding 的 task(因为没有其他的 task 带有 binding)。


然后我们可以去 maven 仓库找一下 gradle:3.6.1 ,惊喜的是,gradle:3.6.1 的依赖项有 18 个,第一个就是 Data Binding Compiler Common:


image.png

然后我们进去找到对应的 compiler 3.6.1 版本,通过 gradle 依赖,我们就能看到源了:

compile group: 'androidx.databinding', name: 'databinding-compiler-common', version: '3.6.1'


image.png

可以看到,ViewBinding 是属于 dataBinding 库里面的一个小功能。

阶段一:收集元素


由于我们仅仅只是查看 dataBinding compiler,所以,对于 gradle 调用 compiler 的哪个部分进行联结,我们是查看不到的,但这也不影响我们跟踪源码。


我们直接来看 :


LayoutXmlProcessor.java

public boolean processResources(final ResourceInput input, boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException
{
   ...
   // 文件处理 callback
   ProcessFileCallback callback = new ProcessFileCallback(){
   ...
   // 是否是增量编译
   if (input.isIncremental()) {
       // 增量编译文件处理
       processIncrementalInputFiles(input, callback);
   } else {
       // 全量编译文件处理
       processAllInputFiles(input, callback);
   }
   ...
 }
复制代码


我们直接来看 全量编译文件处理 :


private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback)throws IOException, XPathExpressionException, SAXException, ParserConfigurationException {
        ...
        for (File firstLevel : input.getRootInputFolder().listFiles()) {
            if (firstLevel.isDirectory()) {
                // ①、判断 firstLevel.getName() 的 startWith 是否为 layout
                if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
                    // ②、创建 subPath 
                    callback.processLayoutFolder(firstLevel);
                    // ③、遍历 firstLevel 目录下面的所有文件,满足 toLowerCase().endsWith(".xml");
                    for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
                       // ④、处理布局文件
                        callback.processLayoutFile(xmlFile);
                    }
                } else {
        ...
    }
复制代码


①、判断当前的文件夹的文件名 startWith 是否是 layout


②、会创建一个文件输出目录, 输出目录为 new

File(input.getRootOutputFolder(),file path); 这个 file path 做了与输入目录的 relativize 化,其实,可以理解为,这个输出目录为 输出目录 + file 文件名


③、判断 layout 下面的文件名 endWith 是否是 .xml


④、处理 xml 文件,这个地方也会创建一个输出目录,跟 ② 的方式一样,最终,这个方法会调用到 processSingleFile 方法


然后我们来看下 processSingleFile 方法:


public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException {
     // ①、解析 xml
     final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
         .parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup,
                   isViewBindingEnabled);
     ...
     // ②、缓存起来
     mResourceBundle.addLayoutBundle(bindingLayout, true);
     return true;
}
复制代码


①、这个地方会拿着 xml 文件的路径和输出路径进行解析


②、将解析结果缓存起来


然后来看下 xml 的解析 parseXml


LayoutFileParser.java

@Nullable
public static ResourceBundle.LayoutFileBundle parseXml(@NonNull final RelativizableFile input,
                                                       @NonNull final File outputFile, @NonNull final String pkg,
                                                       @NonNull final LayoutXmlProcessor.OriginalFileLookup originalFileLookup,
                                                       boolean isViewBindingEnabled){
    ...
    return parseOriginalXml(
                RelativizableFile.fromAbsoluteFile(originalFile, input.getBaseDir()),
                pkg, encoding, isViewBindingEnabled);
}
复制代码


parseOriginalXml:


private static ResourceBundle.LayoutFileBundle parseOriginalXml(
            @NonNull final RelativizableFile originalFile, @NonNull final String pkg,
            @NonNull final String encoding, boolean isViewBindingEnabled)
            throws IOException {
          ...
          // ①、是否是 databinding
          if (isBindingData) {
              data = getDataNode(root);
              rootView = getViewNode(original, root); 
          } else if (isViewBindingEnabled) { 
               // ②、viewBinding 是否开启
              data = null;
              rootView = root;// xml 的根元素
          } else {
              return null;
          }
      ...
      // 生成 bundle
      ResourceBundle.LayoutFileBundle bundle =
        new ResourceBundle.LayoutFileBundle(
          originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
          isMerge, isBindingData, getViewName(rootView));
      final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
      // viewBinding 不会 解析 data
      parseData(original, data, bundle);
      // ③、解析表达式
      parseExpressions(newTag, rootView, isMerge, bundle);
      return bundle;
复制代码


①、是否是 databinding,这个的判断依据是,根元素是否是 layout , 获取 data 和 rootView


②、isViewBindingEnable 就是 gradle 设置的 enable = true,根元素就是就是他的 rootView,这个地方要注意的是 data = null,data 数据只有 databinding 才会有的元素,viewBinding 是不会去解析的


③、解析表达式,这里面会循环遍历元素,解析 view 的 id、tag、include、fragment 等等 xml 相关的元素,并且还有 databinding 相关的 @={ 的表达式,最后将结果缓存起来,源码我就补贴了,太多,影响文章


阶段二:写 Layout 文件


LayoutXmlProcessor.java

// xml 的输出目录
public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {
        writeLayoutInfoFiles(xmlOutDir, mFileWriter);
}  
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
        // ①、遍历收集的 layout file
        for (ResourceBundle.LayoutFileBundle layout : mResourceBundle
                .getAllLayoutFileBundlesInSource()) {
            writeXmlFile(writer, xmlOutDir, layout);
        }
        ...
}
private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,ResourceBundle.LayoutFileBundle layout)throws JAXBException {
        // ②、生成文件名
        String filename = generateExportFileName(layout);
        // ③、写文件
        writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
 }
复制代码


①、遍历之前收集到的所有 LayoutFileBundle,写入 xmlOutDir 路径


②、生成 LayoutFileBundle 的文件名,这个文件名最终生成为:


layout.getFileName() + '-' + layout.getDirectory() + ".xml

例如 activity_main.xml,生成的 fileName 为 activity_main-layout.xml

③、将 LayoutFileBundle 转换 xml ,写入文件


由于我们是直接跟踪的 databinding compiler 库,所以无法跟踪到 gradle 是什么联结 compiler 库的,所以,xmlOutDir 我是未知的,也不知道他存到了哪,但没有关系,我们既然知道了生成的文件名规则,我们可以全局搜索该文件,最终,我们在该目录中搜索到:


app/build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main-layout.xml

文件内容如下:


<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="/Users/codelang/project/app/src/main/res/layout/activity_main.xml"
    isBindingData="false"
    isMerge="false" layout="activity_main" modulePackage="com.codelang.viewBinding"
    rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
    <Targets>
        <Target tag="layout/activity_main_0"
            view="androidx.constraintlayout.widget.ConstraintLayout">
            <Expressions />
            <location endLine="31" endOffset="51" startLine="1" startOffset="0" />
        </Target>
        <Target id="@+id/tv" view="Button">
            <Expressions />
            <location endLine="16" endOffset="51" startLine="8" startOffset="4" />
        </Target>
    </Targets>
</Layout>
复制代码


这份 xml 描述了原始 layout 的相关信息,对于 include 和 merge 是怎么关联 tag 的,读者可以自行运行查看

阶段三:写 ViewBinding 类


BaseDataBinder.java

@Suppress("unused")// used by tools
class BaseDataBinder(val input : LayoutInfoInput) {
    init {
        input.filesToConsider.forEach {
            it.inputStream().use {
                // 又将上面收集的 layout,将 xml 转成 LayoutFileBundle
                val bundle = LayoutFileBundle.fromXML(it)
                // 缓存进 ResourceBundle
                resourceBundle.addLayoutBundle(bundle, true)
             }
        }
        ...
    }
复制代码


可以看到,最后又去读之前生成的 layout xml,这个地方为什么会又写又读,而不是直接利用之前 layout 的缓存?我想可能是因为解耦,他们都是独立的 task。


然后来看是如何生成 Binding 类的:


@Suppress("unused")// used by android gradle plugin
fun generateAll(writer : JavaFileWriter) {
   // 拿到所有的 LayoutFileBundle,并根据文件名进行分组排序
   val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
            .groupBy(LayoutFileBundle::getFileName)
   // 遍历 layoutBindings
   layoutBindings.forEach { layoutName, variations ->
       // 将 LayoutFileBundle 信息包装成 BaseLayoutModel
       val layoutModel = BaseLayoutModel(variations)
       val javaFile: JavaFile
       val classInfo: GenClassInfoLog.GenClass
        // 当前是否是 databinding
        if (variations.first().isBindingData) {
              ...
        } else {
          // ①、不是的话,按照 ViewBinding 处理
          val viewBinder = layoutModel.toViewBinder()
          // ②、生成 java file 文件
          javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
          ...
        }
      writer.writeToFile(javaFile)
    ...
 }
复制代码


①、toViewBinder 是 BaseLayoutModel 的拓展函数,他会将 LayoutFileBundle 包装成 ViewBinder 类返回


②、toJavaFile 是 ViewBinder 的拓展函数,该拓展函数在 ViewBinderGenerateSource 类中


ViewBinderGenerateSource.java

// ①、最终会调用到 JavaFileGenerator 的 create 方法
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
    JavaFileGenerator(this, useLegacyAnnotations).create()
private class JavaFileGenerator(
    private val binder: ViewBinder,
    private val useLegacyAnnotations: Boolean) {
    // 最终会调用生成 javaFile 方法,生成的类信息主要看 typeSpec 方法
    fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
        addFileComment("Generated by view binder compiler. Do not edit!")
    }
复制代码
private fun typeSpec() = classSpec(binder.generatedTypeName) {
        addModifiers(PUBLIC, FINAL)
        val viewBindingPackage = if (useLegacyAnnotations) "android" else "androidx"
        addSuperinterface(ClassName.get("$viewBindingPackage.viewbinding", "ViewBinding"))
        // TODO determine if we can elide the separate root field if the root tag has an ID.
        addField(rootViewField())
        addFields(bindingFields())
        addMethod(constructor())
        addMethod(rootViewGetter())
        if (binder.rootNode is RootNode.Merge) {
            addMethod(mergeInflate())
        } else {
            addMethod(oneParamInflate())
            addMethod(threeParamInflate())
        }
        addMethod(bind())
}
复制代码


这个地方就贴 typeSpec 方法了,具体的,大家可以自己去看源码,从 typeSpec 中,我们就可以看到点生成的 ViewBinding 类包含了哪些东西,rootView 字段,inflater 、bind 方法。

目录
相关文章
|
人工智能 自动驾驶 编译器
英伟达发布 Hopper H100 新架构芯片:面向 AI、自动驾驶汽车及 Metaverse 领域
英伟达发布 Hopper H100 新架构芯片:面向 AI、自动驾驶汽车及 Metaverse 领域
1632 0
英伟达发布 Hopper H100 新架构芯片:面向 AI、自动驾驶汽车及 Metaverse 领域
|
6月前
|
数据采集 Web App开发 前端开发
Python爬虫中time.sleep()与动态加载的配合使用
Python爬虫中time.sleep()与动态加载的配合使用
|
关系型数据库 MySQL 数据库连接
解决在eclipse2021中,用mysql-connector-java-8.0.18.jar不兼容,导致无法访问数据库问题
解决在eclipse2021中,用mysql-connector-java-8.0.18.jar不兼容,导致无法访问数据库问题
530 0
|
数据挖掘 大数据 数据处理
数据分析师的秘密武器:精通Pandas DataFrame合并与连接技巧
【8月更文挑战第22天】在数据分析中,Pandas库的DataFrame提供高效的数据合并与连接功能。本文通过实例展示如何按员工ID合并基本信息与薪资信息,并介绍如何基于多列(如员工ID与部门ID)进行更复杂的连接操作。通过调整`merge`函数的`how`参数(如&#39;inner&#39;、&#39;outer&#39;等),可实现不同类型的连接。此外,还介绍了使用`join`方法根据索引快速连接数据,这对于处理大数据集尤其有用。掌握这些技巧能显著提升数据分析的能力。
321 1
|
机器学习/深度学习 算法 PyTorch
PyTorch 模型性能分析和优化 - 第 6 部分
PyTorch 模型性能分析和优化 - 第 6 部分
|
安全 Java 调度
python3多线程实战(python3经典编程案例)
该文章提供了Python3中多线程的应用实例,展示了如何利用Python的threading模块来创建和管理线程,以实现并发执行任务。
406 0
|
缓存 机器人 网络安全
steam报错“您对 CAPTCHA 的响应似乎无效。请在下方重新验证您不是机器人”
你是否满怀期待地准备加入 Steam 的大家庭,却被烦人的 CAPTCHA 验证拦在了门外? 😫 “您对 CAPTCHA 的响应似乎无效。请在下方重新验证您不是机器人。” 这句冰冷的提示,仿佛在嘲笑你的努力,即使反复尝试,错误依然顽固地存在,让人抓狂!🤯 别担心,你不是一个人!很多小伙伴在初次接触 Steam 时,都会遇到这个令人头疼的问题。
|
机器学习/深度学习 设计模式 人工智能
AIGC对设计行业的影响与启发:AIGC设计能替代真正的设计师吗?
AIGC技术正深刻影响设计行业,提升效率、拓宽创意边界,但无法替代设计师的创造力、审美和情感理解。Adobe国际认证成为设计师掌握AIGC技术的起点,推动行业标准化和设计师职业发展。AIGC与设计师的结合将共创设计行业的未来。
|
机器学习/深度学习 存储 计算机视觉
基于YOLOv8深度学习的遥感地理空间物体检测系统【python源码+Pyqt5界面+数据集+训练代码】深度学习实战、目标检测(2)
基于YOLOv8深度学习的遥感地理空间物体检测系统【python源码+Pyqt5界面+数据集+训练代码】深度学习实战、目标检测
|
消息中间件 并行计算 网络协议
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
325 0