Jetpack | ViewBinding 詳解

A-曉理動(dòng)碼_光頭哥.png

通過 ViewBinding(視圖綁定) 功能,我們可以更輕松地編寫與布局文件交互的代碼。在模塊中啟用視圖綁定之后,AGP 會(huì)為該模塊中的每個(gè) XML 布局文件生成一個(gè)綁定類。該綁定類的實(shí)例中會(huì)直接引用那些在布局中聲明了資源 id 的控件。這樣一來就減少了很多像 findViewById 這種操作,同時(shí)也為控件的安全性保駕護(hù)航。

文章核心點(diǎn):

  • VB 集成與一般使用方式,包括:Activity 、Fragment、Adapter、include、merge、ViewStub
  • KT 屬性代理與泛型實(shí)化類型參數(shù) reified 的介紹
  • 通過 KT 屬性代理簡化 VB 創(chuàng)建流程,并封裝了一個(gè)庫 VBHelper
  • LayoutInflater 原理與參數(shù)解析
  • XXXBinding 類的綁定過程
  • XXXBinding 類的生成過程

VBHelper:是我寫這篇文章提取的一個(gè)庫,通過屬性代理簡化了VB的使用,有想了解的可以提提意見

  1. 在 Activity 中創(chuàng)建 ViewBinding 綁定類
//通過自定義屬性代理 + 反射綁定類的 inflate 方法
private val binding: ActivityMainBinding by vb()
//通過自定義屬性代理 + 傳遞 inflate 方法引用
private val binding: ActivityMainBinding by vb(ActivityMainBinding::inflate)
  1. 在 Fragment 中創(chuàng)建 ViewBinding 綁定類
//通過自定義屬性代理 + 反射綁定類的 inflate 方法
private val binding: FragmentMainBinding by vb()
//通過自定義屬性代理 + 傳遞 inflate 方法引用
private val binding: FragmentMainBinding by vb(FragmentMainBinding::inflate)
  1. 在 View 中創(chuàng)建 ViewBinding 綁定類
//通過自定義屬性代理 + 反射綁定類的 inflate 三參數(shù)方法
private val binding: MyViewBinding by vb()
//通過自定義屬性代理 + 傳遞 inflate 三參數(shù)方法引用
private val binding: MyViewBinding by vb(MyViewBinding::inflate)
  1. 在 Adapter 中創(chuàng)建包含了綁定類的 BindingViewHolder
//通過自定義屬性代理 + 反射綁定類的 inflate 三參數(shù)方法
val holder: BindingViewHolder<LayoutItemTextBinding> by vh(parent)
//通過自定義屬性代理 + 傳遞綁定類的 inflate 三參數(shù)方法引用
val holder: BindingViewHolder<LayoutItemTextBinding> by vh(parent, LayoutItemTextBinding::inflate)

1.VB 概述

  • 視圖綁定在 Android Studio 3.6 Canary 11 及更高版本中可用。

  • 開啟自動(dòng)生成綁定類:模塊 build.gradle 文件中的 android 閉包下,兩種方式

    • viewBinding {enabled = true} 默認(rèn)值為false, Android Studio 3.6 Canary 11 及更高版本中可用。
    • buildFeatures {viewBinding = true} 默認(rèn)值為false, Android Studio 4.0 及更高版本中可用
  • 忽略自動(dòng)生成綁定類:請(qǐng)將 tools:viewBindingIgnore="true" 屬性添加到相應(yīng)布局文件的根視圖中

  • 生成綁定類的名稱:將 XML 文件的名稱轉(zhuǎn)換為駝峰式大小寫,并在末尾添加“Binding”一詞。
    LayoutInflater.Factory

    • result_profile.xml ====>ResultProfileBinding
    • 每個(gè)綁定類還包含一個(gè) getRoot() 方法,用于為相應(yīng)布局文件的根視圖提供直接引用。
  • 與使用 findViewById 相比

    • Null 安全:綁定類的創(chuàng)建是通過解析布局文件在編譯時(shí)生成,布局文件添加了id的控件才會(huì)生成對(duì)應(yīng)的引用,因此不會(huì)發(fā)生綁定類中存在而布局中沒有對(duì)應(yīng)控件的情況,如果布局引用了錯(cuò)誤的類型也會(huì)在編譯時(shí)暴露錯(cuò)誤。
    • 類型安全:布局中聲明的控件是確定類型的。這意味著不存在發(fā)生類轉(zhuǎn)換異常的風(fēng)險(xiǎn)。
  • 與使用 DataBinding 對(duì)比

    • 視圖綁定和數(shù)據(jù)綁定均會(huì)生成可用于直接引用視圖的綁定類。但是,視圖綁定旨在處理更簡單的用例。

    • 更快的編譯速度:視圖綁定不需要處理注解信息,因此編譯時(shí)間更短。

    • 易于使用:視圖綁定不需要在 XML 布局文件中標(biāo)記,只要在模塊中啟用視圖綁定后,它會(huì)自動(dòng)應(yīng)用于該模塊的所有布局。

    • 如果項(xiàng)目中使用了數(shù)據(jù)綁定最好在項(xiàng)目中同時(shí)使用視圖綁定和數(shù)據(jù)綁定。這樣可以在需要高級(jí)功能的布局中使用數(shù)據(jù)綁定,而在不需要高級(jí)功能的布局中使用視圖綁定。如果只是取代 findViewById() 調(diào)用,請(qǐng)考慮改用視圖綁定。

