DataBinding(三)构建过程分析

2,254 阅读9分钟

前两篇讲了 ViewBinding和DataBinding的使用和绑定原理。但是有一个很重要的问题没有将,就是生成的代码是如何生成的,什么时候生成的?这篇讲的是 XxxXxxBinding.java是如何生成的。

其中用到示例在 BindingSample 可以查到,包括 ProcessDataBinding等代码

DataBinding 包含三篇博客:

DataBinding(含ViewBinding)(一)使用

DataBinding(含ViewBinding)(二)绑定原理

一、Task

// ViewBinding
Task :viewbinding:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
Task :viewbinding:dataBindingMergeGenClassesDebug UP-TO-DATE
Task :viewbinding:dataBindingGenBaseClassesDebug UP-TO-DATE

// DataBinding
Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE
Task :app:dataBindingTriggerDebug UP-TO-DATE
Task :app:dataBindingGenBaseClassesDebug

这个task分别有对应如下:

这是位于 com.android.tools.build:gradle:4.2.0 包下的。在 module 中的 build.gradle中加入如下依赖就可以查看图片中的 task,不过加入这个会导致重复某些类重复依赖。

dependencies {
    //……
    implementation 'com.android.tools.build:gradle:4.2.0'
}

简单看一下task的创建流程

graph TD
AppPlugin --> BasePlugin.apply --> BasePlugin.basePluginApply --> configuratorService.recordBlock --> BasePlugin.createTasks  --> BasePlugin.createAndroidTasks --> TaskManager.createTask --> TaskManager.createTasksForVariant --> ApplicationTaskManager.doCreateTasksForVariant --> AbstractAppTaskManager.createCommonTasks -->  TaskManager.createDataBindingTasksIfNecessary

上线整个流程图是创建DataBinding相关的Task,具体源码就不展示,可以自己看源码,或者参考Android Gradle Plugin源码分析,不过起源码是旧版,已有不少改动。

debug 任务的执行流程

关于 gradle plugin 执行流程可以打断点来查看执行流程。

1. 引入文件
 dependencies {
    //……
    implementation 'com.android.tools.build:gradle:4.2.0'
 }
 
2. 添加断点

如图中在会执行的 ApplicationTaskManager中设置断点。

3. debug

image.png image.png

可以使用图中的两种方式进行调试,左边的是全量调试,右边的是单个task调试,如果左边的调试出现错误就可以用右边的调试,右边task位于 Gradle > Project > moduleName > Tasks > other > xxx。运行debug后就可以进入断点调试。

资源合并流程

资源合并流程图:

graph TD
MergeResources.doIncrementalTaskAction --> ResourceMerger.mergeData
MergeResources.doFullTaskAction --> ResourceMerger.mergeData --> MergedResourceWriter -->  MergeResources.maybeCreateLayoutProcessor --> LayoutXmlProcessor.processSingleFile --> LayoutXmlProcessor.End

执行 dataBindingGenBaseClassesDebugtask 合并 dataBinding资源的流程大体上如上图。其中还有一些是不一样的。

MergedResourceWriter.end()方法中会做如下判断,看是否需要解析dataBinding数据

 if (dataBindingExpressionRemover != null
                            && request.getInputDirectoryName().startsWith("layout")
                            && request.getInputFile().getName().endsWith(".xml")) {
  1. 首先判断 dataBindingExpressionRemover != nullSingleFileProcessor != null转换成 LayoutXmlProcessor != null
  2. request.getInputDirectoryName().startsWith("layout")判断存放文件的文件夹名是不是 layout开始的
  3. 判断文件名是不是已xml结束 如果以上三个条件都满足就去进入 LayoutXmlProcessor进行解析文件,LayoutXmlProcessor.End将 xxx_xxx-layout.xml 写入 Project\module\build\intermediates\data_binding_layout_info_type_merge\debug\out\中

MergeResources.doIncrementalTaskAction增量任务或 MergeResources.doFullTaskAction全量任务中是以for循环的方式合并所有的资源文件。

上面的过程可以通过选择 dataBindingGenBaseClassesDebug右键执行 Debug ModuleProject来查看运行步骤。如果不能debug,则可以将module下面的build文件清空,然后再运行查看 编译步骤或者是生成的 XxxBinding.java 文件

对于资源实时编译的理解

当使用了DataBinding或者是ViewBinding后,只要我们新建或者改变布局xml文件,在kotlin或者java文件中就能实时更新,那么理解是android studio有些文件处理进程一直在运行监测资源文件等的变化,当有布局文件有变化时,其就会将增量或全量变化添加进入AndroidStudio运行的内存中,这样我们就可以实时调用。AndroidStudio文件处理器如下图中的 Filesystem events processor:

image.png

二、生成文件过程

收集元素

在上一节中分析了在 MergeResources中是用for循环来遍历所有的资源文件,最后通过 LayoutXmlProcessor来收集。

LayoutXmlProcessor.java

public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,
        boolean isViewBindingEnabled, boolean isDataBindingEnabled)
        throws ParserConfigurationException, SAXException, XPathExpressionException,
        IOException {
    // 解析xml成 LayoutFileBundle
    final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
            .parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup,
                    isViewBindingEnabled, isDataBindingEnabled);
    if (bindingLayout == null
            || (bindingLayout.isBindingData() && bindingLayout.isEmpty())) {
        return false;
    }
    // 缓存文件
    mResourceBundle.addLayoutBundle(bindingLayout, true);
    return true;
}

