玩轉(zhuǎn) Kotlin 委托屬性

Kotlin 屬性

要講 Kotlin 的委托屬性坠陈,要先從 Kotlin 的屬性說起婚脱,當然關(guān)于屬性的定義就不多介紹了。這里介紹一下 Kotlin 區(qū)別于 Java 獨有的 back field 的概念喜颁。用過 Kotlin 的人都知道碾牌,Kotlin 的屬性是天生帶 Setter/Getter 方法的,不過如果要重寫他們的話玷犹,寫法有所不同混滔。

var a: String = "1"
    get() = field
    set(value) {
        field = value
    }

我們可以看到,當需要重寫 Setter/Getter 方法的時候歹颓,就需要用到 field 這個新概念坯屿,它其實是代表這個域本身。有些人剛開始看到這個東西的時候晴股,可能會覺得很神秘愿伴,其實它里面的實現(xiàn)邏輯很簡單,就是對應到 Java 中 Setter/Getter 方法电湘,然后 field 在 Java 的方法中就是該屬性本身隔节,上面的代碼編譯后的代碼:

@NotNull
private String a = "1";

@NotNull
public final String getA() {
  return this.a;
}

public final void setA(@NotNull String value) {
  Intrinsics.checkParameterIsNotNull(value, "value");
  this.a = value;
}

基于這樣的邏輯,對于 Kotlin 屬性的 lateinit 修飾符的實現(xiàn)原理寂呛,就可以很簡單的推理出來怎诫,在屬性的 Getter 方法中先判斷該屬性是否被賦值,否則的話拋出異常贷痪,下面就是一個用 lateinit 修飾的屬性生成的 Getter 方法幻妓。

@NotNull
public final String getPropLateInit() {
  String var10000 = this.propLateInit;
  if(this.propLateInit == null) {
     Intrinsics.throwUninitializedPropertyAccessException("propLateInit");
  }

  return var10000;
}

講到這里,反應快的人應該能猜到到劫拢,下面要講的屬性委托是基于什么原理實現(xiàn)的了肉津。

Kotlin 委托屬性

委托屬性的聲明

定義一個委托屬性的語法是 val/var <property name>: <Type> by <expression>强胰,其中 by 后面的就是屬性的委托。屬性委托不用繼承什么特別的接口妹沙,只要擁有用 operator 修飾的 getValue()setValue() (適用 var)的函數(shù)就可以了偶洋。

需要注意的是在官方的文檔里,要求 getValue()setValue() 兩個函數(shù)提供固定的參數(shù)距糖,就像下面的例子一樣玄窝。但是事實其實并非如此,這里我們先按照官方的說法繼續(xù)悍引,后面再解釋這里的差異恩脂。

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name} in $thisRef.'")
    }
}

對于參數(shù)的描述這里做一個簡單描述:

  • thisRef,屬性的擁有者趣斤;
  • property俩块,對屬性的描述,是 KProperty<*> 類型或是它的父類唬渗;
  • value典阵,屬性的值。

委托屬性的背后實現(xiàn)

Kotlin 官方在官方標準庫里提供委托屬性的三個常用場景镊逝,作為委托屬性的范例。這里重點分析一下 lazy 的背后的實現(xiàn)原理嫉鲸,然后順帶講一下 Observablestoring 的用法撑蒜。

lazy

通過 lazy 我們可以定義一個懶加載的屬性,該屬性的初始化不會再類創(chuàng)建的時候發(fā)生玄渗,而是在第一次用到它的時候賦值座菠。

val propLazy: Int by lazy{1}

我們查看一下編譯后的 bytecode

    LINENUMBER 4 L1
    ALOAD 0
    GETSTATIC PropertiesDemo$propLazy$2.INSTANCE : LPropertiesDemo$propLazy$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD PropertiesDemo.propLazy$delegate : Lkotlin/Lazy;
L2

字節(jié)碼的可讀性太差藤树,我們反編譯一下浴滴,找到相關(guān)的代碼。

public PropertiesDemo() {
  this.propLazy$delegate = LazyKt.lazy((Function0)null.INSTANCE);
}
   
@NotNull
private final Lazy propLazy$delegate;

