Hi, 你好,很高兴见到你

技术无止境,咱们1024快乐。

持续创造,加快成长!这是我参加「日新方案 10 月更文挑战」的第2天,点击检查活动概况

引言

Kotlin 是一个十分 yes 的言语,从 null安全 ,支持 办法扩展特点扩展,到 内联办法内联类 等,运用Kotlin变得越来越简略舒畅。但编程从来不是一件简略的工作,所有简洁都是建立在杂乱的底层完成上。那些看似简略的kt代码,内部往往隐藏着不容忽视的内存开支。

介于此,本篇将根据个人开发阅历,聊一聊 Kotlin 中那些隐藏的内存圈套,也希望每一个同学都能在 功能高雅 之间找到适宜的平衡。

本篇定位简略,主要经过示例+相应字节码剖析的办法,对日常开发十分有协助。

导航

学完本篇,你将了解到以下内容:

  1. 密封类结构函数传值的运用细节;
  2. 内联函数,你应该留意的当地;
  3. 伴生目标隐藏的功能问题;
  4. lazy, 没你想的那么简略;
  5. apply!= 构建者办法;
  6. 关于 arrayOf() 的运用细节。

好了,让咱们开端吧!

密封类的小细节

密封类用来表示受限的类承继结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值调集也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类能够有可包含状况的多个实例。摘自Kotlin中文文档

关于它用法,咱们详细不再做赘述。

密封类尽管十分实用,常常能成为咱们多type的绝佳调配,但其间却藏着一些运用的小细节,比方 结构函数传值所导致的损耗问题。

过错示例

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心
如题, 咱们有一个共用的特点 sum ,为了便于复用,咱们将其抽离到 Fruit 类结构函数中,让子类便于初始化时传入,而不用重复显式声明。

上述代码看着好像没什么问题?依照传统的操作习惯,咱们也很简略写出这种代码。

假如咱们此刻来看一下字节码:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

不难发现,无论是子类Apple仍是父类Fruit,他们都生成了 getSum()setSum() 办法 与 sum 字段,而且,父类的 sum 彻底处于糟蹋阶段,咱们根本没法用到。‍

明显这并不是咱们愿意看到的,咱们接下来对其进行改造一下。

改造实践

咱们对上述示例进行略微改造,如下所示:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心
如题,咱们将sum变量界说为了一个笼统变量,然后让子类自行完成。比照字节码能够发现,比较最开端的示例,咱们的父类 Fruit 中减少了一个 sum 变量的损耗。


那有没有办法能不能把 getsum()setSum() 也一起移除呢?‍♂️

答案是能够,咱们利用 接口 改造即可,如下所示:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

如上所示,咱们增加了一个名为 IFruit 的接口,并让 密封父类 完成了这个接口,子类默许在结构函数中完成该特点即可。

调查字节码可发现,咱们的父类一尘不染,无论是从包大小仍是功能,咱们都避免了没必要的损耗。

内联很好,但别太长

inline ,翻译过来为 内联 ,在 Kotlin 中,一般主张用于 高阶函数 中,意图是用来弥补其运行时的 额外开支

其原理也比较简略,在调用时将咱们的代码移动到调用途运用,然后降低办法调用时的 栈帧 层级。

栈帧: 指的是虚拟机在进行办法调用和办法执行时的数据结构,每一个栈帧里都包含了相应的数据,比方 局部参数,操作数栈等等。

Jvm在执行办法时,每执行一个办法会产生一个栈帧,随后将其保存到咱们当前线程所对应的栈里,办法执行结束时再将此办法出栈,

所以内联后就相当于省了一个栈帧调用。

假如上述描绘中,你只记住了后半句,降低栈帧 ,那么此刻你或许现已陷入了一个运用圈套?

过错示例

如下截图中所示,咱们随意创立了一个办法,并增加了 inline 关键字:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

调查截图会发现,此刻IDE现已给出了提示,它主张你移除 inline , Why? 为什么呢?

不是说内联能够进步功能吗,那么不应该任何办法都应该加 inline 进步功能吗?(就是这么倔强)

上面咱们提到了,内联是会将代码移动到调用途,降低 一层栈帧,但这个功能提高真的大吗?

再仔细想想,移动到调用途,移动到调用途。这是什么概念呢?

假定咱们某个办法里代码只有两行(我想不会有人会某个办法只有一行吧),这个办法又被好几处调用,内联是进步了调用功能,毕竟节省了一次栈帧,再加上办法行数少(暂时扔掉虚拟机优化这个底层条件)。

