Android開發Viewbinding委托實例詳解
背景
前一陣子我們在使用viewbinding的委托的時候碰到瞭點crash問題,然後發現瞭一個比較有意思的解決方案,就和大傢展開聊聊。
另外一點就是我後面打算將kotlin extensions這個插件統一移除掉。
估計大傢應該對Viewbinding的委托應該都有一定的瞭解,好幾個大佬分享過類似的文章,但是大佬們的代碼貌似也有一陣子都沒有維護瞭,所以我找到瞭一個外國大佬寫的倉庫,其實應該算是一個相對來說比較穩定的庫瞭,而且也一直處於一個持續更新迭代的狀態。
倉庫地址 ViewBindingPropertyDelegate
從Crash到有意思的源碼
委托模式是軟件設計模式中的一項基本技巧。在委托模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委托給另一個對象來處理。
Kotlin 直接支持委托模式,更加優雅,簡潔。Kotlin 通過關鍵字 by 實現委托。
上述是kotlin對於委托的釋義,Viewbinding委托就是把生成Viewbinding實例的過程交給委托類去完成,然後讓使用方可以忽略掉其中的細節,是一種非常好玩的模式瞭。
但是由於Viewbinding的特殊性,它其實就會和當前的lifecycle綁定在一起。因為我們要在銷毀的情況下把實例重置為空。否則當我們頁面重新生成的情況下,就會出現view並不是當前的頁面的困擾。
作者在定義的時候就將Viewbinding委托獲取的實例定義為瞭非空,這裡我和我的同事其實是存在一些分歧的,我認為非空其實挺合理的,但是對方並不認為。
恰巧這種空非空的問題,在實際的使用中就出現瞭很多不可預期的crash問題。比如說在一個異步操作中獲取viewbinding實例然後進行賦值操作,就會出現空指針異常。另外由於使用的是lifecycle的頁面銷毀方法,如果我們復寫瞭銷毀方法之後在設置這個值,也會出現崩潰問題。
上述問題我在幾個我之前參考的庫中其實都發現瞭對應的問題。我參考瞭Binding,還有之前彭旭說的那個也有類似的情況。
另外在fragment中,其實問題尤其的明顯。因為我們很多時候使用的fragment相關的LifecycleOwner是fragment本身,但是Android官方其實推薦我們使用的是fragment內部的view相關的LifecycleOwner。因為fragment相比較於activity,存在的問題就是多瞭幾個生命周期,比如createView,和onDestroyView。其中出現最多問題的也就是onDestroyView和onDestroy。
有趣的代碼
接下來我們看下這個作者是如何解決這些奇奇怪怪的問題的哦。
private class FragmentViewBindingProperty<in F : Fragment, out T : ViewBinding>( private val viewNeedInitialization: Boolean, viewBinder: (F) -> T, onViewDestroyed: (T) -> Unit, ) : LifecycleViewBindingProperty<F, T>(viewBinder, onViewDestroyed) { private var fragmentLifecycleCallbacks: FragmentManager.FragmentLifecycleCallbacks? = null private var fragmentManager: Reference<FragmentManager>? = null // 賦值操作 override fun getValue(thisRef: F, property: KProperty<*>): T { val viewBinding = super.getValue(thisRef, property) registerFragmentLifecycleCallbacksIfNeeded(thisRef) return viewBinding } private fun registerFragmentLifecycleCallbacksIfNeeded(fragment: Fragment) { if (fragmentLifecycleCallbacks != null) return val fragmentManager = fragment.parentFragmentManager.also { fm -> this.fragmentManager = WeakReference(fm) } fragmentLifecycleCallbacks = ClearOnDestroy(fragment).also { callbacks -> fragmentManager.registerFragmentLifecycleCallbacks(callbacks, false) } } override fun isViewInitialized(thisRef: F): Boolean { if (!viewNeedInitialization) return true if (thisRef !is DialogFragment) { return thisRef.view != null } else { return super.isViewInitialized(thisRef) } } override fun clear() { super.clear() fragmentManager?.get()?.let { fragmentManager -> fragmentLifecycleCallbacks?.let(fragmentManager::unregisterFragmentLifecycleCallbacks) } fragmentManager = null fragmentLifecycleCallbacks = null } override fun getLifecycleOwner(thisRef: F): LifecycleOwner { try { return thisRef.viewLifecycleOwner } catch (ignored: IllegalStateException) { error("Fragment doesn't have view associated with it or the view has been destroyed") } } // 有意思的代碼 private inner class ClearOnDestroy( fragment: Fragment ) : FragmentManager.FragmentLifecycleCallbacks() { private var fragment: Reference<Fragment> = WeakReference(fragment) override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) { // Fix for destroying view for case with issue of navigation if (fragment.get() === f) { postClear() } } } }
從上述代碼上我們可以看出來,其中獲取的LifecycleOwner就是我上文說的viewLifecycleOwner。這個就其實已經非常精彩瞭。
另外我們可以看下他在內部定義瞭ClearOnDestroy這個類,然後當onFragmentDestroyed觸發的時候調用postClear方法。而這個方法就是解決當我們在Destroyed中還執行瞭ViewBinding內的對象的操作的空指針問題。
經典面試題的真實使用場景,Handler.post執行。很多人覺得Handler相關的面試題都是八股文,這次我們就通過這個真是場景來給大傢說說這個有意思的問題。
首先從onFragmentDestroyed方法會執行在Fragment本身的onDestroyView之前,原來我們會在這個方法下執行引用清空的操作。然後當onDestroyView執行的時候就會出現空指針異常瞭。那麼Lifecycle有沒有提供一個在onDestroyView之後的方法呢?我們是不是可以考慮自己造一個呢?面試中,我們知道所有生命周期方法都是有主線程Handler來負責調度的,這也就是說活我麼可以把生命周期方法認為就是一個Message,當onFragmentDestroyed執行的時候,onDestroyView也已經被添加到主線程的MessageQueue中,這個時候我們在post一個runnable,那麼他的排序規則上來說,就必然在onDestroyView之後瞭。
另外一些有意思的地方
這個庫另外一個優點就是他同時支持反射和非反射的寫法。同時也支持瞭Activity,Fragment,View,FragmentDialog,ViewHolder等等。反射寫法是基於非反射寫法的,所以也保證瞭底層庫的一致性。
//非反射寫法 private val viewBinding by viewBinding(ViewProfileBinding::bind) //反射寫法 private val viewBinding: ItemProfileBinding by viewBinding()
同時他的反射相關的混淆配置文件也非常有意思。
allowoptimization 指定對象可能會被優化,即使他們被keep選項保留。所指定對象可能會被改變(優化步驟),但可能不會被混淆或者刪除。該修飾符隻對實現異常要求有用。
-keep,allowoptimization class * implements androidx.viewbinding.ViewBinding { public static *** bind(android.view.View); public static *** inflate(...); }
它隻會keep實現瞭ViewBinding的類的bind和inflate方法。因為ViewBinding會將所有的xml轉化成一個類實例,如果不刪除掉沒有實際被調用的類的情況下就會導致dex包變大,大傢對於包體積優化都是有追求的嗎。然後用瞭-keep,allowoptimization,這樣在混淆的代碼優化過程中就可以刪除掉沒有被調用的ViewBinding類瞭。
結尾
本次內卷到此結束。但是又是一個老生常談的話題,一個開源庫還是要持續的進行迭代和解決問題才能持續變好,而不是一次性的工作。擁抱變化的代碼世界,解決一些奇奇怪怪的問題,都是挺好玩的,更多關於Android開發Viewbinding委托的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Android使用ViewBinding的詳細步驟(Kotlin簡易版)
- Android ViewBinding使用介紹
- Android Fragment源碼分析Add方法
- Android實現Tab切換界面功能詳解
- Android Fragment的具體使用方式詳解