1 拿到layout资源文件的路径和输出路径进行解析,如下:

//  RelativizableFile input
absoluteFile = {File@12883} "D:\xxx\module\src\main\res\layout\view_stub.xml"
relativeFile = {File@12884} "module\src\main\res\layout\view_stub.xml"
baseDir = {File@12885} "D:\WorkSpace\BindingSample"

// File output     
D:\xxx\module\build\intermediates\incremental\mergeDebugResources\stripped.dir\layout\view_stub.xml

2 将解析的 bindingLayout 存储

LayoutFileParser.java

@Nullable
public static ResourceBundle.LayoutFileBundle parseXml(@NonNull final RelativizableFile input,
        @NonNull final File outputFile, xxx) {
    ……
    try {
        ……
        // 使用input和output生成 xxx.xml文件,去掉根节点 layout和添加 tag
        stripFile(inputFile, outputFile, encoding, originalFileLookup);
        // 解析xml文件
        return parseOriginalXml(
            RelativizableFile.fromAbsoluteFile(originalFile, input.getBaseDir()),
            pkg, encoding, isViewBindingEnabled, isDataBindingEnabled);
    } finally {
        Scope.exit();
    }
}

1 生成 xxx.xml文件,去掉根节点 layout和添加 tag,并且写入到 output文件中。
2 解析 xml文件

生成 xxx.xml 文件

private static void stripFile(File xml, File out, String encoding,
        LayoutXmlProcessor.OriginalFileLookup originalFileLookup)
        throws ParserConfigurationException, IOException, SAXException,
        XPathExpressionException {

    boolean changed = isBindingLayout(doc, xPath);
    if (changed) {
        stripBindingTags(xml, out, binderId, encoding);
    } else if (!xml.equals(out)){
        FileUtils.copyFile(xml, out);
    }
}

private static void stripBindingTags(File xml, File output, String newTag, String encoding)
        throws IOException {
    String res = XmlEditor.strip(xml, newTag, encoding);
    Preconditions.checkNotNull(res, "layout file should've changed %s", xml.getAbsolutePath());
    if (res != null) {
        L.d("file %s has changed, overwriting %s",
                xml.getAbsolutePath(), output.getAbsolutePath());
        FileUtils.writeStringToFile(output, res, encoding);
    }
}

stringBindingTags去掉 xml文件中的layout等标签,并且添加 android:tag="binding_1",然后将其解析的字符串写入到 output文件中。

解析 xml文件

private static ResourceBundle.LayoutFileBundle parseOriginalXml(
        @NonNull final RelativizableFile originalFile, @NonNull final String pkg,
        @NonNull final String encoding, boolean isViewBindingEnabled,
        boolean isDataBindingEnabled)
        throws IOException {
        ……
        if (isBindingData) {
            // 1 开启了dataBinding
            if (!isDataBindingEnabled) {
                L.e(ErrorMessages.FOUND_LAYOUT_BUT_NOT_ENABLED);
                return null;
            }
            data = getDataNode(root);
            rootView = getViewNode(original, root);
        } else if (isViewBindingEnabled) {
            // 2 开启了 ViewBinding
            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;
        }
        // 3 生成LayoutFileBundle文件
        ResourceBundle.LayoutFileBundle bundle =
            new ResourceBundle.LayoutFileBundle(
                originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
                isMerge, isBindingData, rootViewType, rootViewId);
        // 4 解析数据
        parseData(original, data, bundle);
        // 5 解析表达式
        parseExpressions(newTag, rootView, isMerge, bundle);
        return bundle;
    } finally {
        Scope.exit();
    }
}

