深入分析Kotlin的屬性代理機制

前言
熟悉Kotlin的人可能知道,類代理是一種基於父類或者介面的實現,而在代理屬性這邊沒有這種限制,而且這些代理物件的公共方法的引數中還包含了委託物件,這意味著在代理物件中也可以呼叫委託物件的公共方法。Kotlin的標準庫中就包含了許多使用代理屬性的實現,比如lazy。
免費獲取更多安卓開發架構的資料(包括Fultter、高階UI、效能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線網際網路公司關於android面試的題目彙總可以加入 【騰訊@安卓中高階進階】
正文
我們先來學習下寫標準庫的大佬怎麼玩的,lazy的用法很簡單:
val num by lazy { BigInteger.valueOf(120).modPow(BigInteger.valueOf(120)) }
我們假設num的獲取是耗時操作,而且我們還不一定要用到它,一個比較好的策略就是惰性求值,用到時再去獲取,並把結果快取起來避免重複的運算,提高程式碼的效能,lazy提供的就是這樣一種機制。
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
這裡是一個高階函式,接受一個lambda作為引數,返回了一個SynchronizedLazyImpl的物件,現在還看不出是什麼東西,我們再往裡面看:
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) }
哦,這是一個實現了Lazy介面的類,我們可以看到,value的get方法使用了synchronized關鍵字來確保執行緒安全,我們傳入的lambda會在這裡被呼叫計算出一個結果,然後結果被快取在_value中,下次再訪問就不會重新計算結果了。
而Lazy的結構如下:
public interface Lazy<out T> { public val value: T public fun isInitialized(): Boolean } 結構很簡單,沒什麼東西,我們再回到lazy函式的過載方法: public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) }
哦喲,使用這個方法我們可以顯式指定一個LazyThreadSafetyMode,從名字上看它跟執行緒安全有關係,而且每個模式都使用了不同的Lazy實現,除了我們剛剛討論的SynchronizedLazyImpl,還有其它一些。
先來看LazyThreadSafetyMode,這是一個列舉類,支援三種模式:
- SYNCHRONIZED 使用鎖來確保只有一個執行緒來求值。
- PUBLICATION 允許多個執行緒來初始化值,但是隻有第一個返回的值有效。
- NONE 允許多個執行緒來初始化值,但是行為就不確定了。
意思就是我們的app執行在單執行緒裡我們就可以直接把mode傳為NONE囉,避免加鎖帶來的開銷唄,那在Android開發過程中,我們可以這麼用:
private val rv by lazy(LazyThreadSafetyMode.NONE) { findViewById<RecyclerView>(R.id.rv) }
因為系統只會在UI執行緒上操作UI,所以我們不需要擔心有什麼併發訪問,稍加包裝我們甚至可以自己實現一個KotterKnife。
再來看看None模式下使用的UnsafeLazyImpl:
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer private var _value: Any? = UNINITIALIZED_VALUE override val value: T get() { if (_value === UNINITIALIZED_VALUE) { _value = initializer!!() initializer = null } @Suppress("UNCHECKED_CAST") return _value as T } 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) }
還是主要看value的get方法,我們可以看到,get方法只會檢查value有沒有被賦值,然後計算出一個結果或者返回快取的值,但是這裡並沒有加鎖,就不會保證執行緒安全,常見的併發問題都有可能在這裡發生。
最後到了PUBLICATION,它也允許多執行緒訪問,但是跟NONE有些微妙的差別,來看一個小例子,來幫我們理解PUBLICATION的行為:
class CacheThread(val lazyValue: BigInteger) : Thread() { override fun run() { super.run() Thread.sleep(250) println("${this::class.java.simpleName} $lazyValue") } } class NetworkThread(val lazyValue: BigInteger) : Thread() { override fun run() { super.run() Thread.sleep(300) println("${this::class.java.simpleName} $lazyValue") } }
我們模擬了兩個執行緒執行耗時操作,一個取快取,一個取網路資料,他們都需要一些時間來執行操作。
這是我們的測試程式碼:
fun main(args: Array<String>) { val lazyValue by lazy(LazyThreadSafetyMode.PUBLICATION) { println("computation") BigInteger.valueOf(2).modPow( BigInteger.valueOf(7), BigInteger.valueOf(20) ) } CacheThread(lazyValue).start() NetworkThread(lazyValue).start() }
結果如下:
computation CacheThread 8 NetworkThread 8
我們可以發現,值只被計算了一次,當CacheThread引用了lazyValue之後,結果就被快取了下來,後面執行緒再訪問都是訪問的這個快取的值,不會再重新計算了。
它是怎麼做的呢:
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable { @Volatile private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE // this final field is required to enable safe publication of constructed instance private val final: Any = UNINITIALIZED_VALUE override val value: T get() { val value = _value if (value !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return value as T } val initializerValue = initializer // if we see null in initializer here, it means that the value is already set by another thread if (initializerValue != null) { val newValue = initializerValue() if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) { initializer = null return newValue } } @Suppress("UNCHECKED_CAST") return _value as T } 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) companion object { private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater( SafePublicationLazyImpl::class.java, Any::class.java, "_value" ) } }
我們看到這裡在呼叫了initializer之後就把就把它置為空了,確保它只執行一次,然後使用java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater來更新_value的值,這就保證了第一次計算出的結果會被成功儲存下來。
好了,到這裡我們算是弄清楚三種模式的行為了,知道它們用什麼策略來獲取一個結果,接下來就要找哪裡用到了這三個類的value欄位,把這個value返回給我們的委託物件的。
鑑於我之前在文章裡都有告訴大家編譯器會悄咪咪幫我們做事,減少我們的工作量,我猜這次也不例外,還是寫個最簡單的例子,從位元組碼入手:
fun main() { val lazyValue by lazy { 1 } print(lazyValue) }
主要看位元組碼:
// access flags 0x11 public final main()V L0 LINENUMBER 3 L0 GETSTATIC Main$main$lazyValue$2.INSTANCE : LMain$main$lazyValue$2; CHECKCAST kotlin/jvm/functions/Function0 INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy; GETSTATIC Main.$$delegatedProperties : [Lkotlin/reflect/KProperty; ICONST_0 AALOAD ASTORE 2 ASTORE 1 L1 LINENUMBER 4 L1 ALOAD 1 ASTORE 3 ACONST_NULL ASTORE 4 L2 ALOAD 3 INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf) L3 CHECKCAST java/lang/Number INVOKEVIRTUAL java/lang/Number.intValue ()I ISTORE 3 L4 LINENUMBER 4 L4 L5 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; ILOAD 3 INVOKEVIRTUAL java/io/PrintStream.print (I)V L6 L7 LINENUMBER 5 L7 RETURN L8 LOCALVARIABLE lazyValue Lkotlin/Lazy; L1 L8 1 LOCALVARIABLE this LMain; L0 L8 0 MAXSTACK = 3 MAXLOCALS = 5
我們可以看到我們在列印時呼叫了Lazy的getValue方法。
我們就來找一找它,很巧,在這個列舉類上面,相同的檔案下(Lazy.kt),包含了一個叫getValue的擴充套件方法:
/** * An extension to delegate a read-only property of type [T] to an instance of [Lazy]. * * This extension allows to use instances of Lazy for property delegation: * `val property: String by lazy { initializer }` */ @kotlin.internal.InlineOnly public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
雖然這個方法看起來很奇怪,不過這下就很明瞭了,我們使用屬性的時候是呼叫了這個方法,它直接使用了Lazy介面的value值,也就是我們剛剛分析的三個類中產生的value值,注意,它被operator修飾了,這代表著我們不一定要通過方法名來呼叫它。
結合前面的原始碼分析,我們可以稍做總結,Lazy物件確實做到了惰性求值,在我們訪問屬性,間接呼叫了getValue方法的時候才根據有無快取的值來判斷是否要計算結果。
目前看來,這裡就是奧妙所在,而且從註釋來看跟by關鍵字配合起來實現的黑魔法。按照套路總得有個規範是實現特定的功能,我們目前瞭解的公共的東西怕是也只有Lazy介面了,那kotlin是靠Lazy介面來建立代理屬性的嗎?再繼續追究下去,我們就得先說說如何建立一個代理屬性了。
一般來說,對於一個用val宣告的屬性,需要一個包含get方法的代理,而對於用var宣告的,則get,set都需要有,根據文件我們要實現ReadWriteProperty或者ReadOnlyProperty介面,認真看的同學可能要問了,不對呀,我們剛剛看的Lazy系列都沒有實現這些介面呀,怎麼能夠實現代理功能的?別急,即將揭曉,我們往下看:
/** * Base interface that can be used for implementing property delegates of read-only properties. * * This is provided only for convenience; you don't have to extend this interface * as long as your property delegate has methods with the same signatures. * * @param R the type of object which owns the delegated property. * @param T the type of the property value. */ public interface ReadOnlyProperty<in R, out T> { /** * Returns the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @return the property value. */ public operator fun getValue(thisRef: R, property: KProperty<*>): T } /** * Base interface that can be used for implementing property delegates of read-write properties. * * This is provided only for convenience; you don't have to extend this interface * as long as your property delegate has methods with the same signatures. * * @param R the type of object which owns the delegated property. * @param T the type of the property value. */ public interface ReadWriteProperty<in R, T> { /** * Returns the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @return the property value. */ public operator fun getValue(thisRef: R, property: KProperty<*>): T /** * Sets the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @param value the value to set. */ public operator fun setValue(thisRef: R, property: KProperty<*>, value: T) }
很巧,都包含了一個跟前面我們找到的Lazy相似的getValue方法,但是稍微看一下注釋就發現其實並不是巧合,介面不是必須的,只要我們的類包含跟這些介面中的方法相同簽名的方法,就可以實現屬性代理的功能,那這樣說我們也就豁然開朗了,怪不得要給我們的Lazy介面增加一個簽名這麼奇怪的擴充套件方法,怪不得Lazy的子類都能用作屬性代理。
我是覺得實現介面可以避免我們方法簽名寫錯,畢竟這方法又長又奇怪,而且實現起來也很簡單:
class MyDelegate<T> : ReadWriteProperty<Any?, T?> { private var value: T? = null override fun getValue(thisRef: Any?, property: KProperty<*>): T? { value } override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { this.value = value } }
這裡我們直接返回了vaue,根據業務邏輯需求也可以在這裡放複雜的邏輯。
使用起來就更簡單了:
fun main(args: Array<String>) { val value by MyDelegate<String>() println(value) }
到這裡疑惑就都解開了,只要有一個符合特定條件的物件,這個物件的類不一定要實現特定的介面,只要包含了那些簽名特殊的getValue,或者getValue,setValue方法都有,哪怕只是把這些方法宣告成擴充套件方法也可以,那這個物件就能作為代理屬性在by關鍵字後面使用。
另外,即使是區域性變數也是可以使用代理屬性的,不過需要注意的是,如果我們的代理會被區域性變數使用,那第一個泛型引數要是可以為空的(Nullable),為什麼呢,我們來看一下反編譯的Java程式碼:
public final class MyDelegate implements ReadWriteProperty { private Object value;@Nullable public Object getValue(@Nullable Object thisRef, @NotNull KProperty property) { Intrinsics.checkParameterIsNotNull(property, "property"); Object var10000 = this.value; return Unit.INSTANCE; } public void setValue(@Nullable Object thisRef, @NotNull KProperty property, @Nullable Object value) { Intrinsics.checkParameterIsNotNull(property, "property"); this.value = value; } } public final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); MyDelegate var10000 = new MyDelegate(); KProperty var3 = $$delegatedProperties[0]; MyDelegate value = var10000; Object var4 = value.getValue((Object)null, var3); System.out.println(var4); }
我們發現對於本地變數value,getValue的第一個引數傳的是null,因為本地變數不屬於任何物件。
如果確定我們的代理只會被類的屬性使用,那麼我們就可以直接把第一個泛型引數傳為不可空(NonNull)。
還沒完,按照我之前討論類代理的套路,我是要扒一扒使用多個代理的開銷的,再來看一個例子,再新增一個使用相同代理的屬性,
class Main { val value by MyDelegate<String>() val value1 by MyDelegate<String>() } 這是反編譯的java程式碼: public final class Main { // $FF: synthetic field static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(Main.class), "value", "getValue()Ljava/lang/String;")), (KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(Main.class), "value1", "getValue1()Ljava/lang/String;"))}; @Nullable private final MyDelegate value$delegate = new MyDelegate(); @Nullable private final MyDelegate value1$delegate = new MyDelegate();@Nullable public final String getValue() { return (String)this.value$delegate.getValue(this, $$delegatedProperties[0]); } @Nullable public final String getValue1() { return (String)this.value1$delegate.getValue(this, $$delegatedProperties[1]); } }
我們可以看到,跟之前講類代理的時候一樣,每次使用代理都會單獨建立一個代理物件,在這兒顯然不是必須的,大家要有意識地減少開銷,我們可以按照老套路把它宣告成一個單例,至於如何宣告也跟之前類代理的解決辦法類似,這裡就不再贅述了。
此外我還發現一個有意思的東西,我們的代理是支援泛型的,這意味著它可以用於任意類,比如這樣:
class Main { val value by MyDelegate<Int>() val value1 by MyDelegate<Float>() } 反編譯成Java程式碼是這樣的: public final class Main { // $FF: synthetic field static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(Main.class), "value", "getValue()Ljava/lang/Integer;")), (KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(Main.class), "value1", "getValue1()Ljava/lang/Float;"))}; @Nullable private final MyDelegate value$delegate = new MyDelegate(); @Nullable private final MyDelegate value1$delegate = new MyDelegate(); @Nullable public final Integer getValue() { return (Integer)this.value$delegate.getValue(this, $$delegatedProperties[0]); } @Nullable public final Float getValue1() { return (Float)this.value1$delegate.getValue(this, $$delegatedProperties[1]); } }
做了一些型別轉換,這也是有開銷的,而我在之前分析lambda的時候翻到過一個檔案Ref.java,裡面單獨給原始型別建立了類,給其他類才提供了泛型版本:
public static final class ObjectRef<T> implements Serializable { public T element; @Override public String toString() { return String.valueOf(element); } } public static final class ByteRef implements Serializable { public byte element; @Override public String toString() { return String.valueOf(element); } } public static final class ShortRef implements Serializable { public short element; @Override public String toString() { return String.valueOf(element); } }
庫作者為了避免型別轉換帶來的開銷,特地加了這幾個看起來冗餘的類,我們這裡也是可以效仿一下的嘛:
class IntDelegate : ReadOnlyProperty<Any?, Int?> { override fun getValue(thisRef: Any?, property: KProperty<*>): Int? { TODO() } }
總結
好了,經過這麼一通硬核的分析,代理屬性還能難得了誰?還是那句話哈,不一定是Kotlin比Java慢,可能是我們寫的程式碼姿勢不對優化不到位,大家平時學習的時候可以翻一翻原始碼,多看看位元組碼,多看看反編譯的Java檔案,比較比較,就能知道編譯器為我們做了什麼,即能加深對這些語法糖的理解,也能學到一些編碼技巧。
免費獲取更多安卓開發架構的資料(包括Fultter、高階UI、效能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線網際網路公司關於android面試的題目彙總可以加入 【騰訊@安卓中高階進階】
