Today we’ll take a closer look at the nature of ViewBinding and how it generates ActivityMainBinding.
use
ViewBinding currently only supports AS3.6, and it’s easy to use. Just add the following code:
android {
viewBinding {
enabled = true}}Copy the code
After make project, the corresponding Module path will be:
App/build/generated/data_binding_base_class_source_out / ${buildTypes} / out / ${package} / databinding
Generate ViewBinding file. Why do I say corresponding Module? ViewBinding will only handle modules that are currently enabled = True. Then take a look at the processed file:
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(a) {
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 newActivityMainBinding((ConstraintLayout) rootView, tv); }... }}Copy the code
Let’s see what information this file has:
- R.layout.activity_main layout file
- View control and View ID in the layout file
- RootView and type of the layout file
And then we’re going to track how this information is generated in the source code.
ViewBinding is used in include, Merge, Adapter, fragment, and activity
To prepare
Since we don’t rely on any other plugin to use it, we can only be directly identified as the gradle that our classpath depends on:
The classpath ‘com. Android. Tools. Build: gradle: 3.6.1’
Since we can see the generated class of ViewBinding after make Project, we can see what tasks are done based on the build information of make Project:
> Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE > Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE > . > Task :app:dataBindingGenBaseClassesDebugCopy the code
No ViewBinding was found, but dataBinding was found, but you can be sure that dataBinding is the task that generated the ViewBinding (because no other task has a binding). Gradle :3.6.1 contains 18 dependencies. The first is the Data Binding Compiler Common:
The compiler 3.6.1 track
Compile group: ‘androidx.databinding’, name: ‘databinding-compiler-common’, version: ‘3.6.1’
Phase 1: Collect elements
Since we are only looking at the dataBinding Compiler, we cannot see which part of the Gradle invocation compiler is joined, but this does not prevent us from tracing the source code. Let’s get straight to it:
LayoutXmlProcessor.java
public boolean processResources(final ResourceInput input, boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException
{...// The file handles callback
ProcessFileCallback callback = new ProcessFileCallback(){
...
// Is it incremental compilation
if (input.isIncremental()) {
// Incremental compilation file processing
processIncrementalInputFiles(input, callback);
} else {
// Full compilation file processingprocessAllInputFiles(input, callback); }... }Copy the code
Let’s go straight to full compiled file handling:
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 is layout
if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
// create subPath
callback.processLayoutFolder(firstLevel);
ToLowerCase ().endswith (".xml"); toLowerCase().endswith (".xml")
for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
// Do the layout filecallback.processLayoutFile(xmlFile); }}else{... }Copy the code
(1), to determine whether the current folder name startWith layout (2), the output directory will create a File, the output directory for the new File (input. GetRootOutputFolder (), the File path); The file path is relativized with the input directory. The output directory is the output directory + file filename. The layout file name endWith is.xml. The layout file name is endWith. This method calls the processSingleFile method and then we’ll look at the processSingleFile method:
public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException {
// ()
finalResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser .parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup, isViewBindingEnabled); .// create a cache
mResourceBundle.addLayoutBundle(bindingLayout, true);
return true;
}
Copy the code
Parses the path and output path of the XML file. Caches the result of parsing and then looks at 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);
}
Copy the code
ParseOriginalXml:
private static ResourceBundle.LayoutFileBundle parseOriginalXml(
@NonNull final RelativizableFile originalFile, @NonNull final String pkg,
@NonNull final String encoding, boolean isViewBindingEnabled)
throws IOException {...// check whether it is databinding
if (isBindingData) {
data = getDataNode(root);
rootView = getViewNode(original, root);
} else if (isViewBindingEnabled) {
// check whether viewBinding is enabled
data = null;
rootView = root;// The root element of XML
} else {
return null; }.../ / generated 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 does not parse data
parseData(original, data, bundle);
// ()
parseExpressions(newTag, rootView, isMerge, bundle);
return bundle;
Copy the code
IsViewBindingEnable = true gradle enable = true gradle enable = true Data = null; data is only available in databinding; viewBinding is not parsed. View id, tag, include, fragment, and other XML-related elements, and databinding related @={expression, and finally cache the result, source CODE I will pay too much, affect the article
Stage 2: Write the Layout file
LayoutXmlProcessor.java
// The output directory of XML
public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {
writeLayoutInfoFiles(xmlOutDir, mFileWriter);
}
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
// Go through the collected layout file
for(ResourceBundle.LayoutFileBundle layout : mResourceBundle .getAllLayoutFileBundlesInSource()) { writeXmlFile(writer, xmlOutDir, layout); }... }private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,ResourceBundle.LayoutFileBundle layout)throws JAXBException {
// Generate a file name
String filename = generateExportFileName(layout);
// write the file
writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
}
Copy the code
The file name of the LayoutFileBundle is generated. The file name of the LayoutFileBundle must be:
layout.getFileName() + ‘-‘ + layout.getDirectory() + “.xml
For example, activity_main. XML, the generated fileName is activity_main-layout. XML. Since we are directly tracing the Databinding Compiler library, we cannot trace gradle’s connection to the compiler library. Therefore, xmlOutDir is unknown to me, and I do not know where it is stored, but it does not matter. Now that we know the generated filename rules, we can search globally for the file. Finally, we find:
app/build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main-layout.xml
The content of the document is as follows:
<? The 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>Copy the code
This XML file describes the information about the original layout. For details about how to associate tags with include and merge, you can run this file by yourself
Phase 3: Write the ViewBinding class
BaseDataBinder.java
@Suppress("unused")// used by tools class BaseDataBinder(val input : LayoutInfoInput) {init {input. FilesToConsider. ForEach {it. InputStream (.) use {/ / and will collect the layout of the above, Var bundle = layoutFileBundle.fromxml (it) // Cache XML to ResourceBundle resourceBundle.addLayoutBundle(bundle, true) } } ... }Copy the code
You can see that the layout XML generated before is read again. Why does this place write and read instead of directly using the previous layout cache? I think it’s because of decoupling, they’re all independent tasks. Then look at how the Binding class is generated:
@Suppress("unused")// used by android gradle plugin
fun generateAll(writer : JavaFileWriter) {
// Get all the LayoutFileBundles and sort them by filename
val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
.groupBy(LayoutFileBundle::getFileName)
/ / traverse layoutBindings
layoutBindings.forEach { layoutName, variations ->
// Wrap the LayoutFileBundle information as BaseLayoutModel
val layoutModel = BaseLayoutModel(variations)
val javaFile: JavaFile
val classInfo: GenClassInfoLog.GenClass
// It is currently databinding
if (variations.first().isBindingData) {
...
} else {
// If no, use ViewBinding
val viewBinder = layoutModel.toViewBinder()
// Create a Java filejavaFile = viewBinder.toJavaFile(useLegacyAnnotations = ! useAndroidX) ... } writer.writeToFile(javaFile) ... }Copy the code
ToViewBinder is a BaseLayoutModel extension function that returns LayoutFileBundle as a ViewBinder class. The extension function is in the ViewBinderGenerateSource class
ViewBinderGenerateSource.java
// The JavaFileGenerator create method is called
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()
private class JavaFileGenerator(
private val binder: ViewBinder,
private val useLegacyAnnotations: Boolean) {
// The generated javaFile method is called. The generated class information depends on the typeSpec method
fun create(a) = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
addFileComment("Generated by view binder compiler. Do not edit!")}Copy the code
private fun typeSpec(a) = 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())
}
Copy the code
This is where the typeSpec methods are posted. Specifically, you can look at the typeSpec source code to see what the ViewBinding class generated by a dot contains, rootView fields, inflater, and bind methods.
conclusion
The article has tried to keep the source brief, only stick to the core part. I was gonna go on and on, but anyway, that’s it