1 开启了DataBinding,获取数据,获取根Element
2 开启了ViewBinding,首先判断是否 xml文件是否忽略了ViewBinding,如果 tools:viewBindingIgnore="true"则不解析。根 ElementContext就是 rootView。data = null,data 数据只有 databinding 才会有的元素,viewBinding 是不会去解析的
3 创建 LayoutFileBundle
4 解析 xml文件中的 data标签中的数据,解析 import variable标签和class属性
5 解析表达式,这里面会循环遍历元素,解析 Viewidtagincludefragmentbinding_id 等元素,并且还有 databinding 相关的 @{@={ 属性表达式
从中可以注意到 DataBinding和 ViewBinging是可以同时设置,属性表达式有两种表现形式。

生成 xxx_xxx-layout.xml文件

MergeResources

@Override
public void end() throws JAXBException {
    getProcessor() // LayoutXmlProcessor
            .writeLayoutInfoFiles(
                    getDataBindingLayoutInfoOutFolder().get().getAsFile());
}

LayoutXmlProcessor.java

public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {
    writeLayoutInfoFiles(xmlOutDir, mFileWriter);
}

public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
    // For each layout file, generate a corresponding layout info 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());
}

从代码中可以看到遍历 mResourceBundle文件,即上一节中存起来的 LayoutFileBundle文件,然后写入文件 module\build\intermediates\data_binding_layout_info_type_merge\debug\out\xxx_xxx-layout.xml

生成 XxxXxxBinding.java 文件

上面的两步完成后,会执行 DataBindingGenBaseClassesTask.writeBaseClasses生成Java文件

@TaskAction
fun writeBaseClasses(inputs: IncrementalTaskInputs) {
    // TODO extend NewIncrementalTask when moved to new API so that we can remove the manual call to recordTaskAction

    recordTaskAction(analyticsService.get()) {
        val args = buildInputArgs(inputs)
        CodeGenerator(
            args,
            sourceOutFolder.get().asFile,
            Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
            encodeErrors).run()
    }
}

class CodeGenerator @Inject constructor(
    val args: LayoutInfoInput.Args,
    private val sourceOutFolder: File,
    private val logger: Logger,
    private val encodeErrors: Boolean
) : Runnable, Serializable {
    override fun run() {
        try {
            initLogger()
            BaseDataBinder(LayoutInfoInput(args))
                .generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath))
        } finally {
            clearLogger()
        }
    }
}

BaseDataBinder

class BaseDataBinder(val input : LayoutInfoInput) {
    private val resourceBundle : ResourceBundle = ResourceBundle(
            input.packageName, input.args.useAndroidX)
    init {
        // 1 将之前生成 xxx_xxx-layout.xml 重新生成 LayoutFileBundle
        input.filesToConsider
                .forEach {
                    it.inputStream().use {
                        val bundle = LayoutFileBundle.fromXML(it)
                        resourceBundle.addLayoutBundle(bundle, true)
                    }
                }
        resourceBundle.addDependencyLayouts(input.existingBindingClasses)
        resourceBundle.validateAndRegisterErrors()
    }
    
    @Suppress("unused")// used by android gradle plugin
    fun generateAll(writer : JavaFileWriter) {
        // 2 拿到所有的 LayoutFileBundle,并根据文件名进行分组排序
        val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
            .groupBy(LayoutFileBundle::getFileName)
        // 3 遍历 layoutBindings
        layoutBindings.forEach { layoutName, variations ->
            // 将 LayoutFileBundle 信息包装成 BaseLayoutModel
            val layoutModel = BaseLayoutModel(variations)

            val javaFile: JavaFile
            val classInfo: GenClassInfoLog.GenClass
            if (variations.first().isBindingData) {
                check(input.args.enableDataBinding) {
                    "Data binding is not enabled but found data binding layouts: $variations"
                }
                // 4.a 生成JavaFile文件
                val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
                javaFile = binderWriter.write()
                classInfo = binderWriter.generateClassInfo()
            } else {
                check(input.args.enableViewBinding) {
                    "View binding is not enabled but found non-data binding layouts: $variations"
                }
                // 4.b 生成JavaFile
                val viewBinder = layoutModel.toViewBinder()
                javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
                classInfo = viewBinder.generatedClassInfo()
            }
            // 写文件 GradleFileWriter.writeToFile
            writer.writeToFile(javaFile)
            ……
    }
}

1 将之前生成 xxx_xxx-layout.xml 重新生成 LayoutFileBundle
2 拿到所有的 LayoutFileBundle,并根据文件名进行分组排序
3 遍历 layoutBindings 文件生成javaFile
4 根据文件中的数据生成 javaFile 两者生成方式区别不大

// 4.a 和 4.b 都是调用此方法生成 javaFile,只是调用的类不同而已
fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
    addFileComment("Generated by view binder compiler. Do not edit!")
}

