安卓源码阅读——01.grade设置binding为true时,xml如何进行映射

目录

[一. gradle中配置了什么](#一. gradle中配置了什么)

[二. 源码代码映射实现](#二. 源码代码映射实现)

[1. 核心入口:parseXml(...)](#1. 核心入口:parseXml(...))

[2. 布局类型判断:parseOriginalXml(...)](#2. 布局类型判断:parseOriginalXml(...))

[3. 解析 节点:parseData(...)](#3. 解析 节点:parseData(...))

[4. 最核心的方法:parseExpressions(...)](#4. 最核心的方法:parseExpressions(...))

[4.1 两类目标元素](#4.1 两类目标元素)

[4.2 为每个元素创建 BindingTargetBundle](#4.2 为每个元素创建 BindingTargetBundle)

[4.3 解析属性中的表达式](#4.3 解析属性中的表达式)

[5. XML 清洗:stripFile(...)](#5. XML 清洗:stripFile(...))

6.几个辅助工具方法

[三. 如何实现在同步时就编译代码的](#三. 如何实现在同步时就编译代码的)

[四. 总结](#四. 总结)


一. gradle中配置了什么

首先,我们想使用ViewBinding,要在gradle中配置值viewBinding { enabled = true },所以我们先从此处入手。点击viewBinding,我们进入了一个类:

Kotlin 复制代码
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.build.gradle.internal.dsl

import com.android.build.api.dsl.BuildFeatures
import com.android.build.api.dsl.ViewBinding
import com.android.build.gradle.api.ViewBindingOptions
import com.android.build.gradle.internal.services.DslServices
import com.android.build.gradle.options.BooleanOption
import java.util.function.Supplier
import javax.inject.Inject

/** DSL object for configuring view binding options.  */
abstract class ViewBindingOptionsImpl @Inject constructor(
    private val features: Supplier<BuildFeatures>,
    private val dslServices: DslServices
) : ViewBindingOptions, ViewBinding {

    override var enable: Boolean
        get() {
            val bool = features.get().viewBinding
            if (bool != null) {
                return bool
            }
            return dslServices.projectOptions[BooleanOption.BUILD_FEATURE_VIEWBINDING]
        }
        set(value) {
            features.get().viewBinding = value
        }

    /** Whether to enable view binding.  */
    override var isEnabled: Boolean
        get() = enable
        set(value) {
            enable = value
        }
}

可以看见它只是保存了DSL的值到类中,映射的代码不在此处。


二. 源码代码映射实现

这映射代码的位置很难找(作者技术还不够),最后是通过网上搜索,知道代码位置是在android/platform/frameworks/data-binding 中。克隆下面仓库到本地,即可查看源码:https://android.googlesource.com/platform/frameworks/data-binding/https://android.googlesource.com/platform/frameworks/data-binding/附,克隆命令:

bash 复制代码
git clone -c http.proxy=http://127.0.0.1:端口 \
            -c https.proxy=http://127.0.0.1:端口 \
            https://android.googlesource.com/platform/frameworks/data-binding

转换类在这个位置:

java 复制代码
    private static ResourceBundle.LayoutFileBundle parseOriginalXml(
            @NonNull final RelativizableFile originalFile, @NonNull final String pkg,
            @NonNull final String encoding, boolean isViewBindingEnabled,
            boolean isDataBindingEnabled)
            throws IOException {
        File original = originalFile.getAbsoluteFile();
        try {
            Scope.enter(new FileScopeProvider() {
                @Override
                public String provideScopeFilePath() {
                    return original.getAbsolutePath();
                }
            });
            final String xmlNoExtension = ParserHelper.stripExtension(original.getName());
            FileInputStream fin = new FileInputStream(original);
            InputStreamReader reader = new InputStreamReader(fin, encoding);
            ANTLRInputStream inputStream = new ANTLRInputStream(reader);
            XMLLexer lexer = new XMLLexer(inputStream);
            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
            XMLParser parser = new XMLParser(tokenStream);
            XMLParser.DocumentContext expr = parser.document();
            XMLParser.ElementContext root = expr.element();
            boolean isBindingData = "layout".equals(root.elmName.getText());

            XMLParser.ElementContext data;
            XMLParser.ElementContext rootView;
            if (isBindingData) {
                if (!isDataBindingEnabled) {
                    L.e(ErrorMessages.FOUND_LAYOUT_BUT_NOT_ENABLED);
                    return null;
                }
                data = getDataNode(root);
                rootView = getViewNode(original, root);
            } else if (isViewBindingEnabled) {
                if ("true".equalsIgnoreCase(attributeMap(root).get("tools:viewBindingIgnore"))) {
                    L.d("Ignoring %s for view binding", originalFile);
                    return null;
                }
                data = null;
                rootView = root;
            } else {
                return null;
            }

            boolean isMerge = "merge".equals(rootView.elmName.getText());
            if (isBindingData && isMerge && !filter(rootView, "include").isEmpty()) {
                L.e(ErrorMessages.INCLUDE_INSIDE_MERGE);
                return null;
            }

            String rootViewType = getViewName(rootView);
            String rootViewId = attributeMap(rootView).get("android:id");
            ResourceBundle.LayoutFileBundle bundle =
                new ResourceBundle.LayoutFileBundle(
                    originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
                    isMerge, isBindingData, rootViewType, rootViewId);

            final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
            parseData(original, data, bundle);
            parseExpressions(newTag, rootView, isMerge, bundle);

            return bundle;
        } finally {
            Scope.exit();
        }
    }

这个类处理的是编译期逻辑 (不是运行期),Gradle 构建时会调用它。它的输入是你的 res/layout/xxx.xml,输出是两样东西:

  1. 一个清洗后的 XML 文件 (去掉了 @{...} 表达式,变成普通布局)

  2. 一个 LayoutFileBundle 对象 (记录了所有需要绑定的视图、表达式、变量等信息,后续用来生成 XXXBinding.java 代码)

1. 核心入口:parseXml(...)

java 复制代码
public static ResourceBundle.LayoutFileBundle parseXml(...)

这是主入口,做三件事:

步骤 方法 作用
1 findEncoding() 检测 XML 文件编码(防止中文乱码)
2 stripFile() 清洗 XML :把 @{...} 等表达式剥离,输出到 build/ 目录
3 parseOriginalXml() 解析原始 XML ,提取 binding 信息,返回 LayoutFileBundle

2. 布局类型判断:parseOriginalXml(...)

这个方法决定你的 XML 到底是 Data Binding 还是 View Binding

java 复制代码
boolean isBindingData = "layout".equals(root.elmName.getText());
根节点 类型 处理逻辑
<layout> Data Binding 必须开启 isDataBindingEnabled,解析 <data> 节点和内部视图
普通视图根节点 View Binding 必须开启 isViewBindingEnabled,且没有 tools:viewBindingIgnore="true"
其他 不处理 直接返回 null

如果是 Data Binding,还会检查 <merge> 标签里不能有 <include>(代码里有这个限制)。

3. 解析 <data> 节点:parseData(...)

如果是 Data Binding 布局,根节点是 <layout>,它的第一个子节点通常是 <data>

XML 复制代码
<<layout>
    <data>
        <import type="java.util.List"/>
        <variable name="user" type="com.example.User"/>
    </data>
    ...
</layout>

这个方法遍历 <data> 内部:

  • <import> :记录类型别名,比如 type="java.util.List",后续代码生成时用来 import

  • <variable> :记录变量名和类型,比如 name="user"type="com.example.User"

  • class 属性:自定义生成的 Binding 类名(默认是布局名转驼峰)

4. 最核心的方法:parseExpressions(...)

它遍历 XML 树,找出所有需要生成绑定代码的视图,分为两类:

4.1 两类目标元素

java 复制代码
List<ElementContext> bindingElements;        // 需要生成绑定字段的
List<ElementContext> otherElementsWithIds;  // 只有 id,没有表达式的

bindingElements 包括:

  • 根视图本身

  • <merge> 标签的直接子视图

  • 带有 @{...}@={...} 表达式的视图

  • 包含 <include> 子标签的视图

  • <include> 标签本身

otherElementsWithIds

  • 其他只带了 android:id 但没有绑定表达式的普通视图(View Binding 也需要这些)

4.2 为每个元素创建 BindingTargetBundle

遍历 bindingElements,为每个元素创建绑定目标:

java 复制代码
bundle.createBindingTarget(id, viewName, true, tag, originalTag, ...);

特殊处理:

  • <include> :读取 layout="@layout/xxx",提取被包含的布局名

  • <fragment>:不支持数据绑定表达式,直接跳过

  • 普通视图 :生成一个唯一 tag(如 binding_0),后续生成的 Java 代码会用这个 tag 去 findViewWithTag

4.3 解析属性中的表达式

遍历该元素的所有属性,找 @{...}@={...}

java 复制代码
final boolean isOneWay = value.startsWith("@{");   // 单向绑定
final boolean isTwoWay = value.startsWith("@={");  // 双向绑定

提取花括号内的表达式内容,记录到 BindingTargetBundle 中。后续代码生成器会根据这些表达式生成 setXxx()addOnPropertyChangedCallback() 等代码。

5. XML 清洗:stripFile(...)

为什么需要清洗?因为 Android 系统运行时不认识 @{...} 这种语法,如果直接把带表达式的 XML 打包进 APK,运行时会报错。

java 复制代码
private static void stripFile(...) {
    // 1. 用 XPath 判断是否有 <layout> 根节点
    boolean changed = isBindingLayout(doc, xPath);
    
    // 2. 如果有,调用 XmlEditor.strip() 把 @{...} 等全部删掉
    if (changed) {
        stripBindingTags(xml, out, binderId, encoding);
    } else {
        // 没有表达式就直接复制
        FileUtils.copyFile(xml, out);
    }
}

清洗后的 XML 就是普通的、合法的 Android 布局文件,会被打包进 APK。

6.几个辅助工具方法

方法 作用
findEncoding() UniversalDetector 检测文件编码,默认 UTF-8
attributeMap() 把 XML 属性转成 Map<String, String>,方便读取
escapeQuotes() 去掉属性值前后的引号,并做 XML 转义还原
getViewName() 处理特殊标签:view 标签读 class 属性;include/fragment 返回 android.view.View
filter() / filterNot() 按标签名筛选子元素

三. 如何实现在同步时就编译代码的

这个代码是我们在同步时就进行执行的,这个实现的逻辑是什么呢?下面我们来看它具体的实现:

  1. 执行: ./gradlew assembleDebug

  2. javac扫描 classpath 中的 JAR

  3. 发现META-INF/services/javax.annotation.processing.Processor(下图是这个类的位置)

  4. 找到这个类并示例化

  5. 加载并实例化该类


四. 总结

你的 XML 布局文件

parseXml()

├── 1. findEncoding() ──► 检测编码

├── 2. stripFile() ─────► 清洗 XML(去掉 @{...})──► 输出到 build/intermediates/...

└── 3. parseOriginalXml()

├── 判断类型:DataBinding / ViewBinding / 不处理

├── parseData() ─────► 提取 <import> / <variable>

└── parseExpressions()

├── 收集 bindingElements(有表达式、include、根视图等)

├── 收集 otherElementsWithIds(只带 id 的)

└── 为每个元素创建 BindingTargetBundle(记录 id、tag、表达式)

返回 LayoutFileBundle

后续代码生成器用它生成 XXXBinding.java

相关推荐
_李小白1 小时前
【android opencv学习笔记】Day 26: 滤波算法之低通滤波与图像缩放插值
android·opencv·学习
NiceCloud喜云2 小时前
Claude Code Routines 实战:三种触发器跑通云端自动化编码
android·运维·数据库·人工智能·自动化·json·飞书
我命由我123455 小时前
Bugly - Bugly 基本使用( App 质量追踪平台)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
weiggle5 小时前
第二篇:搭建你的第一个 Compose 项目——开发环境与项目结构
android·前端
阿巴斯甜6 小时前
为什么 AIDL 接口客户端、服务端要写两份一模一样的?
android
weiggle6 小时前
第一篇:Jetpack Compose 宣言——为什么 Android 开发需要声明式 UI
android
城管不管8 小时前
什么是Prompt?
android·java·数据库·语言模型·llm·prompt
weiggle8 小时前
Jetpack Compose 重组机制与性能优化深度剖析
android