Kotlin by lazy解析及在findviewById场景中的使用

1、by lazy 简介

从名字上就可以看出来,by lazy 是一种懒加载模式,也就是说变量的值并不是在声明的时候赋值的,而是在真正用到这个变量的时候,才会加载它,也就是为它赋值。并且第一次赋值后会记录好这个值,以后的访问,都会直接返回这个值。我们看一个例子:

class Test {
    private val phone: String by lazy { "123" }
    private val name = "zhang san"

    fun hello() {
        println(name)
        println(phone)
    }
}

当我们在别处调用:

val test = Test()
test.hello()

当创建了 test 时,变量 name 的值已经是 zhang san 了。但是 phone 因为是懒加载模式,所以这时候, phone 的值并没有被赋值为 123,只有调用了 hello 方法中的 println(phone) 用到 phone 时,phone 才会先经懒加载模式被赋值为 123。然后再被 println 打印出来。

如果接下来我们再调用一次 hello() 方法,这时候 phone 不会再走一遍赋值了,因为他已经有值了。所以 by lazy 模式只会执行一次赋值操作,也就是首次用到变量的时候,变量一旦被赋值后,就不可以改变其值了。所以上面我们声明变量时,可以把 name 变量改成 var ,但是 phone 变量,只能使用 val ,而不可以用 var。如果我们使用 var 会被直接爆红提示:

2、by 关键字

by lazy的关键在于 by 关键字,也就是 kotlin 中的代理模式。

代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

代理模式

在 java 中,我们想实现一个代理模式,需要手写各个类和接口,而在 kotlin 中,通过关键字 by 封装了整个代理模式。我们可以把 by 理解为代理模式的语法糖。

下面是 kotlin 的一个代理模式示例:

// 创建接口
interface Base {   
    fun print()
}

// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

// 通过关键字 by 建立委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    val derived = Derived(b)
    derived.print() // 输出 10
}

在 Derived 声明中,by b 子句表示,将 b 保存在 Derived 的对象实例内部,而且编译器将会生成继承自 Base 接口的所有方法, 并将调用转发给 b。所以当我们调用derived.print()方法时,实际上是调用了代理者 BaseImpl 类的 print() 方法。这就是 kotlin 中代理模式的书写方式,比 java 要简洁。

我们看一下上面这段代码编译后的样子,从中就能看到 kotlin 编译器为我们做了哪些工作:

public interface Base {
   void print();
}

Base 接口依然是一个单纯的 Base 接口。

public final class BaseImpl implements Base {
   private final int x;

   public void print() {
      int var1 = this.x;
      boolean var2 = false;
      System.out.print(var1);
   }

   public final int getX() {
      return this.x;
   }

   public BaseImpl(int x) {
      this.x = x;
   }
}

BaseImpl 实现类也没什么好说的,就是对 Base 接口的具体实现,打印出字符串 x 即可。因为声明的 x 变量是 public val 型的,所以这里同时也为我们创建 x 的 get() 方法。这也是为什么我们能再 java 中直接调用起 baseImpl.getX() 方法的原因。

最后我们看一下代理模式的实现,Derived 类: class Derived(b: Base) : Base by b

public final class Derived implements Base {
   // $FF: synthetic field
   private final Base $$delegate_0;

   public Derived(@NotNull Base b) {
      Intrinsics.checkParameterIsNotNull(b, "b");
      super();
      this.$$delegate_0 = b;
   }

   public void print() {
      this.$$delegate_0.print();
   }
}

编译器为我们做了两部分工作:

1、创建一个内部私有变量 $$delegate_0

2、实现了 Derived 的构造方法,将传进来的 b 赋值给了自己的 $$delegate_0 变量。

3、实现了 Derived 类的 print() 方法,直接转发给了 $$delegate_0 对象,从而实现代理模式。

属性代理

这是类的代理模式,在 kotlin 中,还有属性的代理。by lazy 便是属性代理的一种实现方式。

属性代理指的是一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理。

属性委托语法格式:

val/var <属性名>: <类型> by <表达式>

    var/val:属性类型(可变/只读)
    属性名:属性名称
    类型:属性的数据类型
    表达式:委托代理类

by 关键字之后的表达式就是代理, 属性的 get() 方法(以及set() 方法)将被委托给这个对象的 getValue() 和 setValue() 方法。属性代理不必实现任何接口, 但必须提供 getValue() 函数(对于 var属性,还需要 setValue() 函数)。

到这里我们知道,属性代理的关键是 getValue() 方法和 setValue() 方法。我们按照语法格式试着为 Test 类新加一个 age 属性,并为 age 写一个属性代理类 AgeProxy :

class AgeProxy {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "返回被代理的年龄:${property.name}"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("设置被代理的年龄: $value")
    }
}

然后为Test 类新加属性:

class Test {
    private var age by AgeProxy()

    fun hello() {
        age = "12"
        println(age)
    }
}

打印的结果为:

设置被代理的年龄:12

返回被代理的年龄:age

