KN:Kotlin 与 OC 交互

Kotlin Native (KN)

Kotlin/Native | Kotlin Documentation

KN是一种技术,可以将Kotlin的代码编译为原生的二进制库,无需依赖虚拟机即可运行,主要包括下面两部分的内容:

● 基于LLVM的Kotlin编译器的后端

● Kotlin标准库的原生实现

KN 是 Kotlin Multiplatform 的一部分,KN让 Kotlin 代码可以在更多的目标平台运行。

目标平台(Target Platform) 指代码编译后最终运行的硬件和操作系统环境

处理器指令集架构 x86、x86_64、arm32、arm64
二进制文件的组织规范 ELF(Linux)、Mach-O(macOS/iOS)、PE(Windows)
操作系统特有的接口 不同的系统调用接口(IO、多线程)

编译

将 Kotlin IR 转换为 LLVM IR,最终通过LLVM生成对应平台的可执行文件。

处理器指令集架构 以及 二进制文件格式 的差异被处理

Runtime

运行时库,抹平操作系统的差异,为Kotlin标准库提供底层功能实现

可以参考C/C++运行时库理解,可以自行搜索

编译器跟runtime是相互配合的,runtime 提供的能力,需要编译器来桥接,编译器也需要知道哪些功能可以怎么桥接才能完成编译。

Helloworld

创建一个 Hello.kt 文件,内容如下:

kotlin 复制代码
interface HelloAble {
    fun makeIt(): String
}
 
open class Hello(open var name: String) {
 
    var helloable: HelloAble? = null
 
    open fun greetPrefix(): String {
        return "Hello"
    }
 
    fun greet(): String {
        return ("${greetPrefix()} $name")
    }
 
    fun triggleInterface() {
        helloable?.makeIt()
    }
 
    companion object {
        fun createObj(): Hello {
            return Hello("luyu")
        }
    }
}
 
open class Helloworld(override var name: String) : Hello(name) {
    override fun greetPrefix(): String {
        val s = super.greetPrefix()
        return "$s world"
    }
}
 
fun main() {
    val h = Helloworld("cindy")
    println(h.greet())
    h.triggleInterface()
}

执行命令:

scala 复制代码
kotlinc-native Hello.kt -g -Xno-inline -p program -o Hello

● kotlinc-native:编译Kotlin文件

● -g:带调试信息

● -p program:编译为可执行程序,还可以编译为动态库,静态库,framework等

● -o Hello:输出文件名为Hello

在我目前的Mac电脑上,生成下面两个产物:

可以直接运行,不依赖虚拟机环境

启动过程

使用MachOView打开Hello.kexe,搜索main函数,发现函数符号名为kfun:#main(){},可以使用lldb调试看看:

查看堆栈:

需要先初始化runtime,创建参数

调到我们的main函数

对应的runtime源码

对象创建

main函数反编译结果

整体跟C++的机制比较像,成员方法编译为全局的C函数,对象实例是一个C结构体; 调用成员方法就是调用全局函数,并把对象作为第一个参数传入

通过 MachOView 可以看到编译后所有的对应的函数

我们打断点到init方法

清楚的看到有两个参数,第一个参数是this对象,第二个参数是name,分别通过 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 0 寄存器跟 x0寄存器 跟 </math>x0寄存器跟x1 寄存器传递

我们给name传的值是字符串"cindy",验证下是否正确

x/8gx $x1 读取 $x1 地址对应的内存中的内容,g:8个字节为一组 x:以16进制显示 重复 8 次

x/8hx 0x1000b22c0 读取0x1000b22c0地址对应的内存中的内容 h:两个字节为一组 x:以16进制显示 重复 8 次 似乎看到了ASCII 吗

● 以字符串显示,发现了确实是cindy,Kotlin每个字符大小是两个字节

编译器已经把源码中的 "cindy" 字符串编译为了放在全局数据区的一个 String 对象(全局常量),C结构体定义如下

c 复制代码
struct StringHeader {
    TypeInfo* typeInfoOrMeta_;    uint32_t count_;
    int32_t hashCode_;
    uint16_t flags_;
    alignas(KChar) char data_[];
}

