Kotlin之边学边分析


什么是委托模式?以及委托模式的原理和使用场景

koltin中声明的成员属性或者超类不在当前类中去实现,而是交给其他类去完成,叫做委托,使用by关键字来实现。
其使用场景适合那些需要复杂计算并且可以重复使用的场景

lazy懒惰性委托

lazy委托模式不会在声明时就计算好值,而是在第一次使用时才会计算值;并且以后也是用这个计算好的值;lazy(() -> t)参数是一个高阶函数,其内部原理根据传入参数分为三种类型:

LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)

SYNCHRONIZED:
默认参数类型,lazy计算时会加锁进入线程同步模式,性能可能会下降
PUBLICATION:
多线程安全模式,内部采用自旋锁CAS方式比较判断写入值,性能比同步好点
NONE:
不会加任何索,多线程不安全,适用于单线程操作

自定义委托模式类

属性委托关键在于提供get方法,kotlin提供超类ReadOnlyProperty 或 ReadWriteProperty两个接口,根据使用场景继承他们,重写get和set(var型变量)方法即可;在Android中我们可以的SharedPreference可以使用属性委托来很好的为我们工作

类委托(代理模式)

上面讲的都是属性委托,而类的委托则适用于另一个场景,必须委托的基类是接口;其实质就是一种代理模式,类委托基本结构分三类:基础接口类Base、已实现Base的类Base1,以及我们需要代理的类Base2,如下:

interface Base{
	fun print()
}
class Base1 : Base{
	fun print(){
		System.out.println("----")
	}
}

class Base2(b : Base) : Base by b{
}

以上Base2传入参数b,以及后面委托b,实质就是Base2静态代理b对象,无须本身去实现,kotlin帮你去生成中间代码,反编译情况确实如此:

android 开发 kotlin 耗时 任务 刷新 进度 kotlin synchronized_android

更多相关知识请点击


Kotlin语法糖apply

如题,apply关键字作为语法糖,有着提高开发效率,优化代码结构的好处!但是我认为也有不好的地方,请看下面的分析

基本使用

class AFragment : Fragment() {
	private val data : Bundle = Bundle()
	private fun test(){
        LogHelper.de_i("--------")
    }
	companion object {
		AFragment().apply {
		      内部操作相当于在AFragment环境下操作,
		      可以执行AFragment的函数和成员;
		      私有成员也可以调用;
		      test()
		      data.putString("a", "a")
		}
	}
}

使用场景: 可以用到一些实例初始化,需要很多参数初始化那种,不需要写以前那种建造者模式了,方便得很

原理分析

看看java层源码对Apply这个语法糖是如何定义:

inline内联函数,传入block高阶函数,而且是在T模板下的高阶函数,所以可以
执行T模板类下的函数
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    执行高阶函数
    block()
    return this
}

看到上面大致明白了,apply传入的T类型下的高阶函数,所以可以执行T的函数;但是T类型的私有函数仅仅凭上面的操作是办不到的,因为私有成员只能自己类里面调用,外部是无法调用的,如何破?

kotlin采用编译时,根据部分注解,为这些私有成员生成一些公有静态方法,其参数传入T类,在静态方法内部调用这些私有成员,反编译上面的基本使用的样例代码,看看生成了哪些
a. 先看看AFragment.smali类,省去部分代码保留关键代码

# static fields
这是我能定义的伴生对象Companion Object,自动生成一个静态对象,在类加载器加载时创建
.field public static final Companion:Lcom/jz/appframe/AFragment$Companion;
创建的私有成员data
.field private final data:Landroid/os/Bundle;

# direct methods
cinit为类加载器加载时执行的构造方法,而init是创建对象时执行
.method static constructor <clinit>()V
    .locals 2
	创建AFragment$Companion对象,保存到v0寄存器
    new-instance v0, Lcom/jz/appframe/AFragment$Companion;

    const/4 v1, 0x0
	初始化AFragment$Companion对象
    invoke-direct {v0, v1}, Lcom/jz/appframe/AFragment$Companion;-><init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
	把v0寄存器内容赋值给成员Companion:Lcom
    sput-object v0, Lcom/jz/appframe/AFragment;->Companion:Lcom/jz/appframe/AFragment$Companion;
    return-void
.end method

私有函数
.method private final test()V
    .locals 1
    .line 36
    const-string v0, "--------"
    invoke-static {v0}, Lcom/jz/appframe/util/LogHelper;->de_i(Ljava/lang/String;)V

    .line 37
    return-void
.end method

共有静态方法,对应上面的私有test方法
.method public static final synthetic access$test(Lcom/jz/appframe/AFragment;)V
    .locals 0
    .param p0, "$this"    # Lcom/jz/appframe/AFragment;

    .line 21
    调用test方法,传入p0是一个AFragment对象
    invoke-direct {p0}, Lcom/jz/appframe/AFragment;->test()V

    return-void
.end method