会打印两行是因为 age = "12" 触发了 age 的 set() 方法,进而出发了属性代理类 AgeProxy 的 setValue() 方法。而 println(age) 需要获取 age 的值,触发了 age 的get() 方法,进而调用了 AgeProxy 的getValue() 方法。

我们可以看到 age 的声明是 var 而不用非得是 val 了,这是因为代理类实现了 setValue() 方法,于是支持了 被代理属性的赋值功能。如果我们把 AgeProxy 类的 setValue() 方法删掉,这时候 age 声明也是会报错:

和 by lazy 的报错一样,所以我们知道,懒加载模式中,代理类肯定没有提供 setValue() 方法。

接下来依然通过查看编译后的 java 代码,看看编译器是如何帮我们实现属性的代理模式的。

编译后的 Test 类如下:

public final class Test {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{
       (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Test.class), "age", "getAge()Ljava/lang/String;"))};
   private final AgeProxy age$delegate = new AgeProxy();

   private final String getAge() {
      return this.age$delegate.getValue(this, $$delegatedProperties[0]);
   }

   private final void setAge(String var1) {
      this.age$delegate.setValue(this, $$delegatedProperties[0], var1);
   }

   public final void hello() {
      this.setAge("12");
      String var1 = this.getAge();
      boolean var2 = false;
      System.out.println(var1);
      var1 = this.getAge();
      var2 = false;
      System.out.println(var1);
   }
}

我们会惊奇的发现,源码中声明的 age 属性没有了,取而代之的是一个 KProperty[] 数组 $$delegatedProperties 属性和一个 AgeProxy 类型的 age$delegate 属性。除此之外,编译器为 age 创建了 set 和 get 方法,但是很显然,这里将取值和赋值操作全部转发给了 age$delegate 对象。至于属性代理内部的实现原理,我们暂且略过,单从编译后的代码上,就可以看出,确实是将属性的操作 交给了一个代理对象。

从 hello 方法中也可以看到,他处所有对 age 的访问,也全部被替换成了其代理对象 age$delegate。age 消失了,它的代理人 age$delegate 全权代理。

3、解析 by lazy 实现原理

那 by lazy 到底是如何实现的呢,为什么不提供 setValue() 方法呢,为什么还会有模式的选择呢, 属性代理编译后的样子是什么呢?我们通过分析 by lazy 的原理一层层的解析。

by lazy 调用过程

首先,在Test 类中添加 by lazy 调用,看看编译前后的差异:

class Test {
    private val age by lazy(LazyThreadSafetyMode.NONE) { "13" }

    fun hello() {
        println(age)
        println(age)
    }
}

编译后:

public final class Test {
   private final Lazy age$delegate;

   private final String getAge() {
      Lazy var1 = this.age$delegate;
      Object var3 = null;
      boolean var4 = false;
      return (String)var1.getValue();
   }

   public final void hello() {
      String var1 = this.getAge();
      boolean var2 = false;
      System.out.println(var1);
      var1 = this.getAge();
      var2 = false;
      System.out.println(var1);
   }

   public Test() {
      this.age$delegate = LazyKt.lazy(LazyThreadSafetyMode.NONE, (Function0)null.INSTANCE);
   }
}

和上面的属性代理一样,源码中的 age 属性消失了, 取而代之的是一个 Lazy 类型的 age$delegate 属性,并且编译器设置为当 Test 类构造时便初始化 age$delegate 属性:

this.age$delegate = LazyKt.lazy(LazyThreadSafetyMode.NONE, (Function0)null.INSTANCE);

通过 LazyKt 类的 lazy() 方法,示例化了 age$delegate。

接着编译器为 age 创建了 getAge() 方法,因为是 val 类型,所以没有创建 setValue() 方法。getValue 方法里对 age 的访问,也是交给了 Lazy 类型的对象 age$delegate。

所以懒加载模式是妥妥的属性代理,为设置为懒加载的属性提供了一个 Lazy 类型的代理对象,而这个对象只提供 get 操作,不提供 set 操作。 并且,只会执行一次初始化的操作。

Lazy 接口

public interface Lazy<out T> {
    /**
     * Gets the lazily initialized value of the current Lazy instance.
     * Once the value was initialized it must not change during the rest of lifetime of this Lazy instance.
     */
    public val value: T

    /**
     * Returns `true` if a value for this Lazy instance has been already initialized, and `false` otherwise.
     * Once this function has returned `true` it stays `true` for the rest of lifetime of this Lazy instance.
     */
    public fun isInitialized(): Boolean
}

在 Lazy.kt 文件中,Lazy 的声明很简单,一个保存属性初始化值的 value 属性,一个标识是否初始化过的方法。

Lazy 接口的声明如此简单和普通,以至于我们感觉它根本无法提供属性代理的功能啊。Lazy 接口确实和属性代理一点边都不搭,他就是一个普通的接口,让它摇身一变具有属性代理功能的核心点在于 Lazy.kt 文件中的另一句:

