前面我們一起探究了Kotlin對類代理的支援,深扒其實現及限制,見類代理就是這麼簡單。這一趟,我們來深入討論下Kotlin的代理屬性。
我們已經知道類代理是一種基於父類或者介面的實現,而在代理屬性這邊沒有這種限制,而且這些代理物件的公共方法的引數中還包含了委託物件,這意味著在代理物件中也可以呼叫委託物件的公共方法。Kotlin的標準庫中就包含了許多使用代理屬性的實現,比如lazy
。
我們先來學習下寫標準庫的大佬怎麼玩的,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)
}
複製程式碼
到這裡疑惑就都解開了,只要在by
關鍵字後面帶有一個代理物件,這個代理不一定要實現特定的介面,只要包含了那些簽名特殊的get,或者get,set方法都有,那它就能作為一個代理屬性來使用。
另外,即使是區域性變數也是可以使用代理屬性的,不過需要注意的是,如果我們的代理會被區域性變數使用,那第一個泛型引數要是可以為空的(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檔案,比較比較,就能知道編譯器為我們做了什麼,即能加深對這些語法糖的理解,也能學到一些編碼技巧。