iOS-Swift类与结构体(二)

这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战

本篇文章来分析Swift 类底层的源码结构

Swift代码编译流程:

iOS 开发的语言不管是 OC 还是 Swift 后端都是通过LLVM进行编译的,如下图所示:

截屏2021-12-27 下午2.25.01.png

  • OC 通过 clang 编译器编译成 IR, 然后再生成可执行文件.o
  • Swift 则是通过 Swift 编译器编译成 IR,然后再生成可执行文件 .o

截屏2021-12-27 下午2.25.13.png

  • 先是 Swift Code 通过 -dump-parse 进行语义分析解析成 Parse(抽象语法树)
  • Parse 经过 -dump-ast 进行语义分析语法是否正确
  • Sema 之后会把 Swift Code 降级变成 SILGen(Swift 中间代码),对于 SILGen 又分为未优化(-emit-silgen)和经过优化(-emit-sil)
  • 优化完的 SILGen 会有 LLVM 降级 成为 IR,然后 IR 再由后端代码编译成机器码

编译流程的命令:

// 分析输出AST
swiftc main.swift -dump-parse

// 分析并且检查类型输出AST
swiftc main.swift -dump-ast

// 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen

// 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil

// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir

// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc

// 生成汇编
swiftc main.swift -emit-assembly

// 编译生成可执行.out文件
swiftc -o main.o main.swift
复制代码

查看编译称SIL后的代码

class Person {
var age = 20
var name = "Candy"
}

let person = Person()
复制代码

进入到当前代码所在的位置,输入 swiftc main.swift -emit-sil 然后回车

截屏2022-01-25 下午2.22.22.png

关于SIL的语法说明,可以参考官方文档:github.com/apple/swift…

查看汇编代码

通过下面这样,打上代码断点后可以查看汇编代码

截屏2022-01-25 下午2.31.40.png

接下来打开汇编调试

截屏2022-01-25 下午2.30.12.png

截屏2022-01-25 下午2.29.25.png

通过汇编查看,Person 在进行初始化的时候,在底层会调用 __allocating_init 的函数,那么 __allocating_init 做了什么事情呢,跟进去看一下。在圈住的地方打一个断点,让断点走到 __allocating_init 这一行代码,按住 control 键,点击这个向下的按钮。

截屏2022-01-25 下午2.50.55.png

其实进入这个页面也看不出来什么

截屏2022-01-25 下午2.50.32.png

这个时候我们来看一下源码。源码可以去苹果官网下-swift源码下载地址。用 VSCode 打开下载好的 swift 源码,全局搜索 swift_allocObject 这个函数。

在 VSCode 打开 Swift 源码,如果想像 Xcode 一样,点击进行实现跳转,需要安装插件 C/C++ Extension Pack

HeapObject.cpp 文件中找到 swift_allocObject 函数的实现,并且在 swift_allocObject 函数的实现上方,有一个 _swift_allocObject_ 函数的实现。

截屏2022-01-25 下午2.57.25.png

在函数的内部会调用一个 swift_slowAlloc 函数,我们来看下 swift_slowAlloc 函数的内部实现:

截屏2022-01-25 下午3.02.39.png

swift_slowAlloc 函数的内部是去进行一些分配内存的操作,比如 malloc。所以就印证了引用类型->对象申请堆空间的过程。

OC 与 Swift 的区分调用

在调用 _swift_allocObject_ 函数的时候有一个参数,名为 metadata 的 HeapMetadata。以下是 HeapMetadata 跟进的代码过程:

截屏2022-01-25 下午3.06.38.png

截屏2022-01-25 下午3.09.14.png

在这里有对 OC 和 Swift 做兼容。调用的 TargetHeapMetadata 函数的时候,如果是 Swift 类,那么就是MetadataKind 类型参数, 如果是 OC 的类,那么参数为 isa 指针。MetadataKind 是一个 uint32_t 的类型。

截屏2022-01-25 下午3.11.44.png

那么 MetadataKind 的种类如下:

name                       Value