2. VB 一般使用

2.1 Activity

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    setSupportActionBar(binding.toolbar)
}

2.2 Fragment

private var _binding: FragmentFirstBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    firstViewModel = ViewModelProvider(this).get(FirstViewModel::class.java)
    _binding = FragmentFirstBinding.inflate(inflater, container, false)
    binding.rvList.layoutManager = LinearLayoutManager(requireContext())
    return binding.root
}

2.3 Adapter

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextHolder {
    val itemBinding = LayoutItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    //綁定類交給Holder
    return TextHolder(itemBinding)
}

override fun onBindViewHolder(holder: TextHolder, position: Int) {
    val item: String = list[position]
    //數(shù)據(jù)交給Holder
    holder.bind(item)
}

class TextHolder(val itemBinding: LayoutItemTextBinding) : RecyclerView.ViewHolder(itemBinding.root) {
    fun bind(name: String) {
        itemBinding.tvName.text = name
    }
}

2.4 include

binding.includeLayout.tvInfoInclude.text = "tvInfoInclude:$item"
// todo  include 方式有時(shí)候無法識(shí)別到真實(shí)的綁定類類型只能識(shí)別它是個(gè)View類型但是編譯不會(huì)報(bào)錯(cuò), 這種情況清理緩存可能會(huì)好 ,或者也可以強(qiáng)制類型轉(zhuǎn)換或者自己bind
val tvInfoInclude: LayoutInfoBinding = binding.includeLayout as LayoutInfoBinding
val tvInfoInclude = LayoutInfoBinding.bind(binding.root)
tvInfoInclude.tvInfoInclude.text = "tvInfoInclude:$item"

2.5 merge

//include+merge 只能手動(dòng)調(diào)用綁定類的bind方法
val layoutInfoMergeBinding = LayoutInfoMergeBinding.bind(binding.root)
val tvInfoMerge = layoutInfoMergeBinding.tvInfoMerge
tvInfoMerge.text = "tvInfoMerge:$item"

2.6 ViewStub

//ViewStub 只能手動(dòng)調(diào)用綁定類的bind方法
binding.layoutViewStub.setOnInflateListener { _, inflateId ->
    val layoutInfoViewStubBinding = LayoutInfoViewStubBinding.bind(inflateId)
    val tvInfoViewStub = layoutInfoViewStubBinding.tvInfoViewStub
    tvInfoViewStub.text = "tvInfoViewStub:$item"
}
binding.layoutViewStub.inflate()

詳細(xì)的測試代碼參考:Github | VBHelper

3. VB 與 Kotlin by

采用 Kotlin 屬性代理簡化 VB 使用的三方庫

3.1 KT 屬性代理:by lazy

  • by關(guān)鍵字實(shí)際上就是一個(gè)屬性代理運(yùn)算符重載的符號(hào),任何一個(gè)具備屬性代理規(guī)則的類,都可以使用by關(guān)鍵字對(duì)屬性進(jìn)行代理。

  • by關(guān)鍵字后面帶有一個(gè)代理對(duì)象,這個(gè)代理類不一定要實(shí)現(xiàn)特定的接口,但是需要包含下面這兩個(gè)方法的簽名(val 只需要 getValue ),它就能作為一個(gè)代理屬性來使用。

  • //這個(gè)是擴(kuò)展的實(shí)現(xiàn)方式,lazy就是采用的這種
    operator fun MyDelegate.getValue(thisRef: Any?, property: KProperty<*>): String = this.value
    
    class MyDelegate {
        var value: String = "YYY"
        //todo 代理類里面必須提供 getValue 方法,或者擴(kuò)展這個(gè)方法也可
        operator fun getValue(thisRef: Any, property: KProperty<*>): String {
            return value
        }
        operator fun setValue(thisRef: Any, property: KProperty<*>, s: String) {
            value = s
        }
    }
    
  • lazy 是Kotlin 內(nèi)部對(duì)對(duì)屬性代理的一個(gè)最佳實(shí)踐,lazy 返回一個(gè)實(shí)現(xiàn)了 Lazy 接口的代理類,默認(rèn)是 SynchronizedLazyImpl,

  • Lazy<T> 有個(gè)擴(kuò)展方法,符合屬性代理的規(guī)則

  • public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
    

3.2 KT 內(nèi)聯(lián)函數(shù) inline 與泛型實(shí)化類型參數(shù) reified

官方文檔

reified-type-parameters

Kotlin和Java同樣存在泛型類型擦除的問題,但是 Kotlin 通過 inline 內(nèi)聯(lián)函數(shù)使得泛型類的類型實(shí)參在運(yùn)行時(shí)能夠保留,這樣的操作 Kotlin 中把它稱為實(shí)化,對(duì)應(yīng)需要使用 reified 關(guān)鍵字。

  • 滿足實(shí)化類型參數(shù)函數(shù)的必要條件

    • 必須是 inline 內(nèi)聯(lián)函數(shù),使用 inline 關(guān)鍵字修飾
    • 泛型類定義泛型形參時(shí)必須使用 reified 關(guān)鍵字修飾
  • 帶實(shí)化類型參數(shù)的函數(shù)基本定義

    //類型形參T是泛型函數(shù)isInstanceOf的實(shí)化類型參數(shù)
    inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T 
    

