ViewBinding 的本質

今天咱們來深刻的瞭解 ViewBinding 的本質,看看他是怎麼生成 ActivityMainBinding 這種文件的。
java

使用

ViewBinding 目前只支持 as3.6,使用方法很簡單,僅僅只須要添加以下代碼:android

android {
    viewBinding {
        enabled = true
    }
}
複製代碼

make project 以後,會在對應的 module 路徑:緩存

app/build/generated/data_binding_base_class_source_out/${buildTypes}/out/${包名}/databindingapp

生成 ViewBinding 文件,爲何我會說 對應的 module ?由於 viewBinding 只對當前設置了 enabled = true 的 module 纔會進行處理。

而後來看下處理後的文件:maven

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() {
    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 new ActivityMainBinding((ConstraintLayout) rootView, tv);
    }
    ...
  }
}

複製代碼


咱們來看看這個文件有哪些信息:ide

  • R.layout.activity_main 佈局文件
  • 佈局文件中的 view 控件和 view id
  • 佈局文件的 rootView 和類型

接下來,咱們會經過源碼的方式來跟蹤到,這些信息是怎麼產生的。
函數

具體使用能夠參考文章:[譯]深刻研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用佈局

準備

因爲咱們並無依賴其餘 plugin 就能夠使用,因此能被直接識別只能是 classpath 依賴的 gradle 了:post

classpath 'com.android.tools.build:gradle:3.6.1'gradle

既然 make project 以後就能夠看到 ViewBinding 的生成類,那麼,咱們能夠根據 make project 的 build 信息查看作了哪些 task:

> Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
> Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE
> ...
> Task :app:dataBindingGenBaseClassesDebug
複製代碼

沒有找到 ViewBinding,但找到了 dataBinding,但能夠確定的是,這個 dataBinding 就是生成 ViewBinding 的 task(由於沒有其餘的 task 帶有 binding)。

而後咱們能夠去 maven 倉庫找一下 gradle:3.6.1 ,驚喜的是,gradle:3.6.1 的依賴項有 18 個,第一個就是 Data Binding Compiler Common:

image.png
而後咱們進去找到對應的 compiler 3.6.1 版本,經過 gradle 依賴,咱們就能看到源碼了:

compile group: 'androidx.databinding', name: 'databinding-compiler-common', version: '3.6.1'

image.png
能夠看到,ViewBinding 是屬於 dataBinding 庫裏面的一個小功能。

階段一:收集元素

因爲咱們僅僅只是查看 dataBinding compiler,因此,對於 gradle 調用 compiler 的哪一個部分進行聯結,咱們是查看不到的,但這也不影響咱們跟蹤源碼。

咱們直接來看 :

LayoutXmlProcessor.java

public boolean processResources(final ResourceInput input, boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException {
   ...
   // 文件處理 callback
   ProcessFileCallback callback = new ProcessFileCallback(){
   ...
   // 是不是增量編譯
   if (input.isIncremental()) {
       // 增量編譯文件處理
       processIncrementalInputFiles(input, callback);
   } else {
   	   // 全量編譯文件處理
       processAllInputFiles(input, callback);
   }
   ...
 }
複製代碼


咱們直接來看 全量編譯文件處理 :

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 是否爲 layout
                if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
                    // ②、建立 subPath 
                    callback.processLayoutFolder(firstLevel);
                    // ③、遍歷 firstLevel 目錄下面的全部文件,知足 toLowerCase().endsWith(".xml");
                    for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
                       // ④、處理佈局文件
                        callback.processLayoutFile(xmlFile);
                    }
                } else {
        ...
    }
複製代碼

①、判斷當前的文件夾的文件名 startWith 是不是 layout 
②、會建立一個文件輸出目錄, 輸出目錄爲 new File(input.getRootOutputFolder(),file path); 這個 file path 作了與輸入目錄的 relativize 化,其實,能夠理解爲,這個輸出目錄爲 輸出目錄 + file 文件名 。
③、判斷 layout 下面的文件名 endWith 是不是 .xml 
④、處理 xml 文件,這個地方也會建立一個輸出目錄,跟 ② 的方式同樣,最終,這個方法會調用到 processSingleFile 方法

而後咱們來看下 processSingleFile 方法:

public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException {
     // ①、解析 xml
     final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
         .parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup,
                   isViewBindingEnabled);
     ...
     // ②、緩存起來
     mResourceBundle.addLayoutBundle(bindingLayout, true);
     return true;
}
複製代碼

①、這個地方會拿着 xml 文件的路徑和輸出路徑進行解析
②、將解析結果緩存起來
而後來看下 xml 的解析 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);
}
                
複製代碼

parseOriginalXml:

private static ResourceBundle.LayoutFileBundle parseOriginalXml( @NonNull final RelativizableFile originalFile, @NonNull final String pkg, @NonNull final String encoding, boolean isViewBindingEnabled) throws IOException {
          ...
          // ①、是不是 databinding
          if (isBindingData) {
              data = getDataNode(root);
              rootView = getViewNode(original, root); 
          } else if (isViewBindingEnabled) { 
               // ②、viewBinding 是否開啓
              data = null;
              rootView = root;// xml 的根元素
          } else {
              return null;
          }
      ...
      // 生成 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 不會 解析 data
      parseData(original, data, bundle);
      // ③、解析表達式
      parseExpressions(newTag, rootView, isMerge, bundle);
      return bundle;
       
複製代碼

①、是不是 databinding,這個的判斷依據是,根元素是不是 layout , 獲取 data 和 rootView
②、isViewBindingEnable 就是 gradle 設置的 enable = true,根元素就是就是他的 rootView,這個地方要注意的是 data = null,data 數據只有 databinding 纔會有的元素,viewBinding 是不會去解析的
③、解析表達式,這裏面會循環遍歷元素,解析 view 的 id、tag、include、fragment 等等 xml 相關的元素,而且還有 databinding 相關的 @={ 的表達式,最後將結果緩存起來,源碼我就補貼了,太多,影響文章

階段二:寫 Layout 文件

LayoutXmlProcessor.java

// xml 的輸出目錄
public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {
        writeLayoutInfoFiles(xmlOutDir, mFileWriter);
}  
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
        // ①、遍歷收集的 layout 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());
 }
複製代碼

①、遍歷以前收集到的全部 LayoutFileBundle,寫入 xmlOutDir 路徑
②、生成 LayoutFileBundle 的文件名,這個文件名最終生成爲:

layout.getFileName() + '-' + layout.getDirectory() + ".xml

例如 activity_main.xml,生成的 fileName 爲 activity_main-layout.xml
③、將 LayoutFileBundle 轉換 xml ,寫入文件

因爲咱們是直接跟蹤的 databinding compiler 庫,因此沒法跟蹤到 gradle 是什麼聯結 compiler 庫的,因此,xmlOutDir 我是未知的,也不知道他存到了哪,但沒有關係,咱們既然知道了生成的文件名規則,咱們能夠全局搜索該文件,最終,咱們在該目錄中搜索到:

app/build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main-layout.xml

文件內容以下:

<?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>
複製代碼


這份 xml 描述了原始 layout 的相關信息,對於 include 和 merge 是怎麼關聯 tag 的,讀者能夠自行運行查看

階段三:寫 ViewBinding 類

BaseDataBinder.java

@Suppress("unused")// used by tools
class BaseDataBinder(val input : LayoutInfoInput) {
    init {
        input.filesToConsider.forEach {
            it.inputStream().use {
                // 又將上面收集的 layout,將 xml 轉成 LayoutFileBundle
                val bundle = LayoutFileBundle.fromXML(it)
                // 緩存進 ResourceBundle
                resourceBundle.addLayoutBundle(bundle, true)
             }
        }
        ...
    }
複製代碼


能夠看到,最後又去讀以前生成的 layout xml,這個地方爲何會又寫又讀,而不是直接利用以前 layout 的緩存?我想多是由於解耦,他們都是獨立的 task。

而後來看是如何生成 Binding 類的:

@Suppress("unused")// used by android gradle plugin
fun generateAll(writer : JavaFileWriter) {
   // 拿到全部的 LayoutFileBundle,並根據文件名進行分組排序
   val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
            .groupBy(LayoutFileBundle::getFileName)
   // 遍歷 layoutBindings
   layoutBindings.forEach { layoutName, variations ->
       // 將 LayoutFileBundle 信息包裝成 BaseLayoutModel
       val layoutModel = BaseLayoutModel(variations)
       val javaFile: JavaFile
       val classInfo: GenClassInfoLog.GenClass
        // 當前是不是 databinding
        if (variations.first().isBindingData) {
              ...
        } else {
          // ①、不是的話,按照 ViewBinding 處理
          val viewBinder = layoutModel.toViewBinder()
          // ②、生成 java file 文件
          javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
          ...
        }
	    writer.writeToFile(javaFile)
    ...
 }
複製代碼

①、toViewBinder 是 BaseLayoutModel 的拓展函數,他會將 LayoutFileBundle 包裝成 ViewBinder 類返回 
②、toJavaFile 是 ViewBinder 的拓展函數,該拓展函數在 ViewBinderGenerateSource 類中

ViewBinderGenerateSource.java

// ①、最終會調用到 JavaFileGenerator 的 create 方法
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
    JavaFileGenerator(this, useLegacyAnnotations).create()
    
private class JavaFileGenerator( private val binder: ViewBinder, private val useLegacyAnnotations: Boolean) {
    // 最終會調用生成 javaFile 方法,生成的類信息主要看 typeSpec 方法
    fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
        addFileComment("Generated by view binder compiler. Do not edit!")
    }

複製代碼
private fun typeSpec() = 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())
}
複製代碼


這個地方就貼 typeSpec 方法了,具體的,你們能夠本身去看源碼,從 typeSpec 中,咱們就能夠看到點生成的 ViewBinding 類包含了哪些東西,rootView 字段,inflater 、bind 方法。

總結

文章已經儘可能保持源碼簡短,只貼核心部分。原本還想絮絮不休一下,算了,就到這

相關文章
相關標籤/搜索