共有静态方法,对应上面的私有成员
.method public static final synthetic access$getData$p(Lcom/jz/appframe/AFragment;)Landroid/os/Bundle;
    .locals 1
    .param p0, "$this"    # Lcom/jz/appframe/AFragment;
    .line 21
    同上test理解
    iget-object v0, p0, Lcom/jz/appframe/AFragment;->data:Landroid/os/Bundle;
    return-object v0
.end method

这是针对伴生对象Companion对象方法生成静态方法,所有可以直接使用外部类调用newInstance方法,
实质调用的是这个方法,然后内部在使用半生对象实例调用newInsatnce
.method public static final newInstance(Ljava/lang/String;Ljava/lang/String;)Lcom/jz/appframe/AFragment;
    .locals 1
    .annotation runtime Lkotlin/jvm/JvmStatic;
    .end annotation
	get拿到伴生对象Companion
    sget-object v0, Lcom/jz/appframe/AFragment;->Companion:Lcom/jz/appframe/AFragment$Companion;
	调用Companion的newInstance方法
    invoke-virtual {v0, p0, p1}, Lcom/jz/appframe/AFragment$Companion;->newInstance(Ljava/lang/String;Ljava/lang/String;)Lcom/jz/appframe/AFragment;
    move-result-object v0
    return-object v0
.end method

上面AFragment除了我们定义的私有成员之外,还生成了许多辅助的公有静态方法,目的是为了可以从外部调用AFragment的私有成员;看看半生对象做了啥操作,主要看newInstance:

.method public final newInstance(Ljava/lang/String;Ljava/lang/String;)Lcom/jz/appframe/AFragment;
    .locals 8
    p1和p2是参数
    .param p1, "param1"    # Ljava/lang/String;
    .param p2, "param2"    # Ljava/lang/String;
    .annotation runtime Lkotlin/jvm/JvmStatic;
    .end annotation

    const-string v0, "param1"
	检查参数是否为空
    invoke-static {p1, v0}, Lkotlin/jvm/internal/Intrinsics;->checkParameterIsNotNull(Ljava/lang/Object;Ljava/lang/String;)V

    const-string v1, "param2"

    invoke-static {p2, v1}, Lkotlin/jvm/internal/Intrinsics;->checkParameterIsNotNull(Ljava/lang/Object;Ljava/lang/String;)V

    .line 57
    创建AFragment对象
    new-instance v2, Lcom/jz/appframe/AFragment;
	执行构造方法
    invoke-direct {v2}, Lcom/jz/appframe/AFragment;-><init>()V
	把AFragment对象同时给了v3和v2,为什么要这么做,还不是apply语法糖;
	相当于两个指针同时存储一个对象,一个指针用于apply内部的高阶函数操作;
	另一个保存这个初始指针使用,下面的代码也是这么做的
    move-object v3, v2
	这个怪?竟然还有这种用法,第一次遇到,不难理解,这就是对v3这个本地寄存器
	解释,v3类型是AFragment,名字是$this$apply;v3作为apply内部指针使用
	v2作为返回对象使用
    .local v3, "$this$apply":Lcom/jz/appframe/AFragment;
    const/4 v4, 0x0 

    .line 58
    .local v4, "$i$a$-apply-AFragment$Companion$newInstance$1":I
    创建一个Bundle
    new-instance v5, Landroid/os/Bundle;
    invoke-direct {v5}, Landroid/os/Bundle;-><init>()V
    move-object v6, v5
    .local v6, "$this$apply":Landroid/os/Bundle;
    const/4 v7, 0x0
    .line 59
    .local v7, "$i$a$-apply-AFragment$Companion$newInstance$1$1":I
    invoke-virtual {v6, v0, p1}, Landroid/os/Bundle;->putString(Ljava/lang/String;Ljava/lang/String;)V
    .line 60
    invoke-virtual {v6, v1, p2}, Landroid/os/Bundle;->putString(Ljava/lang/String;Ljava/lang/String;)V
    .line 61
     v6寄存器结束使用了
    .end local v6    # "$this$apply":Landroid/os/Bundle;
    .end local v7    # "$i$a$-apply-AFragment$Companion$newInstance$1$1":I
    nop

    .line 58
   	v5设置到AFragment的bundle上
    invoke-virtual {v3, v5}, Lcom/jz/appframe/AFragment;->setArguments(Landroid/os/Bundle;)V

    .line 62
    调用了AFragment的共有静态方法去设置私有成员,access$test是AFragment中自动生成的
    invoke-static {v3}, Lcom/jz/appframe/AFragment;->access$test(Lcom/jz/appframe/AFragment;)V

    .line 63
    data私有成员也是一样的,调用公有静态方法去设置
    invoke-static {v3}, Lcom/jz/appframe/AFragment;->access$getData$p(Lcom/jz/appframe/AFragment;)Landroid/os/Bundle;

    move-result-object v0

    const-string v1, "a"

    invoke-virtual {v0, v1, v1}, Landroid/os/Bundle;->putString(Ljava/lang/String;Ljava/lang/String;)V
    .line 64
    寄存器结束
    .end local v3    # "$this$apply":Lcom/jz/appframe/AFragment;
    .end local v4    # "$i$a$-apply-AFragment$Companion$newInstance$1":I
    nop
    .line 57
    nop
    .line 64
    return-object v2
