Kotlin进阶之——Lazy对于activity、fragment成员变量的妙用

我们在activity或fragment写成员变量时,是不是经常苦恼怎么写都似乎不怎么理想,总感觉缺少一点什么呢?

初入kotlin

当我们刚学习kotlin是,基本上都处于模仿Java写成员变量的过程中:

private var testDialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    testDialog = Dialog(this)
    //…一些设置
}
fun ttt() {
//    一些操作
    testDialog?.show()
//    testDialog?.xxx
//    testDialog?.yyy
//    testDialog?.zzz
}

很明显,这个“?”着实有点多(“!!”也同理),所以当我们入门kotlin后又进化了一下代码:

fun ttt() {
    testDialog?.let { 
        it.show()
//        it.xxx
//        it.yyy
//        it.zzz
    }
}

但是当我们使用很多成员变量的时候又出现了新的头疼事:

private var testDialog: Dialog? = null
private var test2: D2? = null
private var test3: D3? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    testDialog = Dialog(this)
    //…一些设置
    test2 = D2()
    //…一些设置
    test3 = D3()
    //…一些设置
}
fun ttt() {
    testDialog?.let { d ->
        test2?.let { t2 ->
            test3.let { t3 ->
//                …
            }
        }
    }
}

此时我们进入了非空判断地狱……

当然这这是表象之一,有更大的潜在危害:随着更多的成员变量加入,我们根本无从知道哪些是真的需要非空判断哪些不需要非空判断,全都使用“?.let”将会出现更头疼的——理解地狱。

初级进阶

当然Kotlin也早就想到了这种情况,所以“lateinit”应运而生,于是我们又进化了一次代码:

private lateinit var testDialog: Dialog
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    testDialog = Dialog(this)
}
fun ttt() {
    testDialog.show()
//    …
}

这样似乎回到了干净又清爽的新时代。但正当自我沉浸时,突然有一次爆出了“kotlin.UninitializedPropertyAccessException: lateinit property testDialog has not been initialized”。

根据堆栈信息很快发现成员变量没初始化却先被调用了……

这时才发现使用“lateinit”的成员变量对于空安全形同虚设,已经没有任何鸟用了。更细心的你发现,就连set都可以多次调用,想怎么改就怎么改。当然你也可以提前判断是否init了,但语法可不是一般的啰嗦——我们似乎又回到了Java时代。(在此处特别建议在任何地方都慎用lateinit!慎用lateinit!慎用lateinit!最好不用。)

高阶功法

经过一番搜寻“lazy”终于给了答案。首先它是val类型,这样就不怕被无故重新赋值,阅读或修改时也不用再瞻前顾后了,并且它在调用时才初始化,在正确的代码调用下完全不怕activity相关生命周期问题。于是代码又进化了:

private val testDialog by lazyNone {//自己写的lazy无锁拓展
    Dialog(this).apply {
//        …一些设置
    }
}
fun ttt() {
    testDialog.show()
//    …
}

这似乎解决了所有问题,但……

当感觉完美解决问题的正嗨皮奔放时,测试突然发来个bug:几个tab页切换,很多有些操作或数据还是旧的……

这时突然想起来:在Fragment切换时,本身实例不一定会被销毁,而仅仅调用的onDestroyView而已。

此时想改却想起来lazy只能是val类型……

终极目标

点开lazy代码发现lazy其实是一个接口,仔细观察lazy实际上就是调用接口的value成员变量。这似乎有一个新的想法:在Fragment destroy view时将内部成员变量重置,当再次调用的时候完全可以自行重新初始化了。于是有个大胆的想法:

private object UNINITIALIZED_VALUE

/**
 * 跟着ui的生命周期走,当destroy时会销毁,当再次create时会创建新的
 */
class UILazyDelegate<out T>(
    private val ui: IUIContext,//这里相当于activity或fragment,可自行拆成activity和fragment
    private val initializer: () -> T
) : Lazy<T>, Serializable {
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            if (!isInitialized()) {
                _value = initializer.invoke()
                ui.doOnDestroyed {//相当于监听destroy并自动移除
                    _value = UNINITIALIZED_VALUE
                }
                if (!isInitialized()) {
                    throw IllegalStateException("不允许在super.onDestroy后调用")
                }
            }
            return _value as T
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String =
        if (isInitialized()) value.toString() else "${initializer}:当前lazy尚未初始化"
}

这真的是终极解决方案?

当然不是,因为有了新的崩溃:“Method addObserver must be called on the main thread”。

我们的activity、fragment基本都在主线程操作,所以初始化时肯定都是主线程了。但lazy是延时加载,哪个线程第一次调用就会在哪个线程初始化,所以如果一个子线程先调用了,那崩溃就不可避免了。

当然,很直接的解决方案就是:调之前回到主线程。这也不乏是个相对合适的解决方案,但因为我们写的是工具,对于外部调用者来说整块代码都是隐藏的,使用者不仅仅是你一个人,想让所有人以后都不再出现这种情况,对于这种解决方案就有点草率了(博主认为,能用代码解决的就不要再多废口舌了——人总有出错但代码却不曾失误)。

终极无修改版

反过来想:让lazy主动回到主线程,这样调用者就无需考虑自己是否处于哪个线程了。

那么新问题来了:子线程必须要同步结果(这让我想起了“notify”、“wait”、“sleep”、“try”,想想代码都痛苦)

在kotlin里有没有一种功能可以无缝切换线程呢?“协程”——一个拯救万千码农的神器。是否只顾着看“launch”怎么用的了,仍还没注意到有一个叫“runBlocking”的方法?

至此,带线程切换的最终版应运而生:

private object UNINITIALIZED_VALUE

/**
 * 跟着ui的生命周期走,当destroy时会销毁,当再次create时会创建新的
 */
class UILazyDelegate<out T>(
    private val ui: IUIContext,
    private val initializer: () -> T
) : Lazy<T>, Serializable {
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            if (!isInitialized()) {
                if (isMainThread()) createForMain() else runBlocking(Dispatchers.Main) { createForMain() }
                if (!isInitialized()) {
                    throw IllegalStateException("不允许在super.onDestroy后调用")
                }
            }
            return _value as T
        }

    private fun createForMain() {
        _value = initializer.invoke()
        ui.doOnDestroyed {
            _value = UNINITIALIZED_VALUE
        }
    }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String =
        if (isInitialized()) value.toString() else "${initializer}:当前lazy尚未初始化"
}

当然也少不了加个拓展以方便使用:

/**
 * 根据生命周期自动管理初始化
 * 注意:请在super.onCreate后和super.onDestroy前使用
 * 注意:可以在子线程调用,调用期间会强制阻塞子线程
 */
@Suppress("NOTHING_TO_INLINE")
inline fun <T> IUIContext.lazyWithUI(noinline initializer: () -> T) = UILazyDelegate(this, initializer)
//IUIContext:activity或fragment或其他,可自行拆成两个方法

使用示例:

private val testDialog by lazyWithUI {
        Dialog(this)
    }
后话:

结后思考:

lazy比直接赋值多创建了几个对象?lazy对性能影响多大?你真的理解什么会影响性能吗?

博主心得:

1.不建议使用lateinit

2.能使用val就不要再让var bb了

3.能使用非空就不要一直"?."了

4.能用代码解决的就不要再多废口舌了——人总有出错但代码却不曾失误

5.new对象不值钱,能省则省,不省也没关系,主要看用的地方

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值