BaseLayoutBinderWriter.kt

private fun createType() = classSpec(binderTypeName) {
    superclass(viewDataBinding)
    addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
    addFields(createBindingTargetFields())
    addFields(createVariableFields())
    addMethod(createConstructor())
    addMethods(createGettersAndSetters())
    addMethods(createStaticInflaters())
}

ViewBinderGenerateJava.kt

fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()

private fun typeSpec() = classSpec(binder.generatedTypeName) {
    addModifiers(PUBLIC, FINAL)

    val viewBindingPackage = if (useLegacyAnnotations) "android" else "androidx"
    addSuperinterface(ClassName.get("$viewBindingPackage.viewbinding", "ViewBinding"))

    // TODO elide the separate root field if the root tag has an ID (and isn't a binder)
    addField(rootViewField())
    addFields(bindingFields())

    addMethod(constructor())
    addMethod(rootViewGetter())

    if (binder.rootNode is RootNode.Merge) {
        addMethod(mergeInflate())
    } else {
        addMethod(oneParamInflate())
        addMethod(threeParamInflate())
    }

    addMethod(bind())
}

这是使用 javapoet 中的 TypeSpec 生成java文件。

ProcessDataBinding 分析

在 DataBinding中还有一个非常重要的类需要分析一下,那就是解析DataBinding处理注解的类ProcessDataBinding,其继承自 AbstractProcessor。其地址为DataBinding,我的示例中的也有 compiler里面就是 ProcessDataBinding

private boolean doProcess(RoundEnvironment roundEnv) {
    if (mProcessingSteps == null) {
        readArguments();
        initProcessingSteps(processingEnv);
    }
    ……
    boolean done = true;
    Context.init(processingEnv, mCompilerArgs);
    for (ProcessingStep step : mProcessingSteps) {
        try {
            done = step.runStep(roundEnv, processingEnv, mCompilerArgs) && done;
        } catch (JAXBException e) {
            L.e(e, "Exception while handling step %s", step);
        }
    }
    if (roundEnv.processingOver()) {
        for (ProcessingStep step : mProcessingSteps) {
            step.onProcessingOver(roundEnv, processingEnv, mCompilerArgs);
        }
    }
    if (roundEnv.processingOver()) {
        Scope.assertNoError();
    }
    return done;
}

private void initProcessingSteps(ProcessingEnvironment processingEnv) {
    final ProcessBindable processBindable = new ProcessBindable();
    mProcessingSteps = Arrays.asList(
            new ProcessMethodAdapters(),
            new ProcessExpressions(),
            processBindable
    );
    ……
}

创建了三个 ProcessingStepProcessMethodAdaptersProcessExpressionsProcessBindable,分别生成对应数据数据。下面来一一查看一下。

ProcessMethodAdapters

@Override
public boolean onHandleStep(RoundEnvironment roundEnv,
                            ProcessingEnvironment processingEnvironment,
                            CompilerArguments args) {
    final ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
    SetterStore store = SetterStore.get();
    clearIncrementalClasses(roundEnv, store);
    
    // 将用@BindingAdapter注释的方法解析,并存储到SetterStore中
    addBindingAdapters(roundEnv, processingEnvironment, store);
    // 将用@BindingMethod注释属性替换的方法解析,并存储到SetterStore中
    addRenamed(roundEnv, store);
    // 将用@BindingConversion注释的方法解析,并存储到SetterStore中
    addConversions(roundEnv, store);
    // 将用@Untaggable注释的类接口等的解析,并存储到SetterStore中
    addUntaggable(roundEnv, store);
    // 将用@InverseBindingAdapter注释的方法等解析,并存储到SetterStore中
    addInverseAdapters(roundEnv, processingEnvironment, store);
    addInverseBindingMethods(roundEnv, store);
    addInverseMethods(roundEnv, processingEnvironment, store);
    ……
    store.write(args.getModulePackage());
    ……
    return true;
}

