Moosphan / Android-Daily-Interview

:pushpin:每工作日更新一道 Android 面试题,小聚成河,大聚成江,共勉之~

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

2019-10-21:在Kotlin中,什么是内联函数?有什么作用?

Moosphan opened this issue · comments

2019-10-21:在Kotlin中,什么是内联函数?有什么作用?

Kotlin里使用关键 inline 来表示内联函数,那么到底什么是内联函数呢,内联函数有什么好处呢?

  1. 什么是内联inline?
    在 Java 里是没有内联这个概念的,所有的函数调用都是普通方法调用,如果了解 Java 虚拟机原理的,可以知道 Java 方法执行的内存模型是基于 Java 虚拟机栈的:每个方法被执行的时候都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧入栈、出栈的过程。

也就是说每调用一个方法,都会对应一个栈帧的入栈出栈过程,如果你有一个工具类方法,在某个循环里调用很多次,那就会对应很多次的栈帧入栈、出栈过程。这里首先要记住的一点是,栈帧的创建及入栈、出栈都是有性能损耗的。下面以一个例子来说明,看段代码片段:

fun test() {
//多次调用 sum() 方法进行求和运算
println(sum(1, 2, 3))
println(sum(100, 200, 300))
println(sum(12, 34))
//....可能还有若干次
}

/**

  • 求和计算
    */
    fun sum(vararg ints: Int): Int {
    var sum = 0
    for (i in ints) {
    sum += i
    }
    return sum
    }
    在测试方法 test() 里,我们多次调用了 sum() 方法。为了避免多次调用 sum() 方法带来的性能损耗,我们期望的代码类似这样子的:

fun test() {
var sum = 0
for (i in arrayOf(1, 2, 3)) {
sum += i
}
println(sum)

sum = 0
for (i in arrayOf(100, 200, 300)) {
    sum += i            
}
println(sum)

sum = 0
for (i in arrayOf(12, 34)) {
    sum += i            
}
println(sum)

}
3次数据求和操作,都是在 test() 方法里执行的,没有之前的 sum() 方法调用,最后的结果依然是一样的,但是由于减少了方法调用,虽然代码量增加了,但是性能确提升了。那么怎么实现这种情况呢,一般工具类有很多公共方法,我总不能在需要调用这些公共方法的地方,把代码复制一遍吧,内联就是为了解决这一问题。

定义内联函数:

inline fun sum(vararg ints: Int): Int {
var sum = 0
for (i in ints) {
sum += i
}
return sum
}
如上所示,用关键字 inline 标记函数,该函数就是一个内联函数。还是原来的 test() 方法,编译器在编译的时候,会自动把内联函数 sum() 方法体内的代码,替换到调用该方法的地方。查看编译后的字节码,会发现 test() 方法里已经没了对 sum() 方法的调用,凡是原来代码里出现 sum() 方法调用的地方,出现的都是 sum() 方法体内的字节码了。

  1. noinline
    如果一个内联函数的参数里包含 lambda表达式,也就是函数参数,那么该形参也是 inline 的,举个例子:

inline fun test(inlined: () -> Unit) {...}
这里有个问题需要注意,如果在内联函数的内部,函数参数被其他非内联函数调用,就会报错,如下所示:

//内联函数
inline fun test(inlined: () -> Unit) {
//这里会报错
otherNoinlineMethod(inlined)
}

//非内联函数
fun otherNoinlineMethod(oninline: () -> Unit) {

}
要解决这个问题,必须为内联函数的参数加上 noinline 修饰,表示禁止内联,保留原有函数的特性,所以 test() 方法正确的写法应该是:

inline fun test(noinline inlined: () -> Unit) {
otherNoinlineMethod(inlined)
}
3. crossinline
首先来理解一个概念:非局部返回。我们来举个栗子:

fun test() {
innerFun {
//return 这样写会报错,非局部返回,直接退出 test() 函数。
return@innerFun //局部返回,退出 innerFun() 函数,这里必须明确退出哪个函数,写成 return@test 则会退出 test() 函数
}

//以下代码依旧会执行
println("test...")

}

fun innerFun(a: () -> Unit) {
a()
}
非局部返回我的理解就是返回到顶层函数,如上面代码中所示,默认情况下是不能直接 return 的,但是内联函数确是可以的。所以改成下面这个样子:

fun test() {
innerFun {
return //非局部返回,直接退出 test() 函数。
}

//以下代码不会执行
println("test...")

}

inline fun innerFun(a: () -> Unit) {
a()
}
也就是说内联函数的函数参数在调用时,可以非局部返回,如上所示。那么 crossinline 修饰的 lambda 参数,可以禁止内联函数调用时非局部返回。

fun test() {
innerFun {
return //这里这样会报错,只能 return@innerFun
}

//以下代码不会执行
println("test...")

}

inline fun innerFun(crossinline a: () -> Unit) {
a()
}
4. 具体化的类型参数 reified
这个特性我觉得特别牛逼,有了它可以少些好多代码。在 Java 中是不能直接使用泛型的类型的,但是在 Kotlin 中确可以。举个栗子:

