1. 概述
KAE(kotlin-android-extensions)插件在Kotlin 1.4.20版本開始被廢棄,視圖綁定(ViewBinding)是其遷移方案。
更多內(nèi)容可參照谷歌開發(fā)者公眾號(hào)在2020-12-04的如下推送:
https://mp.weixin.qq.com/s/pa1YOFA1snTMYhrjnWqIgg
2. 視圖綁定是什么
ViewBiding,視圖綁定,就是將日常開發(fā)工作中的通過xml來(lái)inflate成View對(duì)象和對(duì)視圖元素進(jìn)行findViewById進(jìn)行對(duì)象獲取的過程,封裝到了具體的視圖綁定類當(dāng)中。同時(shí),這個(gè)類的生成,交于Android Studio中完成,無(wú)需開發(fā)者手動(dòng)參與。
更具體點(diǎn),就是Android Studio會(huì)根據(jù)布局xml生成一個(gè)對(duì)應(yīng)命名的Binding類,類內(nèi)成員屬性時(shí)帶有xml根元素和每個(gè)帶有id的視圖元素(除非顯式設(shè)置tools:viewBindingIgnore="true"),并其中帶有對(duì)應(yīng)xml的靜態(tài)inflate和bind方法用以構(gòu)造視圖綁定對(duì)象。
一言以概之,視圖綁定類就是對(duì)xml視圖元素的包裹類!
所以,只要開發(fā)者會(huì)寫視圖的inflate和findViewById方法,那么便無(wú)學(xué)習(xí)成本地會(huì)用視圖綁定!
3. 視圖綁定的理解視角
概述如下:
視圖綁定類對(duì)象是對(duì)xml視圖元素的包裹類對(duì)象;
視圖綁定的靜態(tài)
bind方法就是通過對(duì)根視圖逐一findViewById獲取元素對(duì)象引用,進(jìn)而構(gòu)造視圖綁定類對(duì)象;視圖綁定的靜態(tài)
inflate方法是從xml布局創(chuàng)建視圖對(duì)象方法并同時(shí)調(diào)用bind函數(shù)獲得視圖綁定對(duì)象的封裝;
所以,視圖綁定并沒有帶來(lái)新的學(xué)習(xí)內(nèi)容,只是新壺(視圖綁定類)裝舊酒(inflate、findViewById),而且這個(gè)”裝酒“的過程交給Android Studio去完成,進(jìn)而減少了開發(fā)者的重復(fù)性工作。
所以,視圖綁定可以說是幾乎無(wú)學(xué)習(xí)成本的!
如果對(duì)概述的內(nèi)容有所疑惑,請(qǐng)繼續(xù)往下看:
假定有以下Java代碼:
View view = LayoutInflater.from(context).inflate(R.layout.view_test, parent, false);
TextView nameTv = view.findViewById(R.id.name_tv);
TextView mailTv = view.findViewById(R.id.mail_tv);
這類代碼應(yīng)該是不采用ButterKnife、KAE、ViewBinding等任何方案時(shí),非常常見的一種代碼方式。
如果用視圖綁定,這里就縮減成如下方式:
ViewTestBinding binding = ViewTestBinding.inflate(LayoutInflater.from(context), parent, false);
拿到視圖綁定引用后,就可以非常方便地任意訪問xml里的對(duì)應(yīng)元素了:
binding.getRoot() // xml根視圖
binding.nameTv; // xml中的name_tv
binding.mailTv; // xml中的mail_tv
這樣,即使xml里有再多的視圖元素要訪問,再也不用手動(dòng)一個(gè)個(gè)地去寫findViewById了,而是轉(zhuǎn)而通過視圖綁定對(duì)象去統(tǒng)一訪問。
所以,視圖綁定類就是對(duì)xml視圖元素的包裹類,僅此而已。
而視圖綁定的類,由Android Studio自行根據(jù)xml實(shí)際情況生成并修改,并不用開發(fā)者手動(dòng)維護(hù)。
那么,視圖綁定是何時(shí)給xml里的視圖元素對(duì)應(yīng)找到視圖對(duì)象引用的呢?答案就在視圖綁定類的靜態(tài)方法里。
每個(gè)視圖綁定類都必定包含下面三個(gè)靜態(tài)方法:
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater)
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent)
@NonNull
public static ViewTestBinding bind(@NonNull View rootView)
這也是初看視圖綁定時(shí)比較困惑的地方,因?yàn)楦黝愇臋n中用了不同的靜態(tài)方法去獲取視圖對(duì)象實(shí)例。
但是,這三者其實(shí)是統(tǒng)一的,一個(gè)參數(shù)的inflate方法調(diào)用了三個(gè)參數(shù)的inflate方法,三個(gè)參數(shù)的inflate方法其實(shí)調(diào)用的是bind方法。
也就是說,無(wú)論用的是哪個(gè)靜態(tài)方法去獲取視圖對(duì)象對(duì)象,其實(shí)最終都是用的都是bind!
而對(duì)于inflate方法,其實(shí)也不過是LayoutInflater#inflate(int, ViewGroup, boolean)的簡(jiǎn)單封裝:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
目的,是為了封裝對(duì)應(yīng)的xml布局到inflate方法內(nèi)部(視圖綁定類命名本身已經(jīng)與對(duì)應(yīng)的xml布局文件所一一對(duì)應(yīng)起來(lái))。
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.view_test, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
而bind方法里,則是最后根據(jù)對(duì)于的根視圖來(lái)對(duì)所有視圖元素進(jìn)行findViewById的過程(枯燥乏味的過程,這里不貼代碼了,況且每個(gè)xml里會(huì)有不同的id和數(shù)量);
想明白了上面的過程,其實(shí)視圖綁定還可以這么用
View view = LayoutInflater.from(context).inflate(R.layout.view_test, parent, false);
ViewTestBinding binding = ViewTestBinding.bind(view);
這樣出來(lái)的binding引用和直接用inflate獲取的引用其實(shí)是一樣的效果,只不過這種寫法,有兩點(diǎn)重復(fù)的內(nèi)容了:
- 視圖綁定類本身即是與layout的文件名匹配生成的,兩者出現(xiàn)其一就足夠了,這里兩者都出現(xiàn)了;
- 這里的view引用和通過
binding.getRoot()獲得的引用會(huì)是同一個(gè)(但類型不同),同一個(gè)引用可以通過兩種方式訪問了;
這兩點(diǎn)影響較小,不過各處文檔中都沒有出現(xiàn)這種寫法,個(gè)人猜測(cè),可能是考慮到啰嗦與重復(fù)了吧,畢竟一個(gè)函數(shù)調(diào)用能做完的事情,為什么要分兩步?
4. 視圖綁定在Activity/Fragment中的用法
這節(jié)來(lái)結(jié)合官方開發(fā)文檔中視圖綁定的用法實(shí)例來(lái)看下,
注,下面示例代碼來(lái)自:https://developer.android.google.cn/topic/libraries/view-binding#kotlin
4.1 在Activity中使用視圖綁定
官方樣例代碼:
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
看到這個(gè)代碼,怎么還能說視圖綁定是無(wú)學(xué)習(xí)成本的呢?不是還要看視圖綁定的inflate方法怎么用嗎?
但是呢,有沒覺得,這個(gè)inflate方法很眼熟?其實(shí)吧,這個(gè)寫法,是精簡(jiǎn)寫法了,不信?一步步來(lái)拆解一下:
沒有視圖綁定之前,其實(shí)可以這樣去給Activity去setContentView:
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val view = layoutInflater.inflate(R.layout.result_profile, null, false)
setContentView(view)
}
只是,普遍寫法是:setContentView(R.layout.result_profile)
普遍寫法不代表只有這種方式,而視圖綁定方式,只是將上面layoutInflater.inflate的方式進(jìn)行了一層封裝并同時(shí)構(gòu)造了視圖綁定對(duì)象。
其實(shí)吧,如果不想改變?cè)瓉?lái)的setContentView(R.layout.xxx)的方式,又想獲取視圖綁定實(shí)例,也是有辦法的,如下:
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.result_profile)
binding = ActivityMainBinding.bind(findViewById<ViewGroup>(android.R.id.content).getChildAt(0))
}
效果和官方示例其實(shí)是一樣的,只是,麻煩和啰嗦了一些。這里僅為說明視圖綁定的情況,不代表個(gè)人建議如上寫法。
4.2 在Fragment中使用視圖綁定
官方樣例代碼:
private var _binding: ResultProfileBinding? = 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? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
可能有一部分開發(fā)者會(huì)疑惑,為什么這里要對(duì)視圖綁定屬性進(jìn)行置空呢?上面Activity使用過程中卻不需要?
概述性的描述為:不置空會(huì)有可能的內(nèi)存泄漏風(fēng)險(xiǎn)。
但是,這個(gè)并不是視圖綁定所帶來(lái)的風(fēng)險(xiǎn),更直接地說,這個(gè)鍋,視圖綁定不接。
為講清楚這個(gè),先從Java代碼中說起,在沒有使用視圖綁定之前,有下面代碼:
private View mRootView;
@Override
public View onCreateView (LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.result_profile, container, false);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mRootView = null;
}
如果在onDestroyView沒有對(duì)mRootView進(jìn)行置空,那么原理上同樣地存在內(nèi)存泄漏風(fēng)險(xiǎn)。
事實(shí)上,上述mRootView的寫法即使沒有在onDestroyView中進(jìn)行相應(yīng)的置空,大多情況下也不會(huì)內(nèi)存泄漏(LeakCanary等內(nèi)存泄漏檢測(cè)結(jié)果),這又是為什么呢?
這是因?yàn)榇蠖嗍褂肍ragment的過程中,很少見有FragmentTransaction#attach和FragmentTransaction#detach的使用,又或者不常用FragmentPagerAdapter(里面對(duì)于Fragment的切換是用attach/detach方法),所以Fragment與View的生命周期幾乎等同進(jìn)而不會(huì)有內(nèi)存泄漏問題。
回過來(lái),視圖綁定類就是對(duì)xml視圖元素的包裹類,對(duì)于視圖綁定在Fragment中的內(nèi)存泄漏風(fēng)險(xiǎn),并不是視圖綁定的鍋,而是對(duì)Fragment和View之間的設(shè)計(jì)以及內(nèi)存泄漏原理理解不足夠產(chǎn)生的“偏見”,如果能處理好Fragment與View之間的關(guān)系,自然也能在Fragment中用好視圖綁定。
題外話,其實(shí)在業(yè)務(wù)Fragment中保存mRootView其實(shí)是多余的,在onCreateView執(zhí)行后并且在onDestroyView執(zhí)行后不久的這段生命周期內(nèi),通過Fragment#getView方法拿出來(lái)的即是onCreateView中的返回值,有興趣的可以從源碼上查看Fragment中的生命周期與視圖View之間的詳細(xì)設(shè)計(jì)。
5. 關(guān)于視圖綁定需要說明的
5.1 視圖綁定跟模塊相關(guān)
模塊(module)下的build.gradle的中,增加:
android {
...
buildFeatures {
viewBinding = true
}
...
}
只有增加了該內(nèi)容的module才會(huì)開啟視圖綁定功能,為module下的layout中的xml生成對(duì)應(yīng)的視圖綁定類,沒有該部分內(nèi)容的module則不會(huì)
由于視圖綁定類是代碼文件,可以引用別的module中的視圖綁定類來(lái)用。
5.2 關(guān)于tools:viewBindingIgnore="true"
放入xml根布局,則不生成對(duì)應(yīng)的視圖綁定類。
放入視圖id元素聲明統(tǒng)計(jì),則不生成該id對(duì)應(yīng)的視圖屬性。
屬于優(yōu)化生成的視圖綁定類代碼內(nèi)容的優(yōu)化項(xiàng)。
5.3 視圖綁定對(duì)于xml中的include處理
視圖綁定類的是以xml布局文件作為維度的,所以僅會(huì)對(duì)當(dāng)前xml中的視圖id元素進(jìn)行屬性生成,而xml中include另一個(gè)xml的內(nèi)容相關(guān)不會(huì)進(jìn)行相應(yīng)屬性生成。
如果對(duì)第4節(jié)的內(nèi)容理解清楚,那其實(shí)include的內(nèi)容就是通過視圖綁定的靜態(tài)bind方法就可以生成相應(yīng)的視圖綁定對(duì)象來(lái)使用。
當(dāng)然,在include標(biāo)簽里新聲明id且被include的xml里沒有使用merge標(biāo)簽時(shí)也可以一步到位,但是吧,如果被include的xml根布局也有id,情況就有點(diǎn)多了。
這里不繼續(xù)討論,因?yàn)椴煌瞬煌瑘?chǎng)景可能對(duì)這部分有不同的選擇,更多場(chǎng)景應(yīng)該是findViewById和視圖id之間的關(guān)聯(lián)和理解,而非視圖綁定本身需要關(guān)注的內(nèi)容。
6. 關(guān)于對(duì)視圖綁定的進(jìn)一步封裝
封裝是一種避免犯錯(cuò)規(guī)范使用的一種好方案,但是視圖綁定從本身設(shè)計(jì)上和使用上就足夠簡(jiǎn)單,為了更簡(jiǎn)潔和更安全,再進(jìn)行一層一層的封裝,一定程度加大了學(xué)習(xí)理解和后續(xù)維護(hù)拓展的成本。
與其理解使用對(duì)于視圖綁定與各項(xiàng)其他內(nèi)容的封裝以及維護(hù)相應(yīng)的邏輯,不如把重點(diǎn)放在視圖綁定和視圖框架本身的理解和使用上。
目前,網(wǎng)上對(duì)于視圖綁定有各種八仙過海式的封裝使用,如反射、基類封裝、Kotlin的具化泛型、Kotlin屬性委托方式等等,封裝的角度和考慮往往相當(dāng)深入,其中個(gè)人看到的屬性委托方式封裝視圖綁定的技術(shù)內(nèi)容更是將Kotlin的屬性委托、Jetpack中的LifeCycle組件、內(nèi)存泄漏問題研究得妥妥帖帖。
如果從學(xué)習(xí)和深入的角度去看待各種封裝思想和用處,這是非常好的一件事,但是反過來(lái),視圖綁定本身使用上就不復(fù)雜不難用,看到各路封裝容易讓不明所以的人容易對(duì)視圖綁定有個(gè)不容易用的錯(cuò)覺。
即使不進(jìn)行任何封裝,本身視圖綁定的使用就是一行或者幾行代碼的事情,至于對(duì)于內(nèi)存泄漏,本身的解決方案也僅是在恰當(dāng)?shù)奈恢弥每找膊⒉粡?fù)雜,這里更多需要注意的是對(duì)于Fragment/View/內(nèi)存泄漏的理解,治標(biāo)不如治本。
7. 為什么要從KAE換用視圖綁定
無(wú)法否認(rèn)的一點(diǎn)是,視圖綁定在使用上,并沒有KAE中直接通過id訪問這么方便,那么為什么要從KAE換用視圖綁定方案呢?
個(gè)人角度的一些想法:
- KAE僅支持Kotlin代碼,視圖支持Kotlin和Java;
另一種觀點(diǎn):現(xiàn)在主推Kotlin開發(fā)代碼了,Java是否能用影響不大。
-
KAE在每個(gè)使用id訪問視圖的地方使用
HashMap并開發(fā)代碼中嵌入了相應(yīng)代碼,造成了運(yùn)行時(shí)內(nèi)存額外占用和更大的代碼打包體積;
視圖綁定也會(huì)產(chǎn)生相應(yīng)的代碼生成,不過多處使用的視圖綁定都是同一個(gè)所以代碼打包增加量是個(gè)常數(shù),包裝類也有內(nèi)存占用成本,但是成本也小于HashMap的使用。
另一種觀點(diǎn):HashMap這一點(diǎn)點(diǎn)的內(nèi)存占用成本和代碼打包體積在整體工程項(xiàng)目下往往可以忽略不計(jì)。
- KAE并不是空安全的,而視圖綁定對(duì)空安全的支持更好;
因?yàn)橐粋€(gè)視圖id即使不在當(dāng)前的視圖布局內(nèi),KAE使用上在代碼開發(fā)階段也可以通過id去訪問,只有在運(yùn)行時(shí)才會(huì)出現(xiàn)異常,而視圖綁定則因?yàn)橥ㄟ^視圖綁定對(duì)象限定了視圖布局的范圍更安全;再者在橫豎屏的xml有不同元素時(shí),視圖綁定會(huì)標(biāo)注視圖是否可空,而KAE有空安全風(fēng)險(xiǎn)。
杠:也不是什么問題,運(yùn)行到了時(shí)候出現(xiàn)異常再檢查修改,分分鐘的事。
答:萬(wàn)一這個(gè)視圖訪問的邏輯業(yè)務(wù)層次很深,不容易觸發(fā)該部分業(yè)務(wù)邏輯的話,存在一定風(fēng)險(xiǎn)。
杠:那就是代碼邏輯性和測(cè)試的問題了,規(guī)范嚴(yán)謹(jǐn)?shù)拈_發(fā)者不會(huì)犯這樣的錯(cuò)誤的。
答:。。。。。。
最后,也是最重要的一點(diǎn),那就是KAE已經(jīng)被官方廢!棄!了!,替代方案是視圖綁定。
8. 總結(jié)
其實(shí)視圖綁定處于一個(gè)挺尷尬的時(shí)期,前有KAE更方便的使用,后有Jetpack Compose這種聲明式UI架構(gòu)正在路上。KAE被廢棄了,開發(fā)者應(yīng)避免使用被廢棄的內(nèi)容;Jetpack Compose是聲明式UI,對(duì)于現(xiàn)存的命令式UI開發(fā)框架是個(gè)較大的沖擊,加上官方在逐步優(yōu)化Compose性能、新框架的學(xué)習(xí)遷移成本等因素,這個(gè)應(yīng)該仍需一定的時(shí)間和過程。
好在,視圖綁定內(nèi)容本身幾乎沒有學(xué)習(xí)成本,使用上也并不麻煩,所以,在現(xiàn)有的命令式UI開發(fā)框架下,仍可一戰(zhàn)!