百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

深入分析Kotlin的属性代理机制

bigegpt 2024-08-28 12:15 7 浏览

前言

熟悉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)
}

到这里疑惑就都解开了,只要有一个符合特定条件的对象,这个对象的类不一定要实现特定的接口,只要包含了那些签名特殊的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文件,比较比较,就能知道编译器为我们做了什么,即能加深对这些语法糖的理解,也能学到一些编码技巧。

文末送福利啦!!

同时我经过多年的收藏目前也算收集到了一套完整的学习资料以及高清详细的Android架构进阶学习导图及笔记免费分享给大家,希望对想成为架构师的朋友有一定的参考和帮助。

下面是部分资料截图,诚意满满:特别适合有开发经验的Android程序员们学习。



资料领取方式:关注我并私信回复“资料”

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...