好玩系列:優雅的處理ButterKnife和KAE被廢棄

最近反思了一下近期的工做,突然就出現了一個想法,將平時乾的好玩的事情,整理成一個系列,和你們分享一下,想來想去也沒想好這個系列叫啥,索性就叫好玩系列得了。java

文中涉及的代碼均在此處能夠找到android

前言

若是你的項目中使用了ButterKnife或者Kotlin-Android-Extention(KAE)插件,近半年你必定關注過以下信息:git

Attention: This tool is now deprecated. Please switch to view binding. Existing versions will continue to work, obviously, but only critical bug fixes for integration with AGP will be considered. Feature development and general bug fixes have stopped. -- ButterKnifegithub

Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them in switch case statements Inspection info:Avoid the usage of resource IDs where constant expressions are required. A future version of the Android Gradle Plugin will generate R classes with non-constant IDs in order to improve the performance of incremental compilation.算法

Issue id: NonConstantResourceId -- lintexpress

The 'kotlin-android-extensions' Gradle plugin is deprecated. Please use this migration guide (goo.gle/kotlin-andr…) to start working with View Binding (developer.android.com/topic/libra…) and the 'kotlin-parcelize' plugin.安全

是的,這兩個在Android中使用面很廣的內容被標記爲廢棄了。markdown

對於ButterKnife,被廢棄的緣由是:從AGP-5.0版本開始,R類生成的值再也不是常量app

對於KAE,問題以下:ide

  • 類型安全:res下的任何id均可以被訪問,有可能因訪問了非當前Layout下的id而出錯,難以利用lint等靜態代碼校驗
  • 空安全:運行時可能出現NPE
  • 兼容性:只能在kotlin中使用,java不友好
  • 侷限性:不能跨module使用

按照官方或者社區的推薦,替代方案仍是迴歸到findViewById or ViewBinding or DataBinding.

將來可能替代XML描述佈局文件的技術:Compose尚未真正到來,並且一時半會也不可能把原先的內容所有遷移到Compose實現,因此咱們仍是要老老實實迴歸到上面的三個方案。

有些同窗知識面廣一點,立馬想到了psi,經過分析代碼文件的psi樹,實現代碼轉換,直接搞一個插件來處理ButterKnife的遷移問題。

固然,這篇文章並不許備去講psi,雖然這是一個挺好玩的東西。下次有時間會專門寫一個好玩的psi

思考1:爲何要廢棄ButterKnife

由於AGP生成的R類資源值再也不是常量,不管是library仍是application,那麼要繼續再思考一個問題:library的R類資源也不是常量,原先ButterKnife是怎麼處理的? 咱們知道,Butterknife有運行時反射用法,也有編譯期使用apt預生成代碼的用法。bk提供了gradle插件,用於copy原始R類內容,生成R2類,R2復刻了R的內容,但均爲常量。由於註解中的內容,是須要在編譯期肯定,它被要求爲常量,而且在編譯時被優化。但咱們知道,經過字節碼技術,能夠修改不少東西,不管是一個常量的值,仍是索性連類都給換了。 一旦這個值被修改,註解中的信息便爲謬誤。但由於R2的存在,咱們能夠經過常量值反向獲取到常量的名字,從而去使用R類。

思考2:是否是Butterknife全部的代碼都沒有意義了?

顯然不是,由於findviewbyid還沒用被革命性改變,bk中全部的核心代碼仍是有用的 若是你使用的apt方式,那麼就有意思了,對於一個特定的target,bk生成的綁定代碼徹底是沒有「廢棄」風險的,咱們徹底能夠拷貝其中的邏技,或者直接對生成類實行「拿來主義」 最終,咱們只須要扔掉bk的gradle插件,註解和apt處理器,歲月靜好。 若是你使用的是運行時反射方案,我不排斥運行時反射,雖然他會多耗一些時間,若是你不介意耗費更多的時間,徹底能夠改造bk的註解和邏輯,雖然它很好玩,但這並非一個值得推薦的作法。

