
通過 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的使用,有想了解的可以提提意見
- 在 Activity 中創(chuàng)建 ViewBinding 綁定類
//通過自定義屬性代理 + 反射綁定類的 inflate 方法
private val binding: ActivityMainBinding by vb()
//通過自定義屬性代理 + 傳遞 inflate 方法引用
private val binding: ActivityMainBinding by vb(ActivityMainBinding::inflate)
- 在 Fragment 中創(chuàng)建 ViewBinding 綁定類
//通過自定義屬性代理 + 反射綁定類的 inflate 方法
private val binding: FragmentMainBinding by vb()
//通過自定義屬性代理 + 傳遞 inflate 方法引用
private val binding: FragmentMainBinding by vb(FragmentMainBinding::inflate)
- 在 View 中創(chuàng)建 ViewBinding 綁定類
//通過自定義屬性代理 + 反射綁定類的 inflate 三參數(shù)方法
private val binding: MyViewBinding by vb()
//通過自定義屬性代理 + 傳遞 inflate 三參數(shù)方法引用
private val binding: MyViewBinding by vb(MyViewBinding::inflate)
- 在 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 使用的三方庫
- ViewBindingPropertyDelegate
- ViewBindingKTX
- VBHelper:這個(gè)是我寫這篇文章提取的一個(gè)庫,借鑒了上面兩個(gè)的實(shí)現(xiàn),精簡了一些代碼
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
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則只采用ViewGroup的LayoutParams作為測量的依據(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 類的生成過程
依賴源碼方便查看
//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ì)看到