static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(PropertiesDemo.class), "propLazy", "getPropLazy()I")))};
   
public final int getPropLazy() {
  Lazy var1 = this.propLazy$delegate;
  KProperty var3 = $$delegatedProperties[0];
  return ((Number)var1.getValue()).intValue();
}

可以看到 Kotlin 為我們生成了一個 Lazy 類型的 propLazy$delegate 屬性岁钓,同時生成一個 getPropLazy() 方法升略,但是我們并沒有找到 propLazy 屬性的定義(這一點我們先不管,后面再說)屡限。

getPropLazy() 的實現(xiàn)里可以看到返回的是 propLazy$delegate.getValue() 的值品嚣,再看下 propLazy$delegate 的賦值是在類的構(gòu)造函數(shù)里面 this.propLazy$delegate = LazyKt.lazy((Function0)null.INSTANCE);。LazyKt 是系統(tǒng)的 Lazy.kt 文件生成的類文件钧大,找到 Lazy.ktlazy() 方法翰撑,返回的是 SynchronizedLazyImpl 的實例。

@kotlin.jvm.JvmVersion
public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

SynchronizedLazyImpl 實現(xiàn)代碼里啊央,通過 _value 用來真正保存屬性的值眶诈。_value 的默認值是 UNINITIALIZED_VALUE (一個自定義的對象)涨醋。當 _value 不是默認值的時候,就會直接把 _value 的值作為 getValue() 的返回逝撬;當 _value 還是默認值的時候东帅,就會調(diào)用 initializer 初始化表達式完成初始化,賦值給 _value 并作為 getValue() 的返回球拦。

private object UNINITIALIZED_VALUE
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                }
                else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

我們發(fā)現(xiàn) SynchronizedLazyImplgetValue() 方法并沒有帶參數(shù)靠闭,在反編譯的 getPropLazy() 代碼中 KProperty var3 = $$delegatedProperties[0]; 這個變量其實根本沒有用到,其實在正常的委托的反編譯的代碼是類似這樣的坎炼。

return (String)this.propObservable$delegate.getValue(this, $$delegatedProperties[1]);

所以說其實我們在定義委托的時候愧膀,getValue()setValue() 方法是可以不帶參數(shù)的,只是官方在編譯階段做了限制谣光,導致我們只能擁有帶參數(shù)的方法檩淋。為了驗證如果這個想法,我參考 lazy 實現(xiàn)了一個類似的功能萄金,發(fā)現(xiàn)根本不能通過編譯蟀悦。

關(guān)于 propLazy 屬性本身

前面我們有提到在生成的字節(jié)碼中,并不能找到 propLazy 這個屬性的定義氧敢,我們先看看官網(wǎng)怎么說的日戈。

class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

根據(jù)官方的文檔描述,Kotlin 會自動生成 prop$delegate 屬性孙乖,并復寫 propSetter/Getter 方法浙炼。按照這個說話的話,我們上面在編譯后的字節(jié)碼里面應該是可以找到 propLazy 屬性的唯袄。

為了驗證這個問題弯屈,我首先想到是不是因為這個屬性是私有變量,在類里面沒有使用恋拷,所以 Kotlin 編譯器為了優(yōu)化生成字節(jié)碼的數(shù)量而故意去掉了呢资厉。于是我故意在另外一個方法里嘗試輸出該屬性,但是最后發(fā)現(xiàn)在編譯后該處的使用被替換成 getPropLazy() 方法的調(diào)用蔬顾,所以看來 propLazy 是真的沒有了宴偿。

為了進一步驗證這個想法,我們還在運行時用反射的方法去獲取該屬性阎抒,發(fā)現(xiàn)的確找不到該屬性酪我,最后我們得出結(jié)論是委托屬性在編譯后會生成對應的 prop$delegate (被委托的屬性」),然后生成生成委托屬性的 Setter/Getter 方法且叁,但是該屬性本身并不在類的域定義里面都哭,這個時候嘗試用反射的方法直接拿到這個屬性是做不到的(當然你可以通過 prop$delegate 反射到你想要的內(nèi)容)。

Observable

官方推薦另外一個委托屬性的應用就是 Observable,讓屬性在發(fā)生變動的時候可以被關(guān)注的地方觀察到欺矫。

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