但假如办法里代码有几十行?每次调用都会把代码内联过来,那调用途岂不,带来的包大小影响某种程度上要比内联本钱更高‍!

如下图所示,咱们对上述示例做一个论证:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

Jvm: 我谢谢你。

引荐示例

咱们在文章最开端提到了,Kotlin inline ,一般主张用于 高阶函数(lambda) 中。为什么呢?

如下示例:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

转成字节码后,能够发现,tryKtx() 被创立为了一个匿名内部类 (Simple$test|1) 。每次调用时,相当于需求创立匿名类的实例目标,然后导致二次调用的功能损耗。

那假如咱们给其增加 inline 呢?,反编译后相应的 java代码 如下:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

详细比照图如上所示,不难发现,咱们的调用途现已被替换为原办法,相应的 lambda 也被消除了,然后显著减少了功能损耗。

Tips

假如检查官方库相应的代码,如下所示,比方 with :

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

不难发现,inline 的大多数场景仅且在 高阶函数 而且 办法行数较短 时适用。因为对于普通办法,jvm自身对其就会进行优化,所以 inline 在普通办法上的的意义几乎聊胜于无。

总结如下:

  • 因为内联函数会将办法函数移动到调用途,会增加调用途的代码量,所以对于较长的办法应该避免运用
  • 内联函数应该用于运用了 高阶函数(lambda) 的办法,而不是普通办法。

伴生目标,或许真的不需求

Kotlin 中,咱们不能像 Java 一样,随意界说一个静态办法或许静态特点。此刻 companion object(伴生目标)就会派上用场。

咱们常常会用于界说一个 key 或许 TAG ,类似于咱们在 Java 中界说一个静态的 Key。其运用起来也很简略,如下所示:

class Book {
    companion object {
        val SUM_MAX: Int = 13
    }
}

这是一段普通的代码,咱们在 Book 类中增加了一个伴生目标,其间有一个静态的字段 SUM_MAX。

上述代码看着好像没什么问题,但假如咱们将其转为字节码后再看一看:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

不难发现,仅仅只是想增加一个 静态变量 ,结果凭空增加了一个 静态目标 以及多增加了 get() 办法,这个本钱或许远超出一个 静态参数 的价值。


const

抛开前者不谈(静态目标),那么咱们有没有什么办法能让编译器少生成一个 get() 办法呢(非private)?

留意调查IDE提示,IDE会主张咱们增加一个 const 的参数,如下所示:

companion object {
    const val SUM_MAX: Int = 13
}

增加了 const 后,相应的 get() 办法也会消失掉,然后节省了一个 get() 办法。

const,在 Kotlin 中,用于润饰编译时已知的 val(只读,类似final) 标注的特点。

  • 只能用于顶层的class中,比方 object class 或许 companion object
  • 只能用于根本类型;
  • 不会生成get()办法。

JvmField

假如咱们 某个字段不是 val 标注呢,其是 var (可变)润饰的呢,而且这个字段要对外暴漏(非private)。

此刻不难猜测,相应的字节码后肯定会一起生成 set与get 办法。

此刻就能够运用 @JvmField 来进行润饰。

如下所示:

class Book {
    companion object {
        @JvmField
        var sum: Int = 0
    }
}

相应的字节码如下:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

Tips

让咱们再回到伴生目标自身,咱们真的必定需求它吗?

对于和业务强关联的 key 或许 TAG ,能够选择运用伴生目标,并为其增加 const val,此刻语义上的清晰比内存上的损耗愈加重要,特别在杂乱的业务布景下。

但假如仅用于保存一些key,那么彻底能够运用 object Class 替代,如下所示,将其回归到一个类中:

object Keys {
    const val DEFAULT_SUM = 10
    const val DEFAULT_MIN = 1
    const val LOGIN_KEY = 99
}

Apply!=结构者办法

apply 作为开发中的常客,为咱们带来了不少便当。其内部完成也十分简略,将咱们的目标以函数的办法回来,this 作为接收者。然后以一种高雅的办法完成对目标办法、特点的调用。

但常常会看到有不少同学在结构者办法中写出以下代码,运用 apply 直接作为回来值,这种办法固然看着高雅,功能也几乎没有差别。但这种场景而言,假如咱们留意到其字节码,会发现其并不是最佳之选。

示例

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