思考3:kae又是怎麼幫助咱們找到view的

沒錯,仍是經過findviewbyid,它被廢棄並非犯了什麼大錯,只是不在適應潮流,且有各類各樣的小毛病。 咱們以Fragment爲例子,看一下編譯器爲咱們植入的代碼:

public android.view.View _$_findCachedViewById(int var1) {
    if (this._$_findViewCache == null) {
       this._$_findViewCache = new HashMap();
    }

    android.view.View var2 = (android.view.View)this._$_findViewCache.get(var1);
    if (var2 == null) {
       android.view.View var10000 = this.getView();
       if (var10000 == null) {
          return null;
       }

       var2 = var10000.findViewById(var1);
       this._$_findViewCache.put(var1, var2);
    }

    return var2;
 }

 public void _$_clearFindViewByIdCache() {
    if (this._$_findViewCache != null) {
       this._$_findViewCache.clear();
    }

 }
複製代碼

以及:

// $FF: synthetic method
public void onDestroyView() {
  super.onDestroyView();
  this._$_clearFindViewByIdCache();
}
複製代碼
//源碼
vMallAccountTitleBar.setTitle("個人錢包")

//反編譯結果
((BarStyle4)this._$_findCachedViewById(id.vMallAccountTitleBar))
	.setTitle((CharSequence)"個人錢包");
     
複製代碼

能夠很輕易的發現,具備多種場景下潛在的npe風險。本質上仍是在使用findViewByID機制

思考4:是否能夠最小程度重構實現從bk切換到databinding或者viewbinding

首先仍是要粗略提一下databinding和viewbinding。記憶中databinding技術先於viewbinding,是Google提供的聲明式UI解決方案,這裏必需要岔開一句:什麼是聲明式UI?

這裏我用SQL舉個例子類比, select * from t where 't.id' = 1, 這就是聲明式,聲明一個符合規則的定則,讓對應的系統執行,獲得目標結果。相應的,對立面就是命令式,命令式須要準確的指出每一步操做的具體指令,以完成一個特定的算法。

膚淺的總結,聲明式是底層實現了一類行爲的抽象,其核心的算法或者控制段均被封裝,只須要控制輸入,便可獲得輸出。而命令式則徹底須要自行實現。

理解了這一點,咱們就會意識到,databinding自己不該該對外暴露這些view,只是這麼幹的話,項目遷移成本就會變大,因此仍是選擇了開放,這也就有了後來的viewbinding。

言歸正傳,原先用bk,咱們須要一個根view做爲起始點,以實現視圖綁定,基本是找到Activity#setContentView(Int id)後activity的decorview,或者是viewholder#getRoot(),或者是開發者inflate獲得的一個view等等。

不難判斷,若是完全的修改代碼,從基類出發應該是沒什麼方案。只能進行一件枯燥乏味的事情

思考5:若是是kotlin語言下,利用屬性代理是否能夠簡化代碼修改

延伸:屬性委託指的是一個類的某個屬性值不是在類中直接進行定義,而是將其託付給一個代理類,從而實現對該類的屬性統一管理。 屬性委託語法格式:

val/var <屬性名>: <類型> by <表達式>

實踐1:屬性代理替代BK的註解

先定一個小目標,咱們會將註解形式變成相似如下代碼的形式:

val tvHello1 by bindView<TextView>(R.id.dialog)

val tvHello by bindView<TextView>(viewProvider, R.id.hello, lifecycle) {
    bindClick { changeText() }
}

val tvHellos by bindViews<TextView>(
    viewProvider,
    arrayListOf(R.id.hello1, R.id.hello2),
    lifecycle
) {
    this.forEach {
        it.bindClick { tv ->
            Toast.makeText(tv.context, it.text.toString(), Toast.LENGTH_SHORT).show()
        }
    }
}
複製代碼

那麼咱們須要先定義一個屬性代理類,並實現操做符,以bindView爲例,