inline fun startActivity() {
startActivity(Intent(this, T::class.java))
}
使用时直接传入泛型即可,代码简洁明了:

startActivity()
5. 小结
网上很多学习教程对内联函数的讲解都是千篇一律,说实话刚开始很难理解。本文尝试着用最简单的例子,来讲清楚什么是内联函数。在Java中我们一般会有很多工具类、工具方法,在Kotlin中则没有了工具类一说,通常都是将工具方法设计成顶层的内联函数来使用。

对比java
函数反复调用时,会有压栈出栈的性能消耗

kotlin优化 内联函数 用来解决 频繁调用某个函数导致的性能消耗

使用 inline标记
内联函数,调用非内联函数会报错,,需要加上noinline标记

noinline,让原本的内联函数形参函数不是内联的,保留原有数据特征

crossinline
非局部返回标记
为了不让lamba表达式直接返回内联函数,所做的标记
相关知识点:我们都知道,kotlin中,如果一个函数中,存在一个lambda表达式,在该lambda中不支持直接通过return退出该函数的,只能通过return@XXXinterface这种方式

reified 具体化泛型
java中,不能直接使用泛型的类型
kotlin可以直接使用泛型的类型

使用内联标记的函数,这个函数的泛型,可以具体化展示,所有 能解决方法重复的问题

上面的老哥都讲的很好,但是大多数同学都用的是java,难免有些不友好,所以我用koltin+java字节码的方式,便于大家有一个概念(虽然暂时没有使用kotlin)。
后面3个看不太懂,还得继续学习。
下面,我用实际操作来演示吧;

public class Test {
    public static void main(String[] args) {
        System.out.println(sum(10)+sum(5));
    }

    private static int sum(int a){
        int sum=8;
        sum+=a;
        return sum;
    }
}

这是一段java代码,简单的不能再简单了吧,就是重复的相加,别注意逻辑,只是为了演示。

同样的kotlin代码:

inline fun sum(a: Int): Int {
    var sum = 8
    sum += a
    return sum
}

fun main() {
    println(sum(10)+ sum(5))
}

虽然一眼看上去很简洁,但我们的关注点不在这里,在 inline 关键字上面。为了便于大家学习,我通过查看字节码的方式来转成相应的 java 代码,便于大家更好的理解。

没加 inline 之前

image-20191021220114042

加上 inline 之后

image-20191021220149030

解释就不用多说了吧,kotlin 自动帮我们将方法在编译期就加在了相应的调用处,免除了 java 中的入方法栈与退栈。

后面的有点看不太懂,所以还得再看看。

感觉上面部分还有一些不太合适,只说了它的使用和意义,并没有讨论 inline 什么时候应该使用什么时候不该使用,以及为什么在 kotlin 中需要 inline

inline 真正发挥它的作用的是在包含 lambda 参数的函数中使用 inline 注解,这时才会真正的起到它的节省开销的作用,因为 kotlin 中有大量的高阶函数。

前段时间自己总结了一下,有兴趣可以看一下这个链接:https://www.jianshu.com/p/8a0d5bae9cdf

上面的老哥都讲的很好,但是大多数同学都用的是java,难免有些不友好,所以我用koltin+java字节码的方式,便于大家有一个概念(虽然暂时没有使用kotlin)。
后面3个看不太懂,还得继续学习。
下面,我用实际操作来演示吧;

public class Test {
    public static void main(String[] args) {
        System.out.println(sum(10)+sum(5));
    }

    private static int sum(int a){
        int sum=8;
        sum+=a;
        return sum;
    }
}

这是一段java代码,简单的不能再简单了吧,就是重复的相加,别注意逻辑,只是为了演示。

同样的kotlin代码:

inline fun sum(a: Int): Int {
    var sum = 8
    sum += a
    return sum
}

fun main() {
    println(sum(10)+ sum(5))
}

虽然一眼看上去很简洁,但我们的关注点不在这里,在 inline 关键字上面。为了便于大家学习,我通过查看字节码的方式来转成相应的 java 代码,便于大家更好的理解。

没加 inline 之前

image-20191021220114042

加上 inline 之后

image-20191021220149030

解释就不用多说了吧,kotlin 自动帮我们将方法在编译期就加在了相应的调用处,免除了 java 中的入方法栈与退栈。

后面的有点看不太懂,所以还得再看看。

浅显易懂,小弟看懂了

commented

inline 还可以解决获取泛型的class问题:

inline fun < refied T> String.json2Obj() : T{
   return Gson().fromJson<T>(this, T::class.java)
}

(代码可能有问题

inline 还可以解决获取泛型的class问题:

inline fun <T> String.json2Obj() : T{
   return Gson().fromJson<T>(this, T::class.java)
}

(代码可能有问题

需要加上实化关键字 refied:

inline fun <refied T> String.json2Obj() : T{
   return Gson().fromJson<T>(this, T::class.java)
}