为数不多的人知道的 Kotlin 技巧
Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。
众所周知 xml 十分耗时,因此在 Android 10.0 上新增加 tryInflatePrecompiled
方法,这是一个在编译期运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled
方法根据 XML 预编译生成 compiled_view.dex
, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,但是目前一直处于禁用状态。源码解析请查看 Android 资源加载源码分析一。
因此一些体量比较大的应用,为了极致的优化,缩短一点时间,对于简单的布局,会使用 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 委托属性、高阶函数、扩展函数、内联、注解的使用等等。
如果想要使用文中的 API 需要将下列代码添加在模块级 build.gradle
文件内, 最新版本号请查看 版本记录。
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 特性。 上述示例的使用可前往查看 KtKit/ProfileActivity.kt。
Kotlin 注解在项目中的使用
contract 是 Kotlin 1.3 添加的实验性的 API,如果我们调用实验性的 API 需要添加 @ExperimentalContracts
注解才可以正常使用,但是如果添加 @ExperimentalContracts
注解,所有调用这个方法的地方都需要添加注解,如果想要解决这个问题。只需要在声明 contract 文件中的第一行添加以下代码即可。
@file:OptIn(ExperimentalContracts::class)
在上述示例中使用了 inline 修饰符,但是编译器会有一个黄色警告,如下图所示。
编译器建议我们将函数作为参数时使用 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();
}
关于注解完整的使用案例,可前往仓库 KtKit 查看。
一行代码接受 Activity 或者 Fragment 传递的参数
如果想要实现一行代码接受 Activity 或者 Fragment 传递的参数,可以通过 Kotlin 委托属性来实现,在仓库 KtKit 中提供了两个 API,根据实际情况使用即可。案例可前往查看 KtKit/ProfileActivity.kt。
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 支持的任意参数,案例可前往查看 KtKit/ProfileActivity.kt。
// 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 支持的任意参数,源码前往查看 KtKit/LoginFragment.kt。
// 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)
}