3.3 通過 lazy 屬性代理 + inflate方法引用

//通過 lazy 屬性代理 + inflate方法引用
fun <VB : ViewBinding> ComponentActivity.binding1(inflate: (LayoutInflater) -> VB) =
    lazy {
        inflate(layoutInflater).also {
            setContentView(it.root)
        }
    }

3.4 通過 lazy 屬性代理 + 反射

//通過 lazy 屬性代理 + 反射
//reified 實(shí)化類型參數(shù),作用是將泛型替換為真實(shí)的類型用于反射等
inline fun <reified VB : ViewBinding> ComponentActivity.binding3() =
    lazy {
        //經(jīng)過內(nèi)聯(lián)后VB是可以確切知道具體類型的,所以這里可以反射獲取具體的 ViewBinding
        val viewBinding: VB = VB::class.java.getMethod("inflate", LayoutInflater::class.java)
            .invoke(null, layoutInflater) as VB
        viewBinding.also {
            setContentView(it.root)
        }
    }

3.5 通過自定義屬性代理 + inflate方法引用

//通過自定義屬性代理 + inflate方法引用
fun <VB : ViewBinding> ComponentActivity.binding2(inflate: (LayoutInflater) -> VB) =
    ReadOnlyProperty<ComponentActivity, VB> { thisRef, property ->
        inflate(layoutInflater).also {
            setContentView(it.root)
        }
    }

3.6 通過自定義屬性代理+ 反射

//通過自定義屬性代理+ 反射
//reified 實(shí)化類型參數(shù),作用是將泛型替換為真實(shí)的類型用于反射等
inline fun <reified VB : ViewBinding> ComponentActivity.binding4() =
    ReadOnlyProperty<ComponentActivity, VB> { thisRef, property ->
        //經(jīng)過內(nèi)聯(lián)后VB是可以確切知道具體類型的,所以這里可以反射獲取具體的 ViewBinding
        val viewBinding: VB = VB::class.java.getMethod("inflate", LayoutInflater::class.java)
            .invoke(null, layoutInflater) as VB
        viewBinding.also {
            setContentView(it.root)
        }
    }

四種方式的使用

//通過 lazy 屬性代理 + inflate方法引用
private val binding1 by binding1(ActivityMainBinding::inflate)
//通過自定義屬性代理 + inflate方法引用
private val binding2 by binding2(ActivityMainBinding::inflate)
//通過 lazy 屬性代理 + 反射
private val binding3: ActivityMainBinding by binding3()
//通過自定義屬性代理+ 反射
private val binding4: ActivityMainBinding by binding4()

其它 Fragment、View、Adapter 等綁定類的生成方式可以根據(jù)上面的方式靈活調(diào)整,也可參考:Github | VBHelper

注意的地方:

  • 反射的方式我這里都是通過綁定類的 inflate 方法,也可以反射 bind 方法,就是入?yún)⒉煌梢愿鶕?jù)具體情況靈活調(diào)整。
  • merge 標(biāo)簽作為根視圖生成的綁定類的inflate 方法只有一個(gè)兩參數(shù)的 其它情況都是一參和三參同時(shí)生成,反射時(shí)需要兼容一下,VBHelper 沒有兼容這一點(diǎn)有需要的可以處理一下,具體做法就是 try-cache 分別處理。
@NonNull
public static LayoutInfoMergeBinding inflate(@NonNull LayoutInflater inflater,
    @NonNull ViewGroup parent) {
  if (parent == null) {
    throw new NullPointerException("parent");
  }
  inflater.inflate(R.layout.layout_info_merge, parent);
  return bind(parent);
}

4. VB 原理解析

4.1 LayoutInflater 原理與參數(shù)解析

參考:反思|Android LayoutInflater機(jī)制的設(shè)計(jì)與實(shí)現(xiàn)

獲取 LayoutInflater 三種方式

//獲取 LayoutInflater
//1、通過 LayoutInflater 的靜態(tài)方法 from 獲取,內(nèi)部調(diào)用的是第二種
val layoutInflater1: LayoutInflater = LayoutInflater.from(this)
//2、通過系統(tǒng)服務(wù) getSystemService 方法獲取
val layoutInflater2: LayoutInflater =
    getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
//3、如果是在 Activity 或 Fragment 可直接獲取到實(shí)例
val layoutInflater3: LayoutInflater = layoutInflater //相當(dāng)于調(diào)用 getLayoutInflater()

//三種方式在 Activity 范圍內(nèi)是單例
Log.d("Jay", "layoutInflater1:${layoutInflater1.hashCode()}")
Log.d("Jay", "layoutInflater2:${layoutInflater2.hashCode()}")
Log.d("Jay", "layoutInflater3:${layoutInflater3.hashCode()}")
//2021-09-06 23:41:52.925 6353-6353/com.jay.vbhelper D/Jay: layoutInflater1:31503528
//2021-09-06 23:41:52.925 6353-6353/com.jay.vbhelper D/Jay: layoutInflater2:31503528
//2021-09-06 23:41:52.925 6353-6353/com.jay.vbhelper D/Jay: layoutInflater3:31503528

