Kotlin inline内联函数

今天介绍下 Kotlin 中 非常重要的 内联函数,小伙伴系紧鞋带准备发车

普通函数调用

下面测试整数相加的情况

fun calculate() {
    println(add(a = 1, b = 3))
}

fun add(a: Int, b: Int) = a + b

反编译查看下 java 代码(Android Studio或idea下可以使用 kotlin插件直接查看,路径是 Tools → Kotlin → Show Kotlin Bytecode → DECOMPILE

public final void calculate() {
  int var1 = this.add(1, 3);
  System.out.println
}

final int add(int a, int b) {
  return a + b;
}

可以看到这是一个正常的函数调用,在calculate函数内部调用了add

inline修饰的普通函数

我们再来看下将add添加了inline的效果

fun calculate() {
    println(add(a = 1, b = 3))
}

inline fun add(a: Int, b: Int) = a + b

反编译查看下 java 代码

public final void calculate() {
  byte a$iv = 1;
  int b$iv = 3;
  int $i$f$add = false;
  int var1 = a$iv + b$iv;
  System.out.println(var1);
}

final int add(int a, int b) {
  int $i$f$add = 0;
  return a + b;
}

可以看到calculate调用了inline函数add,编译时期将add函数方法体拷贝到了调用的地方,意味着方法的调用栈少了一层
add函数虽然可以是可以添加inline,不过编译器却给出了警告

common_inline_warn.png

大概意思就是inline函数在此并不会提高性能,inline更适合在函数参数为函数类型的函数中使用(高阶函数)

普通高阶函数

我们来看下不加inlinelambda参数的情况

fun calculate() {
    add(a = 1, b = 3) {
        println("a + b = $it")
    }
}

fun add(a: Int, b: Int, result: (Int) -> Unit): Int {
    val sum = a + b
    result.invoke(sum)
    return sum
}

反编译查看下 java 代码

public final void calculate() {
  this.add(1, 3, (Function1)null.INSTANCE);
}

public final int add(int a, int b, Function1 result) {
  int sum = a + b;
  result.invoke(sum);
  return sum;
}

是不是发现很奇怪,我们的lambda竟然转换成了(Function1)null.INSTANCE,这是个啥东西?
其实(Function1)null.INSTANCE,是由于反编译器工具在找不到等效的 Java 类时的显示的结果。
这个时候我们就需要使用到我们的反编译工具 jadx了,这里附带 jadx 的地址,有需要学习的小伙伴可自行查阅

jadx 下的代码是这样的

public final void calculate() {
    add(1, 3, calculate$1.INSTANCE);
}

public final int add(int a, int b, Function1<? super Integer, Unit> result) {
    Intrinsics.checkNotNullParameter(result, "result");
    int sum = a + b;
    result.mo2833invoke(Integer.valueOf(sum));
    return sum;
}

final class calculate$1 extends Lambda implements Function1<Integer, Unit> {
   public static final calculate$1 INSTANCE = new calculate$1();

    calculate$1() {
        super(1);
    }

    @Override // kotlin.jvm.functions.Function1
    /* renamed from: invoke */
    public /* bridge */ /* synthetic */ Unit mo2922invoke(Integer num) {
        invoke(num.intValue());
        return Unit.INSTANCE;
    }

    public final void invoke(int it) {
        System.out.println((Object) ("a + b = " + it));
    }
}

可以看到 lambda 表达式转换成了一个 Function1 对象,它是 Kotlin 函数的一部分,那为什么是 Function1呢,实际上是因为 lambda 中传递了一个参数,如果没传递参数则是 Function0,以此类推。这个 Function1 对象的创建无疑是会消耗内存的,假如我们的代码中存在很多的高阶函数(参数类型是函数或者返回值类型是函数,在代码编译之后那么是不是会创建很多的 Function对象呢?这个内存的消耗是不可估计的,所以 Kotlin 官方为了优化这个点,出现了 inline 内联函数

inline修饰的高阶函数

我们还是继续使用上面的例子来看下 inline 内联函数是如何提高性能的

fun calculate() {
    add(a = 1, b = 3) {
        println("a + b = $it")
    }
}

inline fun add(a: Int, b: Int, result: (Int) -> Unit): Int {
    val sum = a + b
    result.invoke(sum)
    return sum
}

继续反编译看下 java 代码

public final void calculate() {
  byte a$iv = 1;
  int b$iv = 3;
  int $i$f$add = false;
  int sum$iv = a$iv + b$iv;
  int var7 = false;
  String var8 = "a + b = " + sum$iv;
  System.out.println(var8);
}

public final int add(int a, int b, @NotNull Function1 result) {
  int $i$f$add = 0;
  Intrinsics.checkNotNullParameter(result, "result");
  int sum = a + b;
  result.invoke(sum);
  return sum;
}

可以看到相比于不加 inline ,方法的调用栈少了一层,并且不会生成额外的对象,这对内存还说是一个很棒的优化。一般情况下我们在高频调用的高阶函数下使用inline ,减少内存的消耗。

noinline

noinline 刚好跟 inline 相反, 它是让 高阶函数中函数类型的参数 不参与内联,先来看下为什么会有 noinline
函数类型的参数不止可以当做函数去调用,还可以当做对象去使用,例如我们把它当做函数返回值
我们还是以上面为示例修改下代码

fun calculate() {
    add(a = 1, b = 3,
        addPrev = {
            println("addPrev!")
        }, result = {
            println("a + b = $it")
        }, addPost = {
            println("addPost!")
        }
    )
}

inline fun add(a: Int, b: Int, addPrev: () -> Unit, result: (Int) -> Unit, addPost: () -> Unit): () -> Unit {
    addPrev.invoke()
    val sum = a + b
    result.invoke(sum)
    return addPost
}

上面的代码编译是不会通过的,并且编译器给出了错误提示

noinline_warn.png

我们知道在 inline 内联函数中函数类型的参数是不会创建函数对象的,它仅仅是作为一个函数体存在而不是一个函数对象,所以无法当成一个对象进行返回
如果我们还是需要将函数类型的参数作为对象去使用,编译器也给出了解决方案,给函数类型的参数加上 noinline 即可

fun calculate() {
    add(a = 1, b = 3,
        addPrev = {
            println("addPrev!")
        }, result = {
            println("a + b = $it")
        }, addPost = {
            println("addPost!")
        }
    )
}

inline fun add(
    a: Int,
    b: Int,
    addPrev: () -> Unit,
    result: (Int) -> Unit,
    noinline addPost: () -> Unit
): () -> Unit {
    addPrev.invoke()
    val sum = a + b
    result.invoke(sum)
    return addPost
}

crossinline

crossinline 是局部加强内联的意思
我们来看下下面这个场景,在 lambda 内部直接return

fun main() {
    linpopopo {
        println("linpopopo function")
        return
    }
    println("main function")
}

inline fun linpopopo(action: () -> Unit) {
    action.invoke()
}

这个return会结束那个函数?linpopopo?main?

按常理来说,这个return会结束 linpopopo函数的执行,后面的 println("main function") 会被执行,不过这里是 linpopopo 内联函数,它在编译的时候会将函数体移到调用的地方,我们来看下编译之后的代码就清楚了

public final void main() {
  int $i$f$linpopopo = false;
  int var3 = false;
  String var4 = "linpopopo function";
  System.out.println(var4);
}

public final void linpopopo(@NotNull Function0 action) {
  int $i$f$linpopopo = 0;
  Intrinsics.checkNotNullParameter(action, "action");
  action.invoke();
}

看到了吧,这里 return 结束的是 main函数,即最外层的函数。甚至在编译的时候会将 println("main function") 舍弃掉不参与编译,因为它总是不会执行,参与编译毫无意义
那我们在函数类型参数的 lambda 表达式中的return的结果就要看该函数是不是内联函数了,这就让我们敲代码极其的不便利了,每个函数我们都进去看下是否是内联函数,这对我们的时间很大的消耗
后来 Kotlin 官方制定了一条新规则,lambda表达式中不允许直接使用return,除非这个 Lambda 是内联函数的参数,并且结束的是最外层的函数
解决上面的问题我们也可以使用return@label的方式结束代码作用域,例如直接return隐式标签 linpopopo

fun main() {
    linpopopo {
        println("linpopopo function")
        return@linpopopo
    }
    println("main function")
}

inline fun linpopopo(action: () -> Unit) {
    action.invoke()
}

反编译再看下代码

public final void main() {
  int $i$f$linpopopo = false;
  int var3 = false;
  String var4 = "linpopopo function";
  System.out.println(var4);
  String var1 = "main function";
  System.out.println(var1);
}

public final void linpopopo(@NotNull Function0 action) {
  int $i$f$linpopopo = 0;
  Intrinsics.checkNotNullParameter(action, "action");
  action.invoke();
}

干得漂亮,非常符合我们的预期,老板都夸你解决问题的方式多,加鸡腿加鸡腿
再来看下下面这个场景,有的时候我们需要在主线程上面去执行 lambda 表达式,这里主线程是使用协程进行切换

fun main() {
    linpopopo {
        println("linpopopo function")
        return
    }
    println("main function")
}

inline fun linpopopo(action: () -> Unit) {
    MainScope().launch {
        action.invoke()
    }
}

这样会引起一个问题,linpopopo 函数和 main函数就属于间接调用关系,导致 lambda 表达式里的 return 无法结束 main 函数。那么它在这里结束的是谁?其实压根不会结束谁,因为上面这段代码根本不会编译通过,编译器也给出了错误提示

crossinline_warn.png

千呼万唤始出来,编译器让我们使用 crossinline 去修饰函数类型的参数,这样间接调用关系才会成立

inline fun linpopopo(crossinline action: () -> Unit) {
    MainScope().launch {
        action.invoke()
    }
}

我们又回到了原始的问题,加了 crossinline 这里的 lambda 表达式里的 return到底结束了谁?是 main 函数 还是协程 launch作用域呢?
对于这种歧义的问题,Kotlin 官方又增加了一条新的规定,内联函数中被 crossinline 修饰的函数类型的参数不允许return
所以上面的 return 也不会编译成功,当然这里还是可以使用return@label的方式结束代码作用域的

fun main() {
    linpopopo {
        println("linpopopo function")
        return@linpopopo
    }
    println("main function")
}

inline fun linpopopo(crossinline action: () -> Unit) {
    MainScope().launch {
        action.invoke()
    }
}

总结

  • inline: 编译时将函数体拷贝到调用的地方,减少函数类型对象的创建
  • noinline: 局部关掉内联,解决不能把函数类型的参数当做对象来使用的问题
  • crossinline: 局部加强内联,让内联函数中函数类型的参数可以间接被调用,并且 crossinline 修饰的函数类型的参数不允许return

致谢

文中部分观点参考了扔物线大佬的作品,小伙伴可自行查阅哦,扔物线大佬文章很生动哈哈哈
原文地址:Kotlin源码里成吨的noinline和crossinline是干嘛的?看完视频你转头也写了一吨

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容