android viewbinding_程序员必懂小技巧之ViewBinding
終于有一篇帶大家從本質來了解 ViewBinding 了。
如果你完全沒了解過 View Binding,可以先看下面這篇介紹:
AS 3.6 Canary 中推出新技術 視圖綁定 View Binding
今天我們來深入的了解 ViewBinding 的本質,看看他是怎么生成 ActivityMainBinding 這種文件的。
1、使用
ViewBinding 目前只支持 as3.6,使用方法很簡單,僅僅只需要添加如下代碼:
android {viewBinding {enabled = true} }make project 之后,會在對應的 module 路徑:
app/build/generated/data_binding_base_class_source_out/${buildTypes}/out/${包名}/databinding生成 ViewBinding 文件,為什么我會說 對應的 module ?因為 viewBinding 只對當前設置了 enabled = true 的 module 才會進行處理。
然后來看下處理后的文件:
我們來看看這個文件有哪些信息:
- R.layout.activity_main 布局文件
- 布局文件中的 view 控件和 view id
- 布局文件的 rootView 和類型
接下來,我們會通過源碼的方式來跟蹤到,這些信息是怎么產生的。
2 、準備
由于我們并沒有依賴其他 plugin 就可以使用,所以能被直接識別只能是 classpath 依賴的 gradle 了:
classpath 'com.android.tools.build:gradle:3.6.1'既然 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:
然后我們進去找到對應的 compiler 3.6.1 版本,通過 gradle 依賴,我們就能看到源碼了:
https://mvnrepository.com/artifact/androidx.databinding/databinding-compiler-common/3.6.1
compile group: 'androidx.databinding', name: 'databinding-compiler-common', version: '3.6.1'可以看到,ViewBinding 是屬于 dataBinding 庫里面的一個小功能。
3 階段一:收集元素
由于我們僅僅只是查看 dataBinding compiler,所以,對于 gradle 調用 compiler 的哪個部分進行聯結,我們是查看不到的,但這也不影響我們跟蹤源碼。
public boolean processResources(final ResourceInput input, boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException {...// 文件處理 callbackProcessFileCallback 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 是否為 layoutif (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 方法:
①、這個地方會拿著 xml 文件的路徑和輸出路徑進行解析
②、將解析結果緩存起來
然后來看下 xml 的解析 parseXml
parseOriginalXml:
private static ResourceBundle.LayoutFileBundle parseOriginalXml(@NonNull final RelativizableFile originalFile, @NonNull final String pkg,@NonNull final String encoding, boolean isViewBindingEnabled)throws IOException {...// ①、是否是 databindingif (isBindingData) {data = getDataNode(root);rootView = getViewNode(original, root); } else if (isViewBindingEnabled) { // ②、viewBinding 是否開啟data = null;rootView = root;// xml 的根元素} else {return null;}...// 生成 bundleResourceBundle.LayoutFileBundle bundle =new ResourceBundle.LayoutFileBundle(originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,isMerge, isBindingData, getViewName(rootView));final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;// viewBinding 不會 解析 dataparseData(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 相關的 @={ 的表達式,最后將結果緩存起來,源碼我就補貼了,太多,影響文章
4 、階段二:寫 Layout 文件
// xml 的輸出目錄 public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {writeLayoutInfoFiles(xmlOutDir, mFileWriter); } public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {// ①、遍歷收集的 layout filefor (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 我是未知的,也不知道他存到了哪,但沒有關系,我們既然知道了生成的文件名規則,我們可以全局搜索該文件,最終,我們在該目錄中搜索到:
文件內容如下:
<?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 的,讀者可以自行運行查看
5 、階段三:寫 ViewBinding 類
@Suppress("unused")// used by tools class BaseDataBinder(val input : LayoutInfoInput) {init {input.filesToConsider.forEach {it.inputStream().use {// 又將上面收集的 layout,將 xml 轉成 LayoutFileBundleval bundle = LayoutFileBundle.fromXML(it)// 緩存進 ResourceBundleresourceBundle.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)// 遍歷 layoutBindingslayoutBindings.forEach { layoutName, variations ->// 將 LayoutFileBundle 信息包裝成 BaseLayoutModelval layoutModel = BaseLayoutModel(variations)val javaFile: JavaFileval classInfo: GenClassInfoLog.GenClass// 當前是否是 databindingif (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 類中
// ①、最終會調用到 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 方法。
總結
文章已經盡量保持源碼簡短,只貼核心部分。本來還想絮絮叨叨一下,算了,就到這。
文章有關參考
https://github.com/AndroidCot/Android?github.com總結
以上是生活随笔為你收集整理的android viewbinding_程序员必懂小技巧之ViewBinding的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python sklearn下载了但是引
- 下一篇: anaconda 安装tensorfol