無論哪種方式獲取最終都會(huì)走到 ContextThemeWrapper 類中 getSystemService

PhoneLayoutInflater 創(chuàng)建流程

獲取 LayoutInflater 三種方式最終會(huì)調(diào)到 ContextThemeWrapper#getSystemService

//class ContextThemeWrapper extends ContextWrapper
@Override
public Object getSystemService(String name) {
    if (LAYOUT_INFLATER_SERVICE.equals(name)) {
        if (mInflater == null) {
            mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
        }
        return mInflater;
    }
    return getBaseContext().getSystemService(name);
}

cloneInContext 是 LayoutInflater 接口的方法,LayoutInflater 唯一實(shí)現(xiàn)類是 PhoneLayoutInflater

//class PhoneLayoutInflater extends LayoutInflater
public LayoutInflater cloneInContext(Context newContext) {
    return new PhoneLayoutInflater(this, newContext);
}

布局填充流程

方法簽名

1.public View inflate(XmlPullParser parser, @Nullable ViewGroup root)
2.public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
3.public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
4.public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

四個(gè) inflate 的重載方法最終都會(huì)調(diào)用到第四個(gè),下面是四個(gè)方法的使用

//調(diào)用 LayoutInflater.inflate 的四個(gè)方法重載
//如果傳入的 root 為 null ,此時(shí)會(huì)將 Xml 布局生成的根 View 對(duì)象直接返回
val view1_1 = layoutInflater3.inflate(R.layout.layout_view, null)
//這種方式加載的布局不需要再次addView(),否則:Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
//如果傳入的 root 不為 null 且 attachToRoot 為 true,此時(shí)會(huì)將 Xml 布局生成的根 View 通過 addView 方法攜帶布局參數(shù)添加到 root 中
//如果 root 參數(shù)不為空 和 view2_1 一樣
val view1_2 = layoutInflater3.inflate(R.layout.layout_view, binding.clContainer)
//第一個(gè)參數(shù)代表所要加載的布局,第二個(gè)參數(shù)是ViewGroup,這個(gè)參數(shù)需要與第3個(gè)參數(shù)配合使用,attachToRoot如果為true就把布局添加到ViewGroup中;若為false則只采用ViewGroup的LayoutParams作為測量的依據(jù)卻不直接添加到ViewGroup中。
val view2_1 = layoutInflater3.inflate(R.layout.layout_view, binding.clContainer, true)
//如果傳入的 root 不為 null 且 attachToRoot 為 false,此時(shí)會(huì)給 Xml 布局生成的根 View 設(shè)置布局參數(shù)
val view2_2 = layoutInflater3.inflate(R.layout.layout_view, binding.clContainer, false)
val parser: XmlResourceParser = resources.getLayout(R.layout.layout_view)
//這兩個(gè)重載方法不常用
val view3 = layoutInflater3.inflate(parser, binding.clContainer)
val view4 = layoutInflater3.inflate(parser, binding.clContainer, false)
binding.clContainer.addView(view1_1)

無論是 Activity 中 setContentView 加載內(nèi)容還是 DecorView 加載屏幕根視圖都是通過 LayoutInflater 加載。

inflate 方法,詳細(xì)的加載過程會(huì)單獨(dú)整理一篇文章

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;
        try {
            advanceToRootNode(parser);
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "+ "ViewGroup root and attachToRoot=true");
                }
                                //merge 根視圖單獨(dú)處理
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                //Temp 是在 xml 中找到的根視圖
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                  
                    // 創(chuàng)建與根匹配的布局參數(shù)(如果提供)
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // 如果我們不附加,請(qǐng)為 temp 設(shè)置根布局的布局參數(shù)
                        temp.setLayoutParams(params);
                    }
                }
                // 將所有處于臨時(shí)狀態(tài)的孩子都根據(jù)其上下文進(jìn)行布局填充。
                rInflateChildren(parser, temp, attrs, true);
                // 將所有視圖添加到 root
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
                // 返回傳入的 root 還是在 xml 中找到的頂視圖。
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        } 
        return result;
    }
}

LayoutInflater 參數(shù)說明

layoutResID:代表所要加載的布局資源id,

root:是ViewGroup類型,這個(gè)參數(shù)需要與第3個(gè)參數(shù)配合使用,

attachToRoot:如果為true就把布局添加到 root 中;若為false則只采用ViewGroupLayoutParams作為測量的依據(jù)卻不直接添加到ViewGroup中。

parser:包含布局層次結(jié)構(gòu)描述的 XML dom 節(jié)點(diǎn)。

LayoutInflater.Factory 接口的擴(kuò)展功能

LayoutInflater設(shè)計(jì)了一個(gè)LayoutInflater.Factory接口,該接口設(shè)計(jì)得非常巧妙:在xml解析過程中,開發(fā)者可以通過配置該接口對(duì)View的創(chuàng)建過程進(jìn)行攔截:通過new的方式創(chuàng)建控件以避免大量地使用反射,Factory接口的意義是在xml解析過程中,開發(fā)者可以通過配置該接口對(duì)View的創(chuàng)建過程進(jìn)行攔截

LayoutInflater 總結(jié)

