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帮你去生成中间代码,反编译情况确实如此:
更多相关知识请点击
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函数等,如下原文:
Flow的取消
可能会想到一个资源泄漏问题,类似于页面与网络操作那种,页面返回后,网络回调数据还在操作中,这样会引起资源泄漏导致页面无法被释放问题?
答案是否定的,不会引起泄漏,首先Flow的终结函数都是suspend类型函数,必须运行在协程环境中,而协程的生命周期和页面绑定好,页面一经返回,协程任务也终将会cancel取消,进而Flow也会被取消
除非在协程外面就无法取消了,但是好像不存在!!