Kotlin Vocabulary | 内联函数的原理与应用
inline 关键字
https://kotlinlang.org/docs/reference/inline-functions.html
函数调用——工作原理
我们在应用中常常要用到 SharedPreferences,现在假设您为了减少每次向 SharedPreferences 中写入内容时产生的模板代码,实现了以下实用函数:
fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) {
editor.commit()
} else {
editor.apply()
}
}
然后,您就可以用这个方法保存一个字符串 "token" :
private const val KEY_TOKEN = “token”
class PreferencesManager(private val preferences: SharedPreferences){
fun saveToken(token: String) {
preferences.edit { putString(KEY_TOKEN, token) }
}
}
NEW com/example/inlinefun/PreferencesManager$saveToken$1
为了便于理解,让我们查看一下反编译后的代码。我们的 saveToken 函数反编译后的代码如下 (我做了注释和格式化):
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final void saveToken(@NotNull final String token) {
// 我们定义的修改 SharedPreferences 的扩展方法被调用了
PreferenceManagerKt.edit$default(
this.preferences, // SharedPreferences 实例对象
false,// commit 标记的默认值
(Function1)(new Function1() { // 为 action 参数创建了新的 Function 对象
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke((Editor)var1);
return Unit.INSTANCE;
}
public final void invoke(@NotNull Editor $this$edit) {
Intrinsics.checkParameterIsNotNull($this$edit, "$receiver");
$this$edit.putString("token", token); // 我们 action 参数中的实现
}
}), 1, (Object)null);
}
每个高阶函数都会造成函数对象的创建和内存的分配,从而带来额外的运行时开销。
内联函数——工作原理
inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) { … }
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final void saveToken(@NotNull String token) {
// SharedPreferences.edit 函数中的内容
SharedPreferences $this$edit$iv = this.preferences;
boolean commit$iv = false;
int $i$f$edit = false;
Editor editor$iv = $this$edit$iv.edit();
Intrinsics.checkExpressionValueIsNotNull(editor$iv, "editor");
int var7 = false;
// action 参数中实现的内容
editor$iv.putString("token", token);
// SharedPreferences.edit 函数中的内容
editor$iv.apply();
}
应该在哪些地方使用 inline 标记?
⚠️ 如果您试图标记为内联函数的函数,并没有接收另一个函数作为参数,您将无法获得明显的性能提升,而且 IDE 甚至会建议您移除 inline 标记:
⚠️ 因为 inline 关键字可能会增加代码的生成量,所以一定要避免内联大型函数。举例来说,如果去查看 Kotlin 标准库中的内联函数,您会发现它们大部分都只有 1 - 3 行。
⚠️ 不要内联大型函数!
⚠️ 使用内联函数时,您不能持有传入的函数参数对象的引用,也不能将传入的函数参数对象传递给另一个函数——这么做将会触发编译器报错,它会说您非法使用内联参数 (inline-parameter)。
举个例子,我们修改一下 edit 方法和 saveToken 方法。edit 方法获得了一个新的函数参数,并在随后将其传递给了另一个函数。saveToken 方法则会在新的函数参数中更新一个随意设置的模拟变量:
fun myFunction(importantAction: Int.() -> Unit) {
importantAction(-1)
}
inline fun SharedPreferences.edit(
commit: Boolean = false,
importantAction: Int.() -> Unit = { },
action: SharedPreferences.Editor.() -> Unit
) {
myFunction(importantAction)
...
}
...
fun saveToken(token: String) {
var dummy = 3
preferences.edit(importantAction = { dummy = this}) {
putString(KEY_TOKEN, token)
}
}
我们将会看到 myFunction(importantAction) 产生了一个错误:
当遇到这种情况时,基于您函数的不同,有下面这些解决方案:
第一种情况: 如果您的函数有多个函数参数,但是您需要持有其中某个的引用时,您可以将对应的参数标记为 noinline。
noinline https://kotlinlang.org/docs/reference/inline-functions.html#noinline
通过使用 noinline,编译器就只会为对应函数创建新的 Function 对象,其余的则依旧会被内联。
我们的 edit 函数现在会变成下面这样:
inline fun SharedPreferences.edit(
commit: Boolean = false,
noinline importantAction: Int.() -> Unit = { },
action: SharedPreferences.Editor.() -> Unit
) {
myFunction(importantAction)
...
}
如果我们去查看字节码,将会看到这里出现了一个 NEW 指令的调用:
NEW com/example/inlinefun/PreferencesManager$saveToken$1
在反编译后的代码中,我们会看到如下内容 (加入了注释):
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final void saveToken(@NotNull String token) {
// saveToken 方法中的功能
final IntRef x = new IntRef();
x.element = 3;
// 内联 edit 方法中的功能
SharedPreferences $this$edit$iv = this.preferences;
// noinline 函数声明导致 new Function 被调用
Function1 importantAction$iv = (Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int $receiver) {
// saveToken 的功能
x.element = $receiver;
}
});
// 内联 edit 方法中的功能
boolean commit$iv = false;
int $i$f$edit = false;
PreferenceManagerKt.myFunction(importantAction$iv);
Editor editor$iv = $this$edit$iv.edit();
Intrinsics.checkExpressionValueIsNotNull(editor$iv, "editor");
int var9 = false;
editor$iv.putString("token", token);
editor$iv.apply();
}
推荐阅读