獲取 LayoutInflater實(shí)例最終都會(huì)走到 ContextThemeWrapper 類中 getSystemService 構(gòu)建一個(gè)局部單例的 PhoneLayoutInflater 實(shí)例。

LayoutInflater 布局填充有四個(gè)重載方法,最終都會(huì)調(diào)用到同一個(gè)方法,再根據(jù)傳遞的參數(shù)做不同的加載處理

4.2 ActivityMainBinding 類的綁定過程

inflate 過程

View 類中通過調(diào)用apt 自動(dòng)生成的綁定類的inflate方法或者 bind 方法獲取綁定類

//CustomView
val layoutInflater: LayoutInflater = LayoutInflater.from(context)
val binding = LayoutViewBinding.inflate(layoutInflater, this, true)
//SecondFragment
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedIS: Bundle?): View {
    _binding = FragmentSecondBinding.inflate(inflater, container, false)
    return binding.root
}
//include+merge 只能手動(dòng)調(diào)用綁定類的bind方法
val layoutInfoMergeBinding = LayoutInfoMergeBinding.bind(binding.root)

綁定類的 inflate 方法,通過傳入的 LayoutInflater 將 layout 填充為 View

//class FragmentSecondBinding implements ViewBinding
@NonNull
public static FragmentSecondBinding inflate(@NonNull LayoutInflater inflater,
    @Nullable ViewGroup parent, boolean attachToParent) {
  View root = inflater.inflate(R.layout.fragment_second, parent, false);
  //這里的 attachToParent 參數(shù)為 true 時(shí)不知為何不傳入 LayoutInflater 來 addView 而是自己單獨(dú)做了判斷
  if (attachToParent) {
    parent.addView(root);
  }
  return bind(root);
}

bind 過程

從 inflate 過程填充的視圖中(或者是從外部傳入的 View)實(shí)例化所有控件并構(gòu)建綁定類

  @NonNull
  public static FragmentSecondBinding bind(@NonNull View rootView) {
    //此方法的主體是以您不會(huì)編寫的方式生成的。這樣做是為了優(yōu)化已編譯的字節(jié)碼的大小和性能。
    int id;
    missingId: {
      //根布局中的普通控件
      id = R.id.button_second;
      Button buttonSecond = ViewBindings.findChildViewById(rootView, id);
      if (buttonSecond == null) {
        break missingId;
      }
      //根布局中的 include 標(biāo)簽
      id = R.id.include_layout;
      View includeLayout = ViewBindings.findChildViewById(rootView, id);
      if (includeLayout == null) {
        break missingId;
      }
      LayoutInfoBinding binding_includeLayout = LayoutInfoBinding.bind(includeLayout);
            //ViewStub標(biāo)簽
      id = R.id.layout_view_stub;
      ViewStub layoutViewStub = ViewBindings.findChildViewById(rootView, id);
      if (layoutViewStub == null) {
        break missingId;
      }
            //自定義 View
      id = R.id.name;
      CustomView name = ViewBindings.findChildViewById(rootView, id);
      if (name == null) {
        break missingId;
      }

            //include+merge 沒有生成對(duì)應(yīng)的類型,只能手動(dòng)調(diào)用綁定類的bind方法

      //構(gòu)建綁定類,并將所有控件賦值給類屬性
      return new FragmentSecondBinding((ConstraintLayout) rootView, buttonSecond, flSecond,
          binding_includeLayout, layoutViewStub, llInfo, name, textviewSecond);
    }
    // 如果有任何一個(gè)控件在 findChildViewById 過程中沒有被找到就會(huì)拋NPE異常
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

遍歷根視圖匹配布局文件中的id并通過findViewById方法返回View實(shí)例

//ViewBindings
/**
 Like `findViewById` but skips the view itself.
 */
@Nullable
public static <T extends View> T findChildViewById(View rootView, @IdRes int id) {
    if (!(rootView instanceof ViewGroup)) {
        return null;
    }
    final ViewGroup rootViewGroup = (ViewGroup) rootView;
    final int childCount = rootViewGroup.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final T view = rootViewGroup.getChildAt(i).findViewById(id);
        if (view != null) {
            return view;
        }
    }
    return null;
}

綁定過程總結(jié)

DataBinding 借助 AGP 會(huì)為所有布局文件自動(dòng)生成綁定類

綁定類的 inflate 方法通過傳入的布局填充器 LayoutInflater 以及自動(dòng)收集的根布局 id 加載出根布局 rootView 然后傳給 bind 方法實(shí)例化控件

綁定類的 bind 方法通過傳入的根布局以及自動(dòng)收集的控件 id 實(shí)例化所有控件 并構(gòu)建綁定類

4.3 ActivityMainBinding 類的生成過程

參考:ViewBinding 的本質(zhì)

DataBinding Compiler Common

依賴源碼方便查看

//todo 依賴 databinding-compiler 方便查看 ViewBinding 類的生成過程
// https://mvnrepository.com/artifact/androidx.databinding/databinding-compiler-common
implementation group: 'androidx.databinding', name: 'databinding-compiler-common', version: '7.0.1'
// https://mvnrepository.com/artifact/com.android.tools.build/gradle
implementation group: 'com.android.tools.build', name: 'gradle', version: '7.0.1'