Class                      0x0
Struct                     0x200
Enum                       0x201
Optional                   0x202
ForeignClass               0x203
ForeignClass               0x203
Opaque                     0x300
Tuple                      0x301
Function                   0x302
Existential                0x303
Metatype                   0x304
ObjCClassWrapper           0x305
ExistentialMetatype        0x306
HeapLocalVariable          0x400
HeapGenericLocalVariable   0x500
ErrorObject                0x501
LastEnumerated             0x7FF
复制代码

Swift 类底层的源码结构

接下来我们找到 TargetHeapMetadata 的继承 TargetMetadata(在 C++ 中结构体是允许继承的)。在 TargetMetadata 结构体中找到了 getTypeContextDescriptor 函数,代码如下:

截屏2022-01-25 下午3.18.38.png

可以看到,当 Metadatakind 是一个 Class 的时候,会拿到一个名为 TargetClassMetadata 的指针,来看看 TargetClassMetadata 的实现:

截屏2022-01-25 下午3.19.54.png

我们看到 TargetClassMetadata 继承于 TargetAnyClassMetadata,在这里看到了superclassisa

截屏2022-01-25 下午3.22.53.png

通过以上的分析,我们可以得出,Swift 类中的 metadata 数据结构大致如下:

struct Metadata {
   var kind: Int
   var superClass: Any.Type
   var cacheData: (Int, Int)
   var data: Int
   var classFlags: Int32
   var instanceAddressPoint: UInt32
   var instanceSize: UInt32
   var instanceAlignmentMask: UInt16
   var reserved: UInt16
   var classSize: UInt32
   var classAddressPoint: UInt32
   var typeDescriptor: UnsafeMutableRawPointer
   var iVarDestroyer: UnsafeRawPointer
}
复制代码

接下来我们做一个测试,通过 lldb 查看 Swift 类的内存结构,那么既然在 Swift 的底层,_swift_allocObject_ 函数返回的是 HeapObject 的指针类型,我们来看一下 HeapObject 的结构:

截屏2022-01-25 下午3.25.45.png

知道了 HeapObject 的源码结构之后,我们也假里假气的模仿源码,自己定义一个 HeapObjectrefCounts 可以先忽略,不管,主要看 metadata,通过上面看到的 Metadata 也还原出对应的源码。 我们将 Person 类转成 HeapObject 结构体,来打印HeapObject和Metadata的内存结构。

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refCounts: UInt32
}

struct Metadata{
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer
    var iVarDestroyer: UnsafeRawPointer
}

class Person {
  var age = 20
  var name = "Candy"
}

let person = Person()

// 将 person 转成 HeapObject 指针
let p_raw_ptr = Unmanaged.passRetained(person as AnyObject).toOpaque()

let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)
// 将 p_ptr 指针转成 HeapObject 的指针类型并打印出 HeapObject 的内存结构
print(p_ptr.pointee)

//将HeapObject中的metadata绑定成Metadata类型,并转成Metadata指针类型,数据大小可以使用MemoryLayout进行测量
let matadata = p_ptr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee

print(matadata)
复制代码

截屏2022-01-25 下午3.35.12.png

通过打印验证了我们还原出来的底层结构是没有问题的。

这里补一个小知识点:

可以通过以下方法获取对象的指针

withUnsafeBytes(of: &person) { (ptr) -> Void in

 }

withUnsafePointer(to: &person) { (ptr) -> Void in
 }
复制代码

假如通过这种方法传递对象的指针,就会在将指针转换成class对象的时候遇到问题。所以,这里需要用到Unmanaged。

Unmanaged是一个结构体,可以用来获取一个对象的指针,也可以将一个指针转换成一个对象:

// 获取指针
let ptr = UnsafeMutableRawPointer(Unmanaged<A>.passUnretained(person).toOpaque())
//获取对象
let obj = Unmanaged<A>.fromOpaque(ptr).takeUnretainedValue()
复制代码

总结

所以就可以得到下面的这些结论

Swift 对象内存分配的流程:

__allocating_init -> swift_allocObject -> _swift_allocObject_ ->
swift_slowAlloc -> Malloc
复制代码

Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是 Metadata ,一个是 RefCount ,默认占用 16 字节大小。 而对于 OC 对象,只存在一个 isa,默认占有8字节的大小。

猜你喜欢

转载自juejin.im/post/7062517715633700894