上面代碼的輸出:

<no name> -> first
first -> second

想了解 Observable 的實現(xiàn)方式纱新,大家可以參考前面分析 lazy 的方法,去探究一下穆趴。關(guān)于 Observable 的進一步實現(xiàn)場景脸爱,我們一直有一個想法,就是基于這個特性封裝出一套 MVVM 的框架未妹,等到這個框架實現(xiàn)以后簿废,再和大家分享。

Storing

Storing 的使用場景是被模型的屬性全部委托到 Map 的結(jié)構(gòu)去真實的存儲數(shù)據(jù)络它,用于解析 Json 或者做一些動態(tài)的事情族檬。

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

不過根據(jù)我的了解,一些 Json 的解析庫是直接用反射的方式實現(xiàn)的反序列化化戳,根據(jù)我們前面的分析单料,這里根本解析不出來,所以這個場景看來是使用不了了点楼。

關(guān)于 BufferKnifeKotterKnife

BufferKnife

Kotlin 剛推出來的時候扫尖,由于不支持 apt ,所以會導致 BufferKnife 這類用注解實現(xiàn)的框架會使用不了掠廓,但是 Kotlin 很快就意識到這個問題并推出 kapt换怖。在 kapt 推出來以后其實 BufferKnife 就可以正常使用了,我們也在我們的代碼里使用了 BufferKnife却盘。但當時 BufferKnife 在增量編譯的時候有時候的確會出一些問題狰域,導致我們那個時候最后選擇了放棄,我們自己簡單封裝下 findViewById 的操作黄橘,有興趣的可以看下 AndroidExtension ∏龋可能有些人關(guān)于 KotlinBufferKnife 的沖突信息是來自我們當時不準確的描述塞关,導致認為他們不能一起使用。而且經(jīng)過這么久的迭代子巾,我相信官方應該早就解決這個問題了帆赢。

KotterKnife

KotterKnife 這個庫的存在可能也是很多人認為 Kotlin 不能使用 BufferKnife 的一個因素。在我看來 KotterKnife 創(chuàng)建的時機是 Kotlin 還不支持 apt 的時候线梗,在 Kotlin 推出 kapt 以后這個庫就已經(jīng)不怎么更新了椰于,而且這個庫從來沒有發(fā)布過一個正式版本,所以可以看出這只是大神在用 Kotlin 做的一些新的嘗試而已(這一點我是通過查看代碼發(fā)現(xiàn) KotterKnife 主要使用「委托屬性」這個特性猜想出來的仪搔,僅供參考)瘾婿。


參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子偏陪,更是在濱河造成了極大的恐慌抢呆,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笛谦,死亡現(xiàn)場離奇詭異抱虐,居然都是意外死亡,警方通過查閱死者的電腦和手機饥脑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門恳邀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灶轰,你說我怎么就攤上這事谣沸。” “怎么了框往?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵鳄抒,是天一觀的道長。 經(jīng)常有香客問我椰弊,道長许溅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任秉版,我火速辦了婚禮贤重,結(jié)果婚禮上棵癣,老公的妹妹穿的比我還像新娘坏匪。我一直安慰自己痰滋,他們只是感情好盐杂,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布榔幸。 她就那樣靜靜地躺著豪诲,像睡著了一般霉囚。 火紅的嫁衣襯著肌膚如雪柄粹。 梳的紋絲不亂的頭發(fā)上粥惧,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天键畴,我揣著相機與錄音,去河邊找鬼突雪。 笑死起惕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的咏删。 我是一名探鬼主播惹想,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼督函!你這毒婦竟也來了嘀粱?” 一聲冷哼從身側(cè)響起激挪,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎草穆,沒想到半個月后灌灾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡悲柱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年锋喜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片豌鸡。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡嘿般,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涯冠,到底是詐尸還是另有隱情炉奴,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布蛇更,位于F島的核電站瞻赶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏派任。R本人自食惡果不足惜砸逊,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掌逛。 院中可真熱鬧师逸,春花似錦、人聲如沸豆混。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽皿伺。三九已至员辩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸵鸥,已是汗流浹背屈暗。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留脂男,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓种呐,卻偏偏與公主長得像宰翅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子爽室,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容