/**
 * 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

就是这一句 operator fun 声明使 Lazy 对象具有了属性代理中必须的 getValue() 方法,因此所有的Lazy 子类均可以作为属性代理。并且这里指定其代理功能就是直接返回自己的 value 属性值。

也正是因为只有这一句,没有 operator fun <T> Lazy<T>.setValue,所以 Lazy 不支持 修改属性值。当然了,Lazy.value 本身也是声明为 val 的。

注释中也说的明明白白了:This extension allows to use instances of Lazy for property delegation

Lazy 对象

接下来我们看看 private val age by lazy(LazyThreadSafetyMode.NONE) { "13" } 是如何创建出 Lazy 对象的

LazyKt.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.NONE:不加线程锁,多线程操作是不安全的

LazyThreadSafetyMode.SYNCHRONIZED :加锁,线程安全

LazyThreadSafetyMode.PUBLICATION : 会多次调用初始化操作,,但是只有第一次的返回的值会被采用,其他的N次初始化逻辑操作,只会执行逻辑,不产生有效返回值。

lazy() 防发默认的是加了同步锁的,但是我们大部分时候不需要同步锁,应该显示的注明 LazyThreadSafetyMode.NONE 不需要同步。

我们以 LazyThreadSafetyMode.NONE 模式为例,看看 UnsafeLazyImpl 类的实现。

UnsafeLazyImpl :

internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer //传进来的 lambda 表达式,即初始化逻辑
    private var _value: Any? = UNINITIALIZED_VALUE //私有属性,保存初始化逻辑返回的值

    // 重写 value 属性的 get 方法
    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 方法,是和 Lazy.kt 中声明的 扩展方法 getValue() 相配合的。

4、在findview中的使用

findViewyById(R.id.tv)的痛点

findViewyById(R.id.tv), 这是我们最常见不过的代码了,也是最繁琐无聊的一部分,在 java 中,有 butterKnife 这样的插件帮助我们自动完成 findViewById 这一部分的工作。

如果我们自己写,对于所有的 view 都是下面这种会写到吐得样子:

private var nameTv: TextView? = null

fun initViews() {
    nameTv = findViewById(R.id.name_tv)
}

除此之外,每当我们用 nameTv 时,还要迫不得已的多加一个问号: nameTv?.text = "你好",而这种问号的判空实际是没什么意义的,只是因为我们无法在 View 声明时为它赋值。

当然,使用 lateinit 关键字可以免去问号,但是依然需要一处声明,一处赋值:

private lateinit var ageTv: TextView

fun initViews() {
    ageTv = findViewById(R.id.age_tv)
}

那可以用 butterknife 这样的插件吗,答案是可以,但是依然不是最好的。这就涉及到了 butterKnife 的一个缺点,那就是他会创建很多中间代码,增加了编译时间,也增加了类文件数量和包体积。

还有一种kotlin 提供的方式,就是一出来就俘获人心的直接使用id的方式,比如:

name_tv.text = "张三"

kotlin支持直接使用 id 的方式访问对应的 view 控件。它的原理依然是编译器对 findViewById 方法的封装,这一方式看起来美好,但是缺点多多啊,所以官方早早的宣布放弃这个了。

by lazy 式 findViewById

所以,最后,我们选了 by lazy 的方式作为我们声明 view 控件的代码规范:

private val nameTv: TextView by lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.name_tv) }
private val ageTv: TextView by lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.age_tv) }

......
nameTv.text = "张三"
ageTv.text = "12"

优点:

1、使用 val 修饰,view 控件没有需要修改的场景

2、声明和赋值均在一处。

3、调用时无需添加问号,直接以非空变量的方式调用

4、懒加载模式,用到时再进行初始化

当然,缺点也是有的:

1、每一个 view 声明,都会创建一个 lazy 对象。

2、没有可用的插件辅助,手写的话依然很繁琐。

缺点1是懒加载模式的原理所在,无法改变。针对缺点2,我们自己写了一个生成 view 的插件,实现了一键生成 by lazy 形式的 view 声明。极大地减少了此处的工作量。

如果你的项目喜欢并接受 by lazy 形式的 view 声明,而苦于同样得需要手写的话,那这个插件会帮到你的。

findViews 插件

安装:

file-settings- plugins - 右上角齿轮 - install plugin from disk- 选择 findViews.jar ------restart---OK

使用:

布局文件中,右键-generate-findviews,点击 OK, 声明便可生成。

回到 Activity 或者其他需要 View 声明的地方,直接 Ctrl -V 即可。

private val mTvTime: TextView by lazy(LazyThreadSafetyMode.NONE) { findViewById<TextView>(R.id.tv_time) }
private val mTvName: TextView by lazy(LazyThreadSafetyMode.NONE) { findViewById<TextView>(R.id.tv_name) }

下载:

https://download.csdn.net/download/xx23x/21456736

猜你喜欢

转载自blog.csdn.net/xx23x/article/details/119867758
今日推荐