ViewBinding 是屬于 dataBinding 庫里面的一個(gè)小功能,對(duì)于解析布局文件生成綁定類的邏輯是通用的,

階段一:解析xml布局文件

LayoutXmlProcessor:處理布局 XML,剝離綁定屬性和元素,并將信息寫入帶注解的類文件以供注釋處理器使用

processResources:假裝這個(gè)方法就是布局文件改動(dòng)后調(diào)用的入口方法(應(yīng)該是由AGP 觸發(fā),暫時(shí)未找到)


android.databinding.tool.LayoutXmlProcessor
  
public boolean processResources(ResourceInput input, boolean isViewBindingEnabled, boolean isDataBindingEnabled)
        throws ParserConfigurationException, SAXException, XPathExpressionException,
        IOException {
    ProcessFileCallback callback = new ProcessFileCallback() {
    //省略回調(diào)代碼
    }
    //布局文件的改動(dòng)輸入源支持增量構(gòu)建
    if (input.isIncremental()) {
        processIncrementalInputFiles(input, callback);
    } else {
        processAllInputFiles(input, callback);
    }
    return true;
}

processIncrementalInputFiles 處理增量輸入(Added、Removed、Changed)

processAllInputFiles 處理全部輸入

//遍歷文件
for (File firstLevel : input.getRootInputFolder().listFiles())
//處理 layout_xx 目錄下面的 xxx.xml 文件
if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
    callback.processLayoutFolder(firstLevel);
    //noinspection ConstantConditions
    for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
        callback.processLayoutFile(xmlFile);
    }
}

ProcessFileCallback 掃描文件后的回調(diào)

public void processLayoutFile(File file)
        throws ParserConfigurationException, SAXException, XPathExpressionException,
        IOException {
          //處理單個(gè)文件,
    processSingleFile(RelativizableFile.fromAbsoluteFile(file, null),
            convertToOutFile(file), isViewBindingEnabled, isDataBindingEnabled);
}

processSingleFile

public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,
        boolean isViewBindingEnabled, boolean isDataBindingEnabled)
        throws ParserConfigurationException, SAXException, XPathExpressionException,
        IOException {
          //解析xml文件 封賬布局文件掃描類
    final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
            .parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup,
                    isViewBindingEnabled, isDataBindingEnabled);
    if (bindingLayout == null
            || (bindingLayout.isBindingData() && bindingLayout.isEmpty())) {
        return false;
    }
          //添加到map緩存起來
    mResourceBundle.addLayoutBundle(bindingLayout, true);
    return true;
}

LayoutFileParser:獲取 XML 文件列表并創(chuàng)建可以持久化或轉(zhuǎn)換為 LayoutBinder 的ResourceBundle列表

android.databinding.tool.store public final class LayoutFileParser

parseXml:路徑、編碼、校驗(yàn)等

parseOriginalXml :將布局文件解析為描述類

private static ResourceBundle.LayoutFileBundle parseOriginalXml(
        @NonNull final RelativizableFile originalFile, @NonNull final String pkg,
        @NonNull final String encoding, boolean isViewBindingEnabled,
        boolean isDataBindingEnabled)
        throws IOException {}

//layout 標(biāo)簽判斷databinding
XMLParser.ElementContext root = expr.element();
boolean isBindingData = "layout".equals(root.elmName.getText());
//dataBinding
if (isBindingData) {
    if (!isDataBindingEnabled) {
        L.e(ErrorMessages.FOUND_LAYOUT_BUT_NOT_ENABLED);
        return null;
    }
    data = getDataNode(root);
    rootView = getViewNode(original, root);
} else if (isViewBindingEnabled) {
  //viewBindingIgnore 根布局添加這個(gè)屬性為true可以跳過生成綁定類的過程
    if ("true".equalsIgnoreCase(attributeMap(root).get("tools:viewBindingIgnore"))) {
        L.d("Ignoring %s for view binding", originalFile);
        return null;
    }
    data = null;
    rootView = root;
} else {
    return null;
}

//dataBinding <include> 元素不支持作為 <merge> 元素的直接子元素
boolean isMerge = "merge".equals(rootView.elmName.getText());
if (isBindingData && isMerge && !filter(rootView, "include").isEmpty()) {
//public static final String INCLUDE_INSIDE_MERGE = "<include> elements are not supported as direct children of <merge> elements";
    L.e(ErrorMessages.INCLUDE_INSIDE_MERGE);
    return null;
}

String rootViewType = getViewName(rootView);
String rootViewId = attributeMap(rootView).get("android:id");
//構(gòu)建布局描述的封裝類
ResourceBundle.LayoutFileBundle bundle =
    new ResourceBundle.LayoutFileBundle(
        originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
        isMerge, isBindingData, rootViewType, rootViewId);
final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
//data 數(shù)據(jù)只有 databinding 才會(huì)有的元素,viewBinding 是不會(huì)去解析的
parseData(original, data, bundle);
//解析表達(dá)式,這里面會(huì)循環(huán)遍歷元素,解析 view 的 id、tag、include、fragment 等等 xml 相關(guān)的元素,并且還有 databinding 相關(guān)的 @={ 的表達(dá)式,最后將結(jié)果緩存起來
parseExpressions(newTag, rootView, isMerge, bundle);

