90%的开发者都不知道的 Kotlin技巧以及原理解析
Posted 网易在职程序猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了90%的开发者都不知道的 Kotlin技巧以及原理解析相关的知识,希望对你有一定的参考价值。
Google 引入 Kotlin 的目的就是为了让 android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。
众所周知 xml 十分耗时,因此在 Android 10.0 上新增加 tryInflatePrecompiled
方法,这是一个在编译期运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled
方法根据 XML 预编译生成 compiled_view.dex
, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,但是目前一直处于禁用状态。
因此一些体量比较大的应用,为了极致的优化,缩短一点时间,对于简单的布局,会使用 Kotlin 去重写这部分 UI,但是门槛还是很高,随着 Jetpack Compose 的出现,其目的是让您更快、更轻松地构建原生 Android 应用,前不久 Google 正式发布了 Jetpack Compose 1.0。
Kotlin 优势已经体现在了方方面面,结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是如果使用不当会对性能造成一些损耗,更多内容可前往查看。
以上两篇文章,主要分享了 Kotlin 在实际项目中使用的技巧,以及如果使用不当会对 性能 和 内存 造成的那些影响以及如何规避这些问题等等。
通过这篇文章你将学习到以下内容:
- 什么是 Contract,以及如何使用?
- Kotlin 注解在项目中的使用?
- 一行代码接受 Activity 或者 Fragment 传递的参数?
- 一行代码实现 Activity 之间传递参数?
- 一行代码实现 Fragment 之间传递参数?
- 一行代码实现点击事件,避免内存泄露?
KtKit 仓库
这篇文章主要围绕一个新库 KtKit来介绍一些 Kotlin 技巧,正如其名 KtKit 是用 Kotlin 语言编写的工具库,包含了项目中常用的一系列工具,是 Jetpack ktx 系列的补充,涉及到了很多从 Kotlin 源码、Jetpack ktx、anko 等等知名的开源项目中学习到的技巧,包含了 Kotlin 委托属性、高阶函数、扩展函数、内联、注解的使用等等。
implementation "com.hi-dhl:ktkit:${ktkitVersion}"
因为篇幅原因,文章中不会过多的涉及源码分析,源码部分将会在后续的文章中分享。
什么是 Contract,以及如何使用
众所周知 Kotlin 是比较智能的,比如 smart cast 特性,但是在有些情况下显得很笨拙,并不是那么智能,如下所示。
public inline fun String?.isNotNullOrEmpty(): Boolean {
return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}
fun testString(name: String?) {
if (name.isNotNullOrEmpty()) {
println(name.length) // 1
}
}
正如你所见,只有字符串 name 不为空时,才会进入注释 1 的地方,但是以上代码却无法正常编译,如下图所示。
编译器会告诉你一个编译错误,经过代码分析只有当字符串 name 不为空时,才会进入注释 1 的地方,但是编译器却无法正常推断出来,真的是编译器做不到吗?看看官方文档是如何解释的。
However, as soon as these checks are extracted in a separate function, all the smartcasts immediately disappear:
将检查提取到一个函数中, smart cast 所带来的效果都会消失
编译器无法深入分析每一个函数,原因在于实际开发中我们可能写出更加复杂的代码,而 Kotlin 编译器进行了大量的静态分析,如果编译器去分析每一个函数,需要花费时间分析上下文,增加它的编译耗时的时间。
如果要解决上诉问题,这就需要用到 Contract 特性,Contract 是 Kotlin 提供的非常有用的特性,Contract 的作用就是当 Kotlin 编译器没有足够的信息去分析函数的情况的时候,Contracts 可以为函数提供附加信息,帮助 Kotlin 编译器去分析函数的情况,修改代码如下所示。
inline fun String?.isNotNullOrEmpty(): Boolean {
contract {
returns(true) implies (this@isNotNullOrEmpty != null)
}
return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}
fun testString(name: String?) {
if (name != null && name.isNotNullOrEmpty()) {
println(name.length) // 1
}
}
相比于之前的代码,在 isNotNullOrEmpty()
函数中添加了 contract 代码块即可正常编译通过,这行代码的意思就是,如果返回值是 true ,this 所指向对象就不为 null。 而在 Kotlin 标准库中大量的用到 contract 特性。
Kotlin 注解在项目中的使用
contract 是 Kotlin 1.3 添加的实验性的 API,如果我们调用实验性的 API 需要添加 @ExperimentalContracts
注解才可以正常使用,但是如果添加 @ExperimentalContracts
注解,所有调用这个方法的地方都需要添加注解,如果想要解决这个问题。只需要在声明 contract 文件中的第一行添加以下代码即可。
@file:OptIn(ExperimentalContracts::class)
在上述示例中使用了 inline 修饰符,但是编译器会有一个黄色警告,如下图所示。
编译器建议我们将函数作为参数时使用 Inline,Inline (内联函数) 的作用:提升运行效率,调用被 inline 修饰符的函数,会将方法内的代码段放到调用处。
既然 Inline 修饰符可以提升运行效率,为什么还给出警告,因为 Inline 修饰符的滥用会带来性能损失。
Inline 修饰符常用于下面的情况,编译器才不会有警告:
- 将函数作为参数(例如:lambda 表达式)
- 结合 reified 实化类型参数一起使用
但是在普通的方法中,使用 Inline 修饰符,编译会给出警告,如果方法体的代码段很短,想要通过 Inline 修饰符提升性能(虽然微乎其微),可以在文件的第一行添加下列代码,可消除警告。
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
然后在使用 Inline 修饰符的地方添加以下注解,即可愉快的使用。
@kotlin.internal.InlineOnly
注解 @kotlin.internal.InlineOnly
的作用:
- 消除编译器的警告
- 修改内联函数的可见性,在编译时修改成 private
// 未添加 InlineOnly 编译后的代码
public static final void showShortToast(@NotNull Context $this$showShortToast, @NotNull String message) {
......
Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}
// 添加 InlineOnly 编译后的代码
@InlineOnly
private static final void showShortToast(Context $this$showShortToast, String message) {
......
Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}
一行代码接受 Activity 或者 Fragment 传递的参数
如果想要实现一行代码接受 Activity 或者 Fragment 传递的参数,可以通过 Kotlin 委托属性来实现,在仓库 KtKit中提供了两个 API,根据实际情况使用即可。
class ProfileActivity : Activity() {
// 方式一: 不带默认值
private val userPassword by intent<String>(KEY_USER_PASSWORD)
// 方式二:带默认值:如果获取失败,返回一个默认值
private val userName by intent<String>(KEY_USER_NAME) { "公众号:ByteCode" }
}
一行代码实现 Activity 之间传递参数
这个思路是参考了 anko 的实现,同样是提供了两个 API , 根据实际情况使用即可,可以传递 Android 支持的任意参数。
// API:
activity.startActivity<ProfileActivity> { arrayOf( KEY_USER_NAME to "ByteCode" ) }
activity.startActivity<ProfileActivity>( KEY_USER_NAME to "ByteCode" )
// Example:
class ProfileActivity : Activity() {
......
companion object {
......
// 方式一
activity.startActivity<ProfileActivity> {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
// 方式二
activity.startActivity<ProfileActivity>(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
}
Activity 之间传递参数 和 并回传结果
// 方式一
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE,
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
// 方式二
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE) {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
回传结果
// 方式一
setActivityResult(Activity.RESULT_OK) {
arrayOf(
KEY_RESULT to "success",
KEY_USER_NAME to "ByteCode"
)
}
// 方式二
setActivityResult(
Activity.RESULT_OK,
KEY_RESULT to "success",
KEY_USER_NAME to "ByteCode"
)
一行代码实现 Fragment 之间传递参数
和 Activity 一样提供了两个 API 根据实际情况使用即可,可以传递 Android 支持的任意参数。
// API:
LoginFragment().makeBundle( KEY_USER_NAME to "ByteCode" )
LoginFragment().makeBundle { arrayOf( KEY_USER_NAME to "ByteCode" ) }
// Example:
class LoginFragment : Fragment(R.layout.fragment_login) {
......
companion object {
......
// 方式一
fun newInstance1(): Fragment {
return LoginFragment().makeBundle(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
// 方式二
fun newInstance2(): Fragment {
return LoginFragment().makeBundle {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
}
}
}
一行代码实现点击事件,避免内存泄露
KtKit 提供了常用的三个 API:单击事件、延迟第一次点击事件、防止多次点击
单击事件
view.click(lifecycleScope) { showShortToast("公众号:ByteCode" }
延迟第一次点击事件
// 默认延迟时间是 500ms
view.clickDelayed(lifecycleScope){ showShortToast("公众号:ByteCode" }
// or
view.clickDelayed(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }
防止多次点击
// 默认间隔时间是 500ms
view.clickTrigger(lifecycleScope){ showShortToast("公众号:ByteCode") }
// or
view.clickTrigger(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }
但是 View#setOnClickListener
造成的内存泄露,如果做过性能优化的同学应该会见到很多这种 case。
根本原因在于不规范的使用,在做业务开发的时候,根本不会关注这些,那么如何避免这个问题呢,Kotlin Flow 提供了一个非常有用的 API callbackFlow
,源码如下所示。
fun View.clickFlow(): Flow<View> {
return callbackFlow {
setOnClickListener {
safeOffer(it)
}
awaitClose { setOnClickListener(null) }
}
}
callbackFlow
正如其名将一个 callback 转换成 flow,awaitClose
会在 flow 结束时执行。
那么 flow 什么时候结束执行
源码中我将 Flow 通过 lifecycleScope 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会将 Listener 置为 null,有效的避免内存泄漏,源码如下所示。
inline fun View.click(lifecycle: LifecycleCoroutineScope, noinline onClick: (view: View) -> Unit) {
clickFlow().onEach {
onClick(this)
}.launchIn(lifecycle)
}
结语
最后给大家分享一份谷歌大佬编写高级Kotlin强化实战(附Demo)。
目录乘上:
第一章 Kotlin入门教程
- Kotlin 概述
- Kotlin 与 Java 比较
- 巧用 Android Studio
- 认识 Kotlin 基本类型
- 走进 Kotlin 的数组
- 走进 Kotlin 的集合
- 集合问题
- 完整代码
- 基础语法
第二章 Kotlin 实战避坑指南
- 方法入参是常量,不可修改
- 不要 Companion 、INSTANCE ?
- Java 重载,在 Kotlin 中怎么巧妙过渡一下?
- Kotlin 中的判空姿势
- Kotlin 复写 Java 父类中的方法
- Kotlin “狠”起来,连TODO 都不放过!
- is、as` 中的坑
- Kotlin 中的 Property 的理解
- also 关键字
- takeIf 关键字
- takeIf 关键字
- 单例模式的写法
第三章 项目实战《Kotlin Jetpack 实战》
- 从一个膜拜大神的 Demo 开始
- Kotlin 写 Gradle 脚本是一种什么体验?
- Kotlin 编程的三重境界
- Kotlin 高阶函数
- Kotlin 泛型
- Kotlin 扩展
- Kotlin 委托
- 协程“不为人知”的调试技巧
- 图解协程:suspend
如果你需要这份完整版的《高级Kotlin强化实战(附Demo)》,点击我的GitHub 免费领取吧!!!
以上是关于90%的开发者都不知道的 Kotlin技巧以及原理解析的主要内容,如果未能解决你的问题,请参考以下文章