深入理解屬性代理

小小小小小粽子-發表於2019-04-07

前面我們一起探究了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,這是一個列舉類,支援三種模式:

  1. SYNCHRONIZED 使用鎖來確保只有一個執行緒來求值。
  2. PUBLICATION 允許多個執行緒來初始化值,但是隻有第一個返回的值有效。
  3. 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檔案,比較比較,就能知道編譯器為我們做了什麼,即能加深對這些語法糖的理解,也能學到一些編碼技巧。

相關文章