ViewBinding-LayoutXmlProcessor解析

本文需要对照源码参考。

从mvn查到,当前 viewbinding 最新版本4.2.2。

viewbinding依赖于databinding,在databing-compiler-common:4.2.2中,我们找到LayoutXmlProcessor类,即是我们要研究的类。

/**
 * Processes the layout XML, stripping the binding attributes and elements
 * and writes the information into an annotated class file for the annotation
 * processor to work with.
 */
public class LayoutXmlProcessor {
}

翻译一下注释:处理XML布局文件,剥离绑定的属性和元素,并将信息写入注解类文件中,供注释处理器使用。

此类的作用就是解析xml。

LayoutXmlProcessor,xml布局处理器,有以下成员变量:

1,LAYOUT_FOLDER_FILTER:layout文件夹筛选器,在项目中搜索所有以"layout"开头的文件夹;

2,XML_FILE_FILTER:xml文件筛选器,在layout文件夹筛选出来后,用此继续筛选以“.xml”结尾的布局文件;

3,mFileWriter:输出最终绑定文件的类,我们都知道,输出绑定的文件在build/generated/data_binding/base_class_source_out文件下,例如:ActivityMainBinding.java,这个文件即是由mFileWriter写出来的,内部可以看到调用了FileUtil.writeStringToFile();

4,mResourceBundle:资源绑定类,绑定了id和控件

5,mProcessingComplete:布尔值,是否处理完毕

6,mOriginalFileLookup:回调接口,找到xml资源的原始复制文件。

以上是成员变量,还有一个静态内部类ResourceInput,和一个内部接口ProcessFileCallback,先看一下ResourceInput,名字:资源录入:

1,mIncremental,是否是增量的

2,mRootInputFolder,录入的根文件夹

3,mRootOutputFolder,输出的根文件夹

4,mAdded:增加的文件集合

5,mRemoved:移除的文件集合

6,mChanged:改变的文件集合

构造函数确定了1,2,3成员变量,其他函数基本围绕几个4,5,6三个集合操作。

再看一下ProcessFileCallback接口几个方法:

1,processLayoutFile:处理布局文件,估计是处理.xml布局文件;

2,processOtherFile:处理其他文件,可能layout布局中混了杂物;

3,processRemovedLayoutFile:移除布局文件

4,processRemovedOtherFile:移除其他文件

5,processOtherFolder:处理其他文件夹

6,processLayoutFolder:处理布局文件夹

7,processOtherRootFile:处理其他根文件

8,processRemovedOtherRootFile:移除其他根文件

接下来分析LayoutXmlProcessor的processResources,最关心的处理资源方法。

1,入参:ResourceInput,isViewBindingEnabled,isDataBindingEnabled。ResourceInput待处理的资源,isDataBindingEnabled不用考虑,我们不研究数据绑定,肯定为false,isViewBindingEnabled一定为true,我们研究布局绑定。

2,input.isIncremental(),判断是否是增量资源,我们从头开始研究,所以为false,走processAllInputFiles()方法,解析所有的录入文件,入参:ResourceInput和回调ProcessFileCallback,参数ResourceInput是编译器传来的,我们直接用,ProcessFileCallback是我们自己定义的,暂时不看,先看解析文件的方法,再回头看回调接口。

3,processAllInputFiles:

        a, 第一步,如下代码,删除根输出文件夹;然后,重新创建输出根文件夹;检测录入文件是文件夹

FileUtils.deleteDirectory(input.getRootOutputFolder());
Preconditions.check(input.getRootOutputFolder().mkdirs(), "out dir should be re-created");
Preconditions.check(input.getRootInputFolder().isDirectory(), "it must be a directory");

        b,遍历录入文件夹,这里说一下,这个录入的根文件夹应该不是layout文件夹,而是工程的资源文件夹:res。