该类主要是处理 @BindingAdapter@BindingMethods@BindingConversion等注解注解的方法、类,并将解析到的信息存储在 SetterStor中,最终存储在 BindingAdapterStore相应的 TreeMap中。

ProcessExpressions

graph TD
ProcessExpressions.onHandleStep --> ProcessExpressions.createIntermediateFromLayouts -->
ProcessExpressions.writeResourceBundle --> CompilerChef.writeViewBinderInterfaces --> DataBinder.writerBaseClasses --> LayoutBinder.writeViewBinderBaseClass --> LayoutBinderWriter.writeBaseClass
ProcessExpressions.writeResourceBundle --> CompilerChef.writeViewBinders --> DataBinder.writeBinders --> LayoutBinder.writeViewBinder --> LayoutBinderWriter.write

……
loadDependencyIntermediates()
……
IntermediateV2 mine = createIntermediateFromLayouts(args.getLayoutInfoDir(), intermediateList);
if (mine != null) {
    if (!args.isEnableV2()) {
        mine.updateOverridden(resourceBundle);
        intermediateList.add(mine);
        saveIntermediate(args, mine);
    }
    mine.appendTo(resourceBundle, true);
}

这一段代码将 xxx-layoutinfo.bin中的数据转换成 LayoutFileBundle解析里面的layout、import、variables等标签。最后使用 LayoutBinderWriter.writeBaseClassLayoutBinderWriter.write生成 XxxXxxBinding.java 和 XxxXxxBingImpl.java(使用kotlin语言才会生成)。

ProcessBindable

private void generateBRClasses(
        ProcessingEnvironment processingEnv,
        CompilerArguments compilerArgs,
        String pkg) {
    try {
        CompilerArguments.Type artifactType = compilerArgs.getArtifactType();
        ……
        BRWriter brWriter = new BRWriter(useFinal);
        bindableBag.getToBeGenerated().forEach(brWithValues -> {
            String out = brWriter.write(brWithValues);
            writer.writeToFile(brWithValues.getPkg() + ".BR", out);
        });
        mCallback.onBrWriterReady(
                bindableBag.getVariableIdLookup(),
                bindableBag.getWrittenPackages());
    } catch (LoggedErrorException e) {
        // This will be logged later
    }
}

生成 BRDataBinderMapper文件。

三、总结

在引入DataBinding后,在布局的xml文件使用了 layout标签,或者引入ViewBinding后,都会在Android Studio中生成虚拟的 XxxXxxBinding.java,如:com.zbt.databinding.databinding.ActivityTest1Binding,在构建项目或者运行相应的task后才会生成相应 XxxXxxBinding.java文件,然后打包到apk中,供app运行时使用。

在执行 dataBindingGenBaseClasses会生成一次XxxXxxBinding.java,同时注解处理类 ProcessDataBinding也会生成一次 XxxXxxBinding.java。

DataBinding涉及的代码太多,流程也相当复杂,所以文中只贴了部分核心代码,某些核心逻辑也是一笔带过,是因为笔者的水平有限。笔者花了相当长时间去看源码也还是有很多逻辑没有理清,待后面有时间再重新理一遍。读者可以根据上面 debug模式去进行 DataBinding的逻辑验证。唯一的遗憾是没能通过 debug模式调试 ProcessDataBinding

四、遗留问题

  1. BaseDataBinder 将之前生成 xxx_xxx-layout.xml 重新生成 LayoutFileBundle,而不是用之前已经生成LayoutFileBundle
  2. 为什么引入了 DataBinding或者 ViewBinding就能够使用 XxxXxxBinding.java 这个时候这些文件还未生成
  3. 为什么task 和 注解要多次生成 XxxXxxBinding.java
  4. task和 注解生成XxxXxxBinding.java的方式为什么不同

五、参考文章

ViewBinding 的本质
AbstractProcessor注解处理器
Android-DataBinding-原理-编译期 Android Gradle Plugin源码分析