<math xmlns="http://www.w3.org/1998/Math/MathML"> x 1 是字符串 " c i n d y " ,编译器生成的全局常量, x1是字符串"cindy",编译器生成的全局常量, </math>x1是字符串"cindy",编译器生成的全局常量,x0是Helloworld对象吗?怎么来的?

可以看main函数中调用 init方法之前的一段代码

● 从数据段获取了Helloworld的类型的信息 kclass:Helloworld

● 调用 AllocInstance 分配内存

● 调用 UpdateStackRef 将分配好的内存的地址保存到栈上

类型信息

kclass:Helloworld 是一个地址,里面存的内容是什么?为什么创建对象需要它?

通过查看源码,对应的类型是 TypeInfo 的结构体,存储一个类的信息(类比与OC的类对象)

对应的定义如下(截图不完整):

● 这些信息本身都是在编译期的常量,编译器组织起来放在了数据段(不可变),运行时可以读取使用

● 很多跟OC类似的信息,父类型、实例大小、成员变量的便宜、遵守的协议等等

可以读取信息看下

有 3 个成员变量:objOffsetsCount_ = 3,便宜地址如下:

relativeName_ = Helloworld

看看ExtendedTypeInfo

三个变量的名字分别是:name helloable name

instanceSize_ = 0x20 占用32个字节

三个成员变量,三个指针,每个指针占用8个字节,这里为啥是32个字节?

对象

返回的对象使用ObjHeader来表示

所有的Kotlin对象,都有一个成员叫 typeInfoOrMeta_,指向自己的类对象 (类比OC中的isa指针)

小结

对象创建的流程大概如下:

● 编译器根据类的定义生成类信息(TypeInfo),并放在全局数据段(__DATA_CONST.__const)

● 代码中遇到类实例化,则将代码翻译为以下几步

● 从数据段取到类信息

● 调用 AllocInstance 分配内存,分配内存的大小 从 TypeInfo 中获取

● 并把前 8 个字节的内容填写为TypeInfo的地址

● 调用class的构造方法初始化成员变量

方法调用

初始化方法

会先调用父类的构造方法

父类设置name属性

子类设置name属性

greet方法

直接调用函数,并且把上面初始化的对象作为第一个参数传入

重载greetPrefix

查看函数,greetPrefix 有三个方法,除了父类与子类的实现个一个外,还有一个trampoline的辅助函数

打断点看到,第一个参数也是我们上面创建的对象实例

逻辑如下:

通过对象(读取前八个字节里的地址对应的内容)获取对应的类对象(TypeInfo)

类对象中有虚函数表,虚函数表里有函数地址,通过偏移读取到目标函数的地址,然后跳转到具体的函数

查看虚函数表

由于我们没有重载toString等方法的实现,所以虚函数表中对应的函数地址是父类的函数的地址。

与OC交互

重新将产物编译为 framework

scala 复制代码
kotlinc-native Hello.kt -g -Xno-inline -Xstatic-framework -p framework -o Hello

得到如下产物:

可以集成到 Xcode 中运行了

生成的头文件也都可以调用了

main函数

在二进制的符号表中搜索#main,发现多了一个包装函数

objc2kotlin_kfun:#main(){} 中会调用 kfun:#main(){}

objc2kotlin_kfun:#main(){} 函数不对应任何一句原代码,完全由编译器生成

objc2kotlin前缀表示 从objc调用kotlin的函数

kfun:#main(){} 的逻辑不变(创建对象,方法调用)

成员方法

查看二进制文件符号,发现每个成员方法也生成了对应的objc2kotlin前缀的函数,例如:

在KN编译代码中找到了"objc2kotlin"

编译器如何通过生成 objc2kotlin_ 前缀的方法?

objc2kotlin_xxx方法是在OC侧调用,所以其接受的参数以及返回值都是 OC 的对象

objc2kotlin_xxx 要"复用" xxx(KN kotlin 方法) 的逻辑

