Kotlin提供了一个reified关键字,这个关键字在实际项目中有很大的用处,比如泛型相关的封装,SDK封装等。今天我们就来学习下这块的知识。首先需要先了解下内联函数,也就是inline关键字

内联函数(inline)

在Kotlin中内联函数就是用关键字inline修饰的函数,它有什么作用呢?我看下面一段代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    getUserInfo {
        println("get user info.")
    }
}

inline fun getUserInfo(block: () -> Unit): String {
    println("start to invoke getUserInfo method.")
    block.invoke()
    return "UserInfo"
}

很简单的一段代码,编译成apk后,我们可以看下它对应Class文件,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public final class Main {
    public static final void main() {
        // 调用处,不在是调用函数,而是代码块
        System.out.println((Object) "start to invoke getUserInfo method.");
        System.out.println((Object) "get user info.");
    }

    public static final String getUserInfo(Functions<Unit> block) {
        Intrinsics.checkNotNullParameter(block, "block");
        System.out.println((Object) "start to invoke getUserInfo method.");
        block.invoke();
        return "UserInfo";
    }
}

从上面的反编译的代码可以看到: - 在调用内联函数的地方,不再是一个函数,而是函数内的代码块 - 而原来单独的getUserInfo函数还是存在,且函数参数编译成了对应的FunctionN对象

上从面可以知道,内联函数在被调用时,并不是分配栈调用函数,而是在编译时,把内联函数中的代码块替换到调用处。

优点

为什么会引入inline的概念呢?主要的原因我个人认为有两个: - 使用inline特性,性能上会有所提升,因为减少了调用函数时的内存,函数参数对象的创建等 - 因为Kotlin支持高阶函数,存在大量的匿名函数,lambda,使用inline可以减少中间类的创建

注意:如果内联函数的代码块很多,那么有可能导致调用处的函数代码量很大。

reified

说这个之前,我先看下在Java中,如何实例化一个泛型对象?我们唯一的办法是像下面这样:

1
2
3
4
5
6
7
    public static <T> T newInstance(Class<T> tClass) {
        try {
            return tClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } 
    }

只能通过传递Class的方式来创建,是因为存在泛型擦除的机制,无法知道T到底是什么类型,无法获取泛型TClass,所以只能在调用处,通过参数传递进来。

在Kotin中,同样也存在泛型擦除,但是他为了解决这个问题,新增了一个 reified关键字,这个关键字的主要作用就是可以具体化泛型T的类型

我们现在用reified关键字实现一个实例化泛型对象的工具方法,如下:

1
2
3
4
5
6
7
8
inline fun <reified T> newInstance(): T? {
    runCatching {
        // 这里直接可以拿到T的class
        val clazz = T::class.java
        return clazz.newInstance()
    }
    return null
}

可以看到,我们不用像Java一样传递泛型的具体Class,直接可以通过泛型拿到Class。在之前的Kotlin属性委托的巧妙使用-埋点上报封装中,我们就是利用到了这个原理实现了埋点的封装上报,非常的方便有效,是封装库的利器。

因为类型已知,所以正常的操作符如!isas 现在都能正常使用。那之前的我们封装SharedPreferences保存key-value的那种方式都可以重新利用这个特性进行封装。

注意:reified参数只能用在内联函数中

原理

那为什么会有这个效果呢?我们先看一个例子:

1
2
3
4
5
6
7
8
fun main() {
    getUserInfo("hello refied")
}

inline fun <reified T> getUserInfo(t: T) {
    // 打印泛型的值
    println("start to invoke getUserInfo method.$t")
}

上面是是一段很简单的代码,就是打印泛型t的值。我们反编译最后生成的class类,结果如下:

1
2
3
4
5
public final class Main {
    public static final void main() {
        System.out.println((Object) ("start to invoke getUserInfo method." + ((Object) "hello refied")));
    }
}

从反编译的结果可以看出,调用的方法没有了,反而只有getUserInfo方法内的代码块,而日志中的T,直接替换成了调用方法传递进去的值hello refied

从这可以明白为什么reified要和内联函数一期使用,主要原因就是编译器在编译的时候,会把内联函数的代码替换到调用处。而reified关键字修饰的泛型T,会泛型T直接替换为调用处实际的数据类型。