如题,咱们存在一个示例Builder,并在其间添加了两个办法,即 addTitle(),与 addSecondTitle() 。后者以 apply 作为回来值,代码可读性十分好,比较前者,在 kotlin 中其显得十分高雅。

但假如咱们去看一眼字节码呢?

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

如上所示,运用了 apply 后,咱们的字节码中增加了多余步骤,比较不运用的,包大小会有一点影响,功能上几乎毫无距离。

Tips

apply 很好用,但需求区分场景。其能够改善咱们在 kotlin 语义下的编程体验,但一起也不是任何场景都需求其。

假如你的办法中需求对某个目标操作屡次,比方调用其办法或许特点,那么此刻能够运用 apply ,反之,假如次数过少,其实你并不需求 apply 的高雅。

警觉,lazy 的运用办法

lazy,中文译名为推迟初始化,顾名思义,用于推迟初始化一些信息。

作用也相对直接,假如咱们有某个目标或字段,咱们或许只想运用时再初始化,此刻就能够先声明,等到运用时再去初始化,而且这个初始化进程默许也是线程安全(不特定运用NONE)。这样的好处就是功能优势,咱们不必运用或许页面加载时就初始化一切,比较过往的 var xx = null ,这种办法必定程度上也愈加快捷。

相应的,lazy一共有三种办法,即:

  • SYNCHRONIZED(同步锁,默许完成)
  • PUBLICATION(CAS)
  • NONE(不作处理)

lazy 尽管运用简略,但在 Android 的开发布景下,lazy 常常简略运用不当‍♂️,也因而常常会出现为了[便当] 而形成的功能隐患。

示例如下:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

如上所示,咱们推迟初始化了一个点击事情,方便在 onCreate() 中进行设置 点击事情 以及后续复用

上述示例尽管看着好像没什么问题。但放在这样的场景下,这个 mClickListener 自身的意义或许并不大。为什么这样说?

  1. 上述运用了 默许的lazy ,即同步锁,而Android默许线程为 UI线程 ,当前操作办法又是 onCreate() ,即当前自身就是线程安全。此刻依然运用 lazy(sys) ,即糟蹋了必定初始化功能。
  2. MainActivity初始化时,会先在 结构函数 中初始化 lazy 目标,即 SYNCHRONIZED 对应的 SynchronizedLazyImpl。也就是说,咱们一开端就现已多生成了一个目标。然后仅仅是为了一个点击事情,内部又会进行包装一次

类似的场景有许多,假如你的lazy是用于 Android生命周期组件 ,再加上自身会在 onCreate() 等中进行调用,那么很或许彻底没有必要推迟初始化。

关于 arrayOf() 的运用细节

对于 arrayOf ,咱们一般常常用于初始化一个数组,但其也隐藏着一些运用细节。

通常来说,对于根本类型的数组,主张运用默许已供给的函数比方,intArrayOf() 等等,然后便于提高功能。

至于原因,咱们下面来剖析,如下所示:

fun test() {
    arrayOf(1, 2, 3)
}
fun testNoInteger() {
    intArrayOf(1, 2, 3)
}

咱们供给了两个办法,前者是默许办法,后者是带优化的办法,详细字节码如下:

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

如题,不难发现,前者运用的是 java 中的 包装类型 ,运用时还需求阅历 拆箱装箱 ,而后者对错包装类型,然后免除了这一操作,然后节省功能。

什么是装箱与拆箱?

布景:Java 中,万物皆目标,而八大根本类型不是目标,所以 Java 为每种根本类型都供给了相应的包装类型。

装箱就是指将根本类型转为包装类型,拆箱则是将包装类型转为根本类型。

总结

本篇中,咱们以日常开发的视角,去探寻了 Kotlin 中那些 [隐藏] 的内存圈套。

仔细回想,上述的不恰当用法都是建立在 [不熟练] 的布景下。Kotlin 自身的各种便当没有任何问题,其使得咱们的 代码可读性开发舒适度 增强了太多。但假如一起,咱们还能留意到其背面的完成,也是不是就能在 功能与高雅 之间找到了一种平衡。

所谓左眼 kt ,右眼 java,正是如此。作为一个 Kotlin 运用者,这也是咱们所不断追寻的。

善用字节码剖析,你的技艺也将更上一筹。

参看

  • Kotlin代码检查在美团的探究与实践

关于我

我是 Petterp ,一个三流 Kotlin 运用者,假如本文对你有所协助,欢迎点赞谈论收藏,你的支持是我持续创造的最大鼓励!