根据相关位置的代码以及编译结果看,大概的流程为以下三步:

  1. 转换 OC对象 -> Kotlin对象(struct ObjHeader)

  2. 调用kotlin函数,获得Kotlin对象的返回值

  3. 将返回值转回 OC 对象(还有其他分支,暂不关心)

比如:

由于是init方法,返回值就是传入的self对象,所以不需要第三部转换,可以通过下面这个函数来看下返回值的情况

OC Kotlin 交互

Kotlin_ObjCExport_refFromObjC

查看源码可知是调用objc_msgSend,target是obj,SEL 是toKotlin:

任意一个对象能响应这个方法吗?

可以响应,runtime给NSObject添加了分类方法

当然对于 OC 的原生类有具体的处理:

还有一个特殊的KotlinBase的类,是 所有从Kotlin生成的OC的类的 基类

比如:HelloHello,是从Kotlin的Hello类生成的,其基类就是 KotlinBase

这个类有一个特殊的成员变量 refHolder

在toKotlin:中,返回值就是从refHolder中获取的,所以需要看下这个refHolder什么时候赋值的,其内容是什么?

情况1

● 94:分配内存,创建 OC 的实例对象(Oojb)

● 96:获取到HelloHello类对应的Kotlin Hello类的TypeInfo

○ OC的HelloHello 是 编译器根据 Hello 生成的,编译器跟runtime有办法获取到,具体获取方法暂不介绍

● 109:通过 Hello类的TypeInfo,创建一个Hello类的实例对象(Kobj),并把 Oobj 绑定到 Kobj 上

○ 绑定:通过Kobj可以轻松的(成本很低的)获取到Oobj

● 112:把 Kobj 放在 Oobj 的 refHolder 成员变量里

串一下这段代码后面发生的事情:

● alloc 过程:有两个分配内存的过程(创建了两个对象),OC 侧跟Kotlin侧个创建了一个对象,并且把两个对象绑定起来了(互相持有引用),通过其中任意一个对象,可以轻松获取到对侧的对象

● init 过程:给上述Alloc获取的对象发消息,SEL='initWithName',对应的实现是objc2kotlin_kfun:Hello#<init>(kotlin.String){}函数,函数中self对象跟NSString 通过 toKotlin: 转换为 Kotlin 侧的对象,并作为调用 kfun:Hello#<init>(kotlin.String){}函数的参数

问题

上述过程好像只是把Kobj初始化了,Oobj初始化成功了吗?

Oobj有成员变量吗?

output:

ini 复制代码
ivar count = 0; property count = 0; method count = 8;

其实HelloHello这个类没有实例变量,其父类有一个成员变量:refHolder

可以认为OC 侧的对象是 Kotlin侧对象的一个包装,自己不保存任何数据,对OC 侧做的操作(函数调用)都完全转发给Kotlin侧来实现

● OC函数是对Kotlin函数的包装

● OC对象是对Kotlin对象的包装

情况2

Kotlin_ObjCExport_refToRetainedObjC

● 462:获取Kobj绑定的Oobj

○ 之前绑定过,获取成功直接返回

● 下面的逻辑:需要走新建对象并绑定

○ 1. 通过TypeInfo获取到对应的OC class

○ 2. 调用 createRetainedWrapper 方法

● 分配内存,创建 Oobj

● 双向绑定

下面的情况属于这种情况,先创建Kotlin对象,然后创建OC对象

情况3

不是从Kotlin class 生成的OC 的类的实例对象转Kotlin对象

● MyObject 从Kotlin 的class生成的

● 如果有一个类集成了 'HelloHello',这个类也不是从Kotlin class 映射过来的

转Kotlin对象走Kotlin_ObjCExport_convertUnmappedObjCObject

动态生成一个TypeInfo,然后单向绑定,只有从Kotlin对象可以找到OC对象

动态生成一个TypeInfo里会根据父类,遵守的协议设置虚函数表(这些虚标是编译器提前为每个类型准备好的),对应的实现函数是 以kotlin2objc为前缀的这些函数:

这些函数是跟上面介绍的objc2kotlin的作用相反

● kotlin2objc函数 在Kotlin侧调用(绑定在TypeInfo的虚表中);objc2kotlin函数在OC侧调用,绑定在OC类对应的SEL上