for (File firstLevel : input.getRootInputFolder().listFiles()) {
     if (firstLevel.isDirectory()) {
           if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
               callback.processLayoutFolder(firstLevel);
               //noinspection ConstantConditions
               for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
                   callback.processLayoutFile(xmlFile);
               }
           } else {
               callback.processOtherFolder(firstLevel);
               //noinspection ConstantConditions
               for (File file : firstLevel.listFiles()) {
                   callback.processOtherFile(firstLevel, file);
               }
           }
      } else {
           callback.processOtherRootFile(firstLevel);
      }

}

        我们直接进到方法的核心中,找到layout文件夹后,callback回调processLayoutFolder方法,处理layout文件夹,然后遍历layout中所有以.xml结尾的布局文件,callback回调processLayoutFile方法,处理布局文件。

        至此,我们知道,callback最重要的一个方法是processLayoutFile,这时,我们进到ProcessFileCallback接口的实例类callback中看一下processLayoutFile方法怎么实现的。

        processLayoutFile:入参:布局文件,内部调用了processSingleFile方法,

        processSingleFile:入参(布局文件的绝对路径,输出的绝对路径,是否绑定view:true,是否绑定data:false),

 public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,
            boolean isViewBindingEnabled, boolean isDataBindingEnabled)
            throws ParserConfigurationException, SAXException, XPathExpressionException,
            IOException {
        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;
    }

        可以看到LayoutFileParser .parseXml()这个有趣的方法:解析xml。这是个静态方法,入参:输入文件,输出路径,包名,xml原始拷贝查找回调接口,是否绑定view,是否绑定data。

        进入LayoutFileParser类,parseXml中前半部分确认了原始文件originalFile,后面 stripFile()方法判断是否绑定了doc节点,如果绑定了就复写绑定的文件,没有绑定就复制文件。最后进入parseOriginalXml()方法。

        parseOriginalXml,入参:原始文件绝对路径,包名,编码方式,是否绑定view,是否绑定data。首先,文件转为流,通过ANTLR文本处理,最后解析出XMLParser.ElementContext root,

root包含了解析出的所有元素。之后判断有没有viewBindingIgnore属性,如果为true的话,就不绑定view了,直接返回null。

        新建 ResourceBundle.LayoutFileBundle,parseData不用考虑,直接看 parseExpressions()。

        parseExpressions,入参:路径标识tag,rootView,isMerge(false),bundle。首先新建两个集合,bindingElements和otherElementsWithIds,绑定元素集合和其他元素id集合。rootView直接调用accept方法间接调用 visitElement 方法访问 rootView 本身,其子节点递归调用了accept方法:

public T visitChildren(RuleNode node) {
        T result = this.defaultResult();
        int n = node.getChildCount();

        for(int i = 0; i < n && this.shouldVisitNextChild(node, result); ++i) {
            ParseTree c = node.getChild(i);
            T childResult = c.accept(this);
            result = this.aggregateResult(result, childResult);
        }

        return result;
    }

      visitElement方法中,判断如果是rootView或者 isMerge(false) 或者 本元素及子元素中含有 include 节点(无) 或者含有表达式(无)的话,直接放入 bindingElements 集合中。其他子元素,如果包含 android:id 属性,放入 otherElementsWithIds 集合中。我们不用关心 bindingElements,只用看 otherElementsWithIds,如下代码:

 for (XMLParser.ElementContext elm : otherElementsWithIds) {
            final String id = attributeMap(elm).get("android:id");
            final String className = getViewName(elm);
            bundle.createBindingTarget(id, className, true, null, null, new Location(elm));
        }

        可见,直接操作 LayoutFileBundle 添加依据 otherElementWithIds 创建的BindingTargetBundle 类。最后,返回bundle,bundle中添加了所有绑定了id的类 :mBindingTargetBundles。

        回到LayoutXmlProcessor的 processSingleFile 处理布局文件的方法中,得到的 bindingLayout对象(即上面的bundle)。mResourceBundle 通过 addLayoutBundle 添加该 bindingLayout,去 addLayoutBundle中查看下源码。

        addLayoutBundle,入参:布局文件的bundle解析对象,fromSource(true)。内部进行了简单的检测、筛重,最后加入进了 mLayoutFileBundlesInSource 和 mLayoutBundles 两个集合中。

        ------------------------------------------------下一阶段--------------------------------------------------------

       解析布局完毕,差写入注解类文件,看一下 LayoutXmlProcessor 中的 mFileWriter 传进来后怎么用的。只有一个地方用到了:

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

    public JavaFileWriter getFileWriter() {
        return mFileWriter;
    }

        进 writeLayoutInfoFiles,入参:xmlOutDir 输出路径,writer 写入工具。放代码:

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);
        }

        // Delete stale layout info files due to removed/changed layout files. There are 2 cases:
        //   1. Layout files were removed
        //   2. Layout files previously containing data binding constructs are now no longer
        //      containing them (see bug 153711619). NOTE: This set of layout files is a subset of
        //      mResourceBundle.getFilesWithNoDataBinding() because
        //      mResourceBundle.getFilesWithNoDataBinding() may also contain layout files that do
        //      not have a history or did not have data binding constructs in the previous build, in
        //      which cases their associated layout info files don't exist.
        List<File> staleLayoutFiles = new ArrayList<>(mResourceBundle.getRemovedFiles());
        staleLayoutFiles.addAll(mResourceBundle.getFilesWithNoDataBinding());
        for (File staleLayoutFile : staleLayoutFiles) {
            File staleLayoutInfoFile = new File(xmlOutDir, generateExportFileName(staleLayoutFile));
            // Delete quietly as the file may not exist (see comment above)
            FileUtils.deleteQuietly(staleLayoutInfoFile);
        }
    }

    可以看到,直接遍历了 mLayoutFileBundlesInSource,我们刚往里面加入了 很多bundle布局文件,这里就用上了,话不多说,进writeXmlFile方法,贴代码:

private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,
            ResourceBundle.LayoutFileBundle layout)
            throws JAXBException {
        String filename = generateExportFileName(layout);
        writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
    }

        生成导出文件名字(布局文件名-父布局文件名.xml,例如activity_main-layout.xml),然后将布局文件信息写入。对于每个布局文件,生成相应的布局信息文件。

        举个例子,看看生成的是啥,xml布局代码,有一个img是没有id的:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/left_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/right_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@color/black" />

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="fpaksdp"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="30sp" />

    </LinearLayout>


</LinearLayout>

        生成的布局信息文件:

        

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="app\src\main\res\layout\activity_main.xml"
    isBindingData="false" isMerge="false" layout="activity_main"
    modulePackage="com.lemon.newcompany" rootNodeType="android.widget.LinearLayout">
    <Targets>
        <Target tag="layout/activity_main_0" view="LinearLayout">
            <Expressions />
            <location endLine="40" endOffset="14" startLine="1" startOffset="0" />
        </Target>
        <Target id="@+id/left_btn" view="Button">
            <Expressions />
            <location endLine="17" endOffset="50" startLine="14" startOffset="8" />
        </Target>
        <Target id="@+id/right_btn" view="Button">
            <Expressions />
            <location endLine="22" endOffset="50" startLine="19" startOffset="8" />
        </Target>
        <Target id="@+id/text" view="TextView">
            <Expressions />
            <location endLine="35" endOffset="37" startLine="29" startOffset="8" />
        </Target>
    </Targets>
</Layout>

        比较一下,会发现,img没有被记录,只记录了根布局和有id的子元素。    

        至此,LayoutXmlProcessor类分析完毕,总结一下:收集项目中的xml文件,筛选,记录布局有用的信息,写入布局信息文件,改名,供apt调用,稍后分析 各种布局 binding.java 文件是怎么写出来的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值