模仿 Kotlin by lazy 实现绑定数据到 ViewModel

877 阅读7分钟

模仿 Kotlin by lazy 实现绑定数据到 ViewModel

ViewModel 相信每个人都很熟悉,我认为他的最大的一个作用是:当由于配置改变的时候重启 Activity 时可以保存之前被销毁的 Activity 的变量,常见的场景是屏幕旋转,系统语言改变,系统的深色主题切换等等。这里举一个例子:比如我有一个视频播放页面,我的 Player 实例的生命周期就不能和 Activity 实例生命周期绑定在一起,当屏幕旋转时当前的 Activity 实例就会被销毁,然后立即创建一个新的 Activity,如果 Player 实例和 Activity 实例绑定在一起,那么新的 Activity 就不能获取之前 Player 的播放状态,这时借助 ViewModel 来保存 Player 实例就是一个很好的选择。关于 ViewModel 的工作原理可以参考我之前的文章:[Framework] ViewModel 如何做到 Activity 重启不丢失数据

我发现很多人写代码时都会为每个 Activity / Fragment 创建一个 ViewModel 对象,尽管这些页面的业务逻辑也非常简单,ViewModel 中也只简单保存一两个成员变量,然后还有一些非常简单的方法来处理 Activity / Fragment 的请求。时间长了我就会开始思考这样的 ViewModel 我们真的需要每次都创建吗?明明一个非常简单页面逻辑我们为什么要一定把这这些逻辑分散写在好几个源码文件中,看代码时跳来跳去的。假如我们把一些简单的逻辑就写在一个文件中是不是阅读代码时就更加的连贯,我认为代码不超过 500 行时,逻辑写在一个文件里阅读起来是最舒适的。(当然某些通用逻辑明显该独立出去,这里就要自己做判断了)我觉得如果是编程新手严格按照前人的经验来要求自己,我觉得没有问题;工作很多年后如果还是保持教条主义我认为就不太妥了,更多需要加入自己的思考,毕竟技术是一直在发展的。

前面的废话有点多了 ^_^,进入正题,这次我想要做的就是简单的页面不再需要单独创建 ViewModel 类,需要保存到 ViewModel 的对象就直接放到 Activity / Framgent 的成员变量中,简单的方法也都直接放到 Activity / Fragment 中。重点是如何将需要保存到 ViewModel 的变量直接放到 Activity / Fragment 成员变量中,这个时候我们就会想到 Kotlin 的代理 / 委托的实现,像我这次就是模仿 by lazy 的实现,关于 Kotlin 代理 / 委托的原理可以参考我之前的文章:基于字节码指令分析 Kotlin 代理

Kotlin by lazy 的代码实现

比如我下面就通过 by lazy 创建了一个延迟初始化的对象:

// ...
private val greetingStr: String by lazy {
    "Hello, World"
}
// ...

上面的变量 greetingStr 变量在没有别的地方使用时是不会完成初始化的,当有地方使用时才会去尝试初始化,初始化时会去调用 lazy 方法传递过去的 lambda 来完成,如果已经完成初始化后,再去使用该变量时就不需要再初始化。

看看 lazy() 方法的源码:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

方法返回的是 Lazy 接口,然后他的实现类是 SynchronizedLazyImpl

看看 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
}

接口比较简单,value 就是我们保存的对象的实例,isInitialized() 方法就是表示 value 是否已经完成初始化。

接着看看 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 {
                    // 调用我们穿过来初始化的 labmda
                    val typedValue = initializer!!()
                    // 赋值给 _value
                    _value = typedValue
                    // 将初始化 labmda 置空
                    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)
}

initializer 就是我们传递过来初始化对象的 lambda

_value 用来保存初始化后的对象,默认值是 UNINITIALIZED_VALUE,也就是表示当前没有初始化,注意 _value 是使用 @Volatile 注解标记的,他的修改是对其他线程可见的,没错,SynchronizedLazyImpl 是线程安全的。

lock 保证线程安全的锁对象,如果没有设置,直接使用 SynchronizedLazyImpl 实例作为锁对象。