.end method

总结

apply确实可以提高手动编写代码的开发效率,为了完成私有成员调用,编译时生成了许多公有静态方法来调用!同时apply在完成高阶函数执行操作时,每次都多创建一个寄存器,如上move-object v6, v5;这样看来apply为了完成高效的代码操作也付出了很多东西;一个静态代码区新增了方法,二是运行时,寄存器会比平时多一倍,所以在内存上还是有开销存在的,并且调用逻辑是多了几个步骤的,AFragment.newInstance创建AFragment变量,调用逻辑依次是从AFragment.newInstance -> AFragment$Companion.newInstance ->AFragment.access$test -> access$getData$p,完成了AFragment的创建和初始化了;所以使用apply模式使用要注意了


Kotlin语法糖let

与apply不同,let则是将调用者当做参数传递进入高阶函数,并且将高阶函数最后的返回值返回,当然,如果高阶函数没有返回值也没关系!看看他的定义就知道了:

内敛函数
block高阶函数,T是入参 R是回参
public inline fun <T, R> T.let(block: (T) -> R): R {
	contract是契约,告知编译器在合适的地方调用,也就是外层函数执行时去调用
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    直接返回
    return block(this)
}

逆向let为smali

对这段kotlin逆向的结果查看

源码 arguments为bundle
arguments?.let {
            param1 = it.getString(ARG_PARAM1)
}

.line 29
获取arguments
invoke-virtual {p0}, Lcom/jz/appframe/AFragment;->getArguments()Landroid/os/Bundle;
move-result-object v0
检查arguments等于0不
if-eqz v0, :cond_0
解释v0的含义,名字为it,类型为bundle
.local v0, "it":Landroid/os/Bundle;
const/4 v1, 0x0 

.line 30
这个v1寄存器不知道是干啥用的,只标记了一下,根本没用上
.local v1, "$i$a$-let-AFragment$onCreate$1":I
const-string v2, "param1"

invoke-virtual {v0, v2}, Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String;

move-result-object v2

.line 32
.end local v0    # "it":Landroid/os/Bundle;
.end local v1    # "$i$a$-let-AFragment$onCreate$1":I

如上,let也没有什么神奇的,常规操作,判空arguments后,直接用他的寄存器进行操作;可以适用于一些判空后要对调用者进行配置的场景,不过里面会多一个整型类型的寄存器,也没用到,不知道为何,有知道的请留言交流交流,上面的apply也是


runBlocking

阻塞函数,运行在runBlocking中的代码时,会阻塞本线程,直到函数中所有代码的协程执行完才会取消阻塞

runBlocking<Unit> {
        launch {
            ....
        }
    }

launch是协程的api高阶函数,默认的协程在上一个线程即负线程中

Flow流

FLow流不像普通函数返回一次结果,而是会序列性的逐个返回多次结果,并且支持异步操作,可以配合协程使用,在数据库访问、网络操作返回等异步操作中返回;如下简单的使用:

private fun foo() : Flow<Int> = flow{
    for (i in 0..2){
        这里的delay是协程的延时函数,记住不要调用Thread的延时
        delay(100)
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    launch {
        for (i in 7..9){
            LogUtil.i("got the value $i")
        }
    }
    foo().collect { LogUtil.i("got the value $it") }
}

会依次输出:
10-27 11:44:20.373 7762 7762 I j_tag : got the value 7
10-27 11:44:20.373 7762 7762 I j_tag : got the value 8
10-27 11:44:20.373 7762 7762 I j_tag : got the value 9
10-27 11:44:20.474 7762 7762 I j_tag : got the value 0
10-27 11:44:20.575 7762 7762 I j_tag : got the value 1
10-27 11:44:20.675 7762 7762 I j_tag : got the value 2

这里的collect可以理解为生产者-消费者模型中的消费者,Flow则是数据的生产者,而且生产者函数调用不会立即执行,只有当终结函数如collect调用后,生产者函数才会执行,除此之外还有终结函数如:toList、first和single函数等,如下原文:

android 开发 kotlin 耗时 任务 刷新 进度 kotlin synchronized_android_02

Flow的取消

可能会想到一个资源泄漏问题,类似于页面与网络操作那种,页面返回后,网络回调数据还在操作中,这样会引起资源泄漏导致页面无法被释放问题?

答案是否定的,不会引起泄漏,首先Flow的终结函数都是suspend类型函数,必须运行在协程环境中,而协程的生命周期和页面绑定好,页面一经返回,协程任务也终将会cancel取消,进而Flow也会被取消

除非在协程外面就无法取消了,但是好像不存在!!