階段二:輸出描述文件

LayoutXmlProcessor

writeLayoutInfoFiles:這個(gè)方法的執(zhí)行點(diǎn)可以在AGP里面找到,task 為:com.android.build.gradle.tasks.MergeResources

MergeResources

@Override
public void doTaskAction(@NonNull InputChanges changedInputs) {
    ...
    SingleFileProcessor dataBindingLayoutProcessor = maybeCreateLayoutProcessor();
    if (dataBindingLayoutProcessor != null) {
        dataBindingLayoutProcessor.end();
    }
    ...
}
//maybeCreateLayoutProcessor
return new SingleFileProcessor() {

    private LayoutXmlProcessor getProcessor() {
        return processor;
    }

    @Override
    public boolean processSingleFile(
            @NonNull File inputFile,
            @NonNull File outputFile,
            @Nullable Boolean inputFileIsFromDependency)
            throws Exception {
        return getProcessor()
               .processSingleFile(
                        normalizedInputFile,
                        outputFile,
                        getViewBindingEnabled().get(),
                        getDataBindingEnabled().get());
    }
    @Override
    public void end() throws JAXBException {
        getProcessor().writeLayoutInfoFiles(getDataBindingLayoutInfoOutFolder().get().getAsFile());
    }
};

//輸出路徑可以從這里查看
artifacts.setInitialProvider(taskProvider, MergeResources::getDataBindingLayoutInfoOutFolder)
        .withName("out")
        .on( mergeType == MERGE? DATA_BINDING_LAYOUT_INFO_TYPE_MERGE.INSTANCE
                        : DATA_BINDING_LAYOUT_INFO_TYPE_PACKAGE.INSTANCE);

writeLayoutInfoFiles

public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
    //遍歷之前收集到的所有 LayoutFileBundle,寫入 xmlOutDir 路徑
    for (ResourceBundle.LayoutFileBundle layout : mResourceBundle
            .getAllLayoutFileBundlesInSource()) {
        writeXmlFile(writer, xmlOutDir, layout);
    }
}

writeXmlFile

private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,
        ResourceBundle.LayoutFileBundle layout)
        throws JAXBException {
    String filename = generateExportFileName(layout);//  fileName + '-' + dirName + ".xml";
          //遍歷之前收集到的所有 LayoutFileBundle,寫入 xmlOutDir 路徑
    writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
}

描述文件的生成路徑為:app/build/intermediates/data_binding_layout_info_type_merge/debug/out

//fragment_second-layout.xml

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="app/src/main/res/layout/fragment_second.xml"
    isBindingData="false" isMerge="false" layout="fragment_second"
    modulePackage="com.jay.vbhelper" rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
    <Targets>
        <Target tag="layout/fragment_second_0"
            view="androidx.constraintlayout.widget.ConstraintLayout">
            <Expressions />
            <location endLine="78" endOffset="51" startLine="1" startOffset="0" />
        </Target>
        <Target id="@+id/ll_info" tag="binding_1"
            view="androidx.appcompat.widget.LinearLayoutCompat">
            <Expressions />
            <location endLine="51" endOffset="50" startLine="9" startOffset="4" />
        </Target>
        <Target id="@+id/include_layout" include="layout_info" tag="binding_1">
            <Expressions />
            <location endLine="31" endOffset="42" startLine="29" startOffset="8" />
        </Target>
        <Target include="layout_info_merge" tag="binding_1">
            <Expressions />
            <location endLine="35" endOffset="53" startLine="35" startOffset="8" />
        </Target>
 
    </Targets>
</Layout>

階段三:輸出綁定類

AGP Task DataBindingGenBaseClassesTask 觸發(fā)

com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask

DataBindingGenBaseClassesTask

@TaskAction
fun writeBaseClasses(inputs: IncrementalTaskInputs) {
    // TODO extend NewIncrementalTask when moved to new API so that we can remove the manual call to recordTaskAction
    recordTaskAction(analyticsService.get()) {
        // TODO figure out why worker execution makes the task flake.
        // Some files cannot be accessed even though they show up when directory listing is
        // invoked.
        // b/69652332
        val args = buildInputArgs(inputs)
        CodeGenerator(
            args,
            sourceOutFolder.get().asFile,
            Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
            encodeErrors,
            collectResources()).run()//觸發(fā)生成流程
    }
}

//綁定類生成器
class CodeGenerator @Inject constructor(
    val args: LayoutInfoInput.Args,
    private val sourceOutFolder: File,
    private val logger: Logger,
    private val encodeErrors: Boolean,
    private val symbolTables: List<SymbolTable>? = null
) : Runnable, Serializable {
    override fun run() {
        try {
            initLogger()
            BaseDataBinder(LayoutInfoInput(args), if (symbolTables != null) this::getRPackage else null)
          //生成邏輯
                .generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath))
        } finally {
            clearLogger()
        }
    }
    ...
}

//sourceOutFolder路徑信息
creationConfig.artifacts.setInitialProvider(
    taskProvider,
    DataBindingGenBaseClassesTask::sourceOutFolder
).withName("out").on(InternalArtifactType.DATA_BINDING_BASE_CLASS_SOURCE_OUT)

BaseDataBinder