咱們先緩一緩,定義一個基類,接受屬性持有者的生命週期,以實現其生命週期走到特定節點時釋放依賴

abstract class LifeCycledBindingDelegate<F,T>(lifecycle: Lifecycle): ReadOnlyProperty<F,T> {

    protected var property: T? = null

    init {
        lifecycle.onDestroyOnce { destroy() }
    }

    protected open fun destroy() {
        property = null
    }
}

internal class OnDestroyObserver(var lifecycle: Lifecycle?, val destroyed: () -> Unit) :
    LifecycleEventObserver {
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        val lifecycleState = source.lifecycle.currentState
        if (lifecycleState == Lifecycle.State.DESTROYED) {
            destroyed()
            lifecycle?.apply {
                removeObserver(this@OnDestroyObserver)
                lifecycle = null
            }
        }
    }
}

fun Lifecycle.onDestroyOnce(destroyed: () -> Unit) {
    addObserver(OnDestroyObserver(this, destroyed))
}
複製代碼

這時候咱們來處理findViewById的核心部分

class BindView<T:View>(
    private val targetClazz: Class<T>,
    private val rootViewProvider: ViewProvider,
    @IdRes val resId: Int,
    lifecycle: Lifecycle,
    private var onBind: (T.() -> Unit)?
):LifeCycledBindingDelegate<Any,T>(lifecycle) {


    override fun getValue(thisRef: Any, property: KProperty<*>): T {
        return this.property ?: let {

            val rootView = rootViewProvider.provide()
            val v = rootView.findViewById<T>(resId)
                ?: throw IllegalStateException(
                    "could not findViewById by id $resId," +
                            " given name: ${rootView.context.resources.getResourceEntryName(resId)}"
                )
            return v.apply {
                this@BindView.property = this
                onBind?.invoke(this)
                onBind = null
            }
        }
    }
}
複製代碼

咱們須要幾樣東西以支持:

View#<T extends View> T findViewById(@IdRes int id)

對應了目標類根View提供者目標view的id屬性持有者的生命週期初次屬性初始化後的附加邏輯

至於BindViews,咱們如法炮製便可。

這時候會發現,這樣使用太累了,對於Activity、Fragment、ViewHolder等常見的類而言,雖然他們提供根視圖等內容的方式有所差異,但這種行爲基本是能夠抽象的。

以ComponentActivity爲例,咱們只須要定義擴展函數:

inline fun <reified T : View> ComponentActivity.bindView(@LayoutRes resId: Int) =
    BindView<T>(
        targetClazz = T::class.java,
        rootViewProvider = object : ViewProvider {
            override fun provide(): View {
                return this@bindView.window.decorView
            }
        },
        resId = resId,
        lifecycle = this.lifecycle,
        onBind = null
    )
複製代碼

就能夠比較方便的使用,剩下來的Fragment、ViewHolder之類的東西,講起來太囉嗦了,都是如法炮製

再定義一個大而全的:

inline fun <reified T : View> Any.bindView(
    rootViewProvider: ViewProvider,
    @LayoutRes resId: Int,
    lifecycle: Lifecycle,
    noinline onBind: (T.() -> Unit)?
) =
    BindView<T>(
        targetClazz = T::class.java,
        rootViewProvider = rootViewProvider,
        resId = resId,
        lifecycle = lifecycle,
        onBind = onBind
    )
複製代碼

實際項目中想怎麼用徹底看實際就好了。

思考6:讓DataBinding和ViewBinding擁有一樣的特性是否有價值

固然是有價值的,一個大項目中,尤爲是進行了模塊化拆分,不一樣模塊使用不一樣的技術是很正常的,DataBinding和ViewBinding並存的狀況必定會發生,雖然我並無真正遇到過同時使用的,而且並不清楚同時使用會不會有bug

實踐2:支持DataBinding和ViewBinding