● kotlin2objc 将参数转为OC对象,然后给OC对象发消息,获取返回值转为Kotlin对象

● objc2kotlin 将参数转为Kotlin对象,然后调用Kotlin的函数,获取返回值转为OC对象

背后的过程是

● OC_37

○ 创建 MyObject 的实例对象 O1

○ 调用 h1 的 setHelloable 的方法 参数是 h1(self) SEL O1 函数对应的实现是 objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){}

objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){} 将 h1 跟 O1 转为 对应的 Kotlin 对象 Kh1 KO1

○ h1 -> Kh1 的转换读取refHolder即可,因为 h1 是通过 createRetainedWrapper 方法创建的,创建的时候已经绑定了kotlin对象

○ O1 -> KO1 的过程: 通过 Kotlin_ObjCExport_convertUnmappedObjCObject实现,过程中会动态创建 TypeInfo,通过 O1 遵守的HelloHelloAble协议(OC的runtime可以获取到) 找到Kotlin侧 HelloAble 协议对应的虚表,并设置到动态创建的TypeInfo上,然后通过创建的TypeInfo创建一个 KO1 对象,

○ 然后将 KO1 设置到 Kh1 对应的成员变量上

● OC_38

○ 调用 h1 的 triggleInterface 方法,方法的实现是 objc2kotlin_kfun:Hello#triggleInterface(){}

○ 获取 h1 对应的 Kh1

○ 调用 kotlin 侧的 triggleInterface 方法

● Kotlin_18

○ 读取 Kh1 的成员变量helloable,读取到的对象是 KO1

○ 调用 KO1 makeIt 方法 kfun:HelloAble#makeIt(){}kotlin.String-trampoline -trampoline 后缀的方法是需要通过 TypeInfo 读虚表(此TypeInfo是上面动态创建的,创建的时候设置好了虚表)来确定调用最终的函数为kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String

kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String 中会把 KO1 转为 O1 对象,调用 O1 对象的 makeIt 方法,通过msgSend 实现,获取到返回值通过toKotlin: 方法转为Kotlin侧的对象

总结

● KN 处理 Kotlin 的逻辑整体跟C++的方式比较类似

● refHolder、toKotlin:、objc2kotlin_xx、kotlin2objc_xx 实现了与OC的交互

附录

objc2kotlin_xx 与 kotlin2objc_xx 方法如何绑定

● 每个class 每个 interface 编译器都会未其生成 TypeInfo

● 如果需要跟OC交互,TypeInfo中有个变量

● objc2kotlin_xx 与 kotlin2objc_xx 就存在这些列表里,runtime在运行时会根据类型匹配,动态绑定这些函数入口

● 在OC类的initialize方法中,会给OC类添加(通过 OC 的 runtime) directAdapters,classAdapters,implementedInterfaces_

● 在动态创建TypeInfo 的过程中,则会使用到kotlinVtable,kotlinItable,reverseAdapters

相关推荐
黄毛火烧雪下5 小时前
创建一个ios小组件项目
ios
songgeb6 小时前
🧩 iOS DiffableDataSource 死锁问题记录
ios·swift
2501_929157689 小时前
「IOS苹果游戏」600个
游戏·ios
00后程序员张9 小时前
iOS 26 App 运行状况全面解析 多工具协同监控与调试实战指南
android·ios·小程序·https·uni-app·iphone·webview
白玉cfc10 小时前
【iOS】KVC 与 KVO 的基本了解与使用
macos·ios·objective-c·cocoa
2501_9160074710 小时前
iOS 混淆实战,多工具组合完成 IPA 混淆、加固与发布治理(iOS混淆|IPA加固|无源码混淆|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
2501_9159184110 小时前
怎么上架 App?iOS 应用上架完整流程详解与跨平台发布实战指南
android·ios·小程序·https·uni-app·iphone·webview
马拉萨的春天10 小时前
谈谈你对iOS的runtime和runloop的了解
macos·ios·cocoa
开开心心loky10 小时前
[iOS] 计算器仿写
ios