@Suppress("unused")// used by tools
class BaseDataBinder(val input : LayoutInfoInput, val getRPackage: ((String, String) -> (String))?) {
    private val resourceBundle : ResourceBundle = ResourceBundle(
            input.packageName, input.args.useAndroidX)
      //
    init {
        input.filesToConsider .forEach {
                    it.inputStream().use {
                     // 又將上面收集的 layout,將 xml 轉(zhuǎn)成 LayoutFileBundle
                        val bundle = LayoutFileBundle.fromXML(it)
                        resourceBundle.addLayoutBundle(bundle, true)
                    }
                }
        resourceBundle.addDependencyLayouts(input.existingBindingClasses)
        resourceBundle.validateAndRegisterErrors()
    }
  
  
  
    @Suppress("unused")// used by android gradle plugin
    fun generateAll(writer : JavaFileWriter) {
            // 拿到所有的 LayoutFileBundle,并根據(jù)文件名進(jìn)行分組排序
        val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
            .groupBy(LayoutFileBundle::getFileName).toSortedMap()

        layoutBindings.forEach { layoutName, variations ->
            // 將 LayoutFileBundle 信息包裝成 BaseLayoutModel
            val layoutModel = BaseLayoutModel(variations, getRPackage)
            val javaFile: JavaFile
            val classInfo: GenClassInfoLog.GenClass
            if (variations.first().isBindingData) {
                val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
                javaFile = binderWriter.write()
                classInfo = binderWriter.generateClassInfo()
            } else {
              //不是DataBinding,按照 ViewBinding 處理
              //toViewBinder 是 BaseLayoutModel 的拓展函數(shù),他會(huì)將 LayoutFileBundle 包裝成 ViewBinder 類返回 
                val viewBinder = layoutModel.toViewBinder()
              //toJavaFile 是 ViewBinder 的拓展函數(shù),通過Javapoet生成Java文件
                javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
                classInfo = viewBinder.generatedClassInfo()
            }
            writer.writeToFile(javaFile)
            myLog.classInfoLog.addMapping(layoutName, classInfo)
            variations.forEach {
                it.bindingTargetBundles.forEach { bundle ->
                    if (bundle.isBinder) {
                        myLog.addDependency(layoutName, bundle.includedLayout)
                    }
                }
            }
        }
        input.saveLog(myLog)
        // data binding will eat some errors to be able to report them later on. This is a good
        // time to report them after the processing is done.
        Scope.assertNoError()
    }
}

通過Javapoet 生成綁定類


fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
    JavaFileGenerator(this, useLegacyAnnotations).create()

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)
    addSuperinterface(ClassName.get(viewBindingPackage, "ViewBinding"))

    // TODO elide the separate root field if the root tag has an ID (and isn't a binder)
    addField(rootViewField())
    addFields(bindingFields())

    addMethod(constructor())
    addMethod(rootViewGetter())
        //如果跟標(biāo)簽是 merge  是生成的兩參數(shù)的infate 參數(shù)
    if (binder.rootNode is RootNode.Merge) {
        addMethod(mergeInflate())
    } else {
      //其它情況都是同時(shí)生成一參數(shù)和三參數(shù)的inflate方法
        addMethod(oneParamInflate())
        addMethod(threeParamInflate())
    }

    addMethod(bind())
}

生成過程總結(jié)

實(shí)時(shí)更新生成:布局文件改動(dòng)(新加/更新/刪除)后AS或AGP或立即更新綁定類,這個(gè)過程還沒找到對(duì)應(yīng)的源碼

編譯更新生成:AGP 不同的任務(wù)觸發(fā)

  • 解析xml布局文件:LayoutXmlProcessor#processResources 方法應(yīng)該是改動(dòng)布局文件的輸入口,暫時(shí)沒找到對(duì)應(yīng)的Task,收集過程支持增量更新。處理 layout_xx 目錄下面的 xxx.xml 文件,解析xml文件的過程區(qū)分 DataBinding 和 ViewBinding ,最后的產(chǎn)物是 ResourceBundle.LayoutFileBundle 以及 HashMap<String, List<LayoutFileBundle>> mLayoutBundles
  • 輸出描述文件:有AGP中的 MergeResourcesTask 觸發(fā) , 遍歷之前收集到的所有 LayoutFileBundle,寫入 xmlOutDir 路徑, 這個(gè)xml文件中描述了布局的 文件路徑、包名、布局名、控件id、控件行號(hào)等信息
  • 輸出綁定類:AGP DataBindingGenBaseClassesTask觸發(fā),將上個(gè)過程生成的布局描述xml文件再解析成 LayoutFileBundle 類信息,然后再次包裝這些信息,最后通過Javapoet 生成綁定類

TODO

  • 布局文件更新后觸發(fā)掃描和處理布局文件的操作也就是調(diào)用 processResources 方法的地方

    • 猜測AGP 和 AS 都有參與
  • 為什么點(diǎn)擊 ActivityMainBinding 會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的布局文件

    • 這個(gè)猜測應(yīng)該和編譯相關(guān),生成詞法分析器和解析器代碼
  • 為什么添加了新的布局文件還沒有編譯就獲取到綁定類,但是在data_binding_base_class_source_out路徑下沒有這個(gè)綁定類只有編譯才會(huì)看到

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容