value 对象,就是 Lazy 接口中定义的,它的 get() 方法中描述了 SynchronizedLazyImpl 的核心实现。实现非常简单,我在代码中添加了注释,它的实现和 double check 的单利模式实现有异曲同工之妙。

  1. 首先第一次检查是否初始化,如果已经初始化就直接返回,注意第一次检查是没有加锁的。
  2. 加锁第二次检查是否初始化,如果还是没有初始化就调用我们穿过来的 lambda 来完成初始化,最后返回。

为什么这里要检查两次我就不多说了,网上已经都讲烂了。

模仿 Kotlin by lazy 实现绑定数据到 ViewModel

像我自己写的模仿绑定数据到 ViewModel 的使用代码如下:

private val greetingStr by lazyViewModelField("greetingStr") {
    "Hello, World."
}

Kotlinby lazy 用起来差不多,不过需要多一个 key 参数,这是用来告诉 ViewModel 保存的数据的 key

我们来简单看看实现:

/**
 * Get field value from ViewModel.
 */
fun <T : Any> lazyViewModelField(key: String, initializer: () -> T): Lazy<T> {
    return ViewModelFieldLazy(key, initializer)
}

我的 Lazy 实现类是 ViewModelFieldLazy

private inner class ViewModelFieldLazy<T : Any>(
    private val key: String,
    private val initializer: () -> T
) : Lazy<T> {

    @Suppress("UNCHECKED_CAST")
    override val value: T
        get() {
            if (application == null) {
                error("Can't init view model lazy field, because activity is not attached.")
            }
            val vm = this@BaseActivity.baseActivityViewModel
            if (vm.isCleared()) {
                error("ViewModel was cleared.")
            }
            val firstCheckValue = vm.getField(key)
            var result: T? = null
            // First check.
            if (firstCheckValue != null) {
                try {
                    result = firstCheckValue as T
                } catch (e: Throwable) {
                    error("Wrong field type, maybe you use same key in different fields, key=$key, error=${e.message}")
                }
            } else {
                synchronized(this) {
                    val secondCheckValue = vm.getField(key)
                    // Second check.
                    result = if (secondCheckValue != null) {
                        try {
                            secondCheckValue as T
                        } catch (e: Throwable) {
                            error("Wrong field type, maybe you use same key in different fields, key=$key, error=${e.message}")
                        }
                    } else {
                        val newValue = initializer()
                        if (vm.saveField(key, newValue)) {
                            newValue
                        } else {
                            error("ViewModel was cleared.")
                        }
                    }
                }
            }
            return result!!
        }

    override fun isInitialized(): Boolean {
        return application != null && this@BaseActivity.baseActivityViewModel.containField(key)
    }

}

我自己的实现基本是和 Kotlinby lazy 是大同小异的,我会检查 ViewModelActivity 的生命周期状态,Kotlin 是将缓存的 value 放在成员变量中,并且用 @Volatile 注解修饰,以保证修改对其他线程可见。而我是保存在 ViewModel 中,用 ConcurrentHashMap 来保存,ConcurrentHashMap 也能够保证修改对其他线程可见。

ViewModel 的实现如下,也是非常简单的代码:

internal class BaseActivityViewModel : ViewModel() {


    private val savedFields: ConcurrentHashMap<String, Any> by lazy {
        ConcurrentHashMap()
    }

    private var clearObserver: ViewModelClearObserver? = null

    @Volatile private var isCleared: Boolean = false

    @MainThread
    fun setViewModelClearObserver(o: ViewModelClearObserver?) {
        clearObserver = o
    }

    fun containField(key: String): Boolean = savedFields.containsKey(key)

    fun getField(key: String): Any? = savedFields[key]

    fun saveField(key: String, field: Any): Boolean {
        if (isCleared()) {
            return false
        }
        savedFields[key] = field
        tUiUtilsLog.d(TAG, "Save new field: $key")
        return true
    }

    fun isCleared(): Boolean = isCleared

    override fun onCleared() {
        super.onCleared()
        clearObserver?.onViewModelCleared()
        savedFields.clear()
        isCleared = true
    }

    companion object {

        private const val TAG = "BaseActivityViewModel"
        interface ViewModelClearObserver {
            fun onViewModelCleared()
        }
    }

}

上面的代码也非常的简单,就不多说了。

最后

完整的源码在这里,如果我的代码对你有所帮助,希望能够得到你的 Star。