由於筆者項目中沒有使用ViewBinding,咱們就粗暴的只實現DataBinding了,其實都是獲取Binding類實例而已,機制是一致的,ViewBinding能夠如法炮製

得益於咱們上面定義的基類,咱們能夠直接幹一個處理DataBinding的子類了

class BindDataBinding<T : ViewDataBinding>(
    private val targetClazz: Class<T>,
    private val inflaterProvider: LayoutInflaterProvider,
    @LayoutRes val resId: Int,
    lifecycle: Lifecycle,
    private var onBind: (T.() -> Unit)?
) : LifeCycledBindingDelegate<Any, T>(lifecycle) {

    override fun getValue(thisRef: Any, property: KProperty<*>): T {

        return this.property ?: let {

            val layoutInflater = inflaterProvider.provide()
            val bind = DataBindingUtil.bind<T>(layoutInflater.inflate(resId, null))
                ?: throw IllegalStateException(
                    "could not create binding ${targetClazz.name} by id $resId," +
                            " given name: ${layoutInflater.context.resources.getResourceEntryName(resId)}"
                )
            return bind.apply {
                this@BindDataBinding.property = this
                onBind?.invoke(this)
                onBind = null
            }
        }

    }
}
複製代碼

依葫蘆畫瓢,咱們直接搞定inflate方式獲取Binding。

仔細一想,這還不夠,原本咱們將佈局改成DataBinding模板,有多種方案設置視圖,使用屬性代理,有一個目的是:讓設置視圖獲得Binding實例之間減小限制。

再幹一個:

class FindDataBinding<T : ViewDataBinding>(
    private val targetClazz: Class<T>,
    private val viewProvider: ViewProvider,
    lifecycle: Lifecycle,
    private var onBind: (T.() -> Unit)?
) : LifeCycledBindingDelegate<Any, T>(lifecycle) {

    override fun getValue(thisRef: Any, property: KProperty<*>): T {

        return this.property ?: let {
            val view = viewProvider.provide()
            val bind = DataBindingUtil.bind<T>(view)
                ?: throw IllegalStateException(
                    "could not find binding ${targetClazz.name}"
                )
            return bind.apply {
                this@FindDataBinding.property = this
                onBind?.invoke(this)
                onBind = null
            }
        }
    }
}
複製代碼

咱們又能夠經過bind的方式,從一個View發現其binding了。尋找binding設置視圖的前後,就能夠靈活選擇了。

加上一些擴展方法後,咱們就能夠開心的使用了:

class MainActivity2 : AppCompatActivity() ,ViewProvider{
    val binding by dataBinding<ActivityMainBinding>(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        binding.hello.text = "fragment"
        binding.hello.bindClick {

        }
    }

    override fun provide(): View {
        return window.decorView.findViewById(R.id.ll_root)
    }
}
複製代碼

總結

正如開篇提到的,好玩系列其出發點必定是好玩,它極可能是對一個問題展開的一次腦暴和嘗試,不必定是一個真正成熟的特定問題通用解法。

這一篇,咱們從Butterknife的廢棄和KAE的廢棄開始思考,回顧了二者的實現原理和被廢棄的緣由,再到尋找遷移方案,並進行了實踐。拋開還未涉及到的PSI,基本能夠畫上一個階段性句號了。

再次貼上代碼連接: UIBinding,若是本文中的內容對你有一絲絲的幫助,但願能夠獲得點贊支持。

補充:2021-1-25 再補充一段內容重點。

  • 對於Java編寫的業務,不牽涉kae,只涉及bk,我的建議拷貝其生成類核心邏輯,再刪除相關注解點。
  • 對於kotlin編寫的業務,bk內容能夠和Java同樣處理,kae相關內容考慮使用屬性代理方式,增長全局變量。
  • 這一波重構,並不適合在基類作手腳。
  • 對於尚未遷移到databinding或者viewbinding的內容,配合屬性代理遷移到databinding或者viewbinding也不麻煩。
相關文章
相關標籤/搜索