本文需要对照源码参考。
从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 文件是怎么写出来的。