为数不多的人知道的 Kotlin 技巧及解析(三)

为数不多的人知道的 Kotlin 技巧及解析(三)

  • 如果评论区没有及时回复,欢迎来公众号:ByteCode 咨询
  • 公众号:ByteCode。致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、算法、译文、系统源码相关的文章

文章中没有奇淫技巧,都是一些在实际开发中常用的技巧

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)
}

总结

仓库 KtKit 是用 Kotlin 语言编写的工具库,包含了项目中常用的一系列工具,但是目前还不是很完善,正在陆续将一些常用的功能,结合着 Kotlin 的高级函数的特性,不仅让代码可读性更强,使用更加简单,而且还可以帮助我们解决项目中常见的问题。

项目中引用了 spotless 插件,执行 ./gradlew spotlessApply 会将 Java 、Kotlin 、xml 、gradle 、md 、gitignore 等等文件按照官方标准去格式化代码。这也是 Google 提交代码的时候,推荐的方式。

全文到这里就结束了,如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR ❤️❤️❤️


如果有帮助 点个赞 就是对我最大的鼓励

代码不止,文章不停

欢迎关注公众号:ByteCode,持续分享最新的技术


致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,在技术的道路上一起前进

Android10 源码分析

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis

算法题库的归纳和总结

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin

精选国外的技术文章

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation

评论