通过源码看 Swift 中的类、对象、属性

swift 类、对象、属性

Step 1 swift 源码编译

坑很多,编译时间长(顺利的话 3 小时以内,错一次重新等1小时,为了不影响工作建议专门准备一台电脑跑),编译后项目占20G 到 60G 不等,看你的编译方式,可以先按照我的步骤尝试,如果遇到问题可以参考我搜罗的各种博客包括官方指南,每个人遇到的问题都不一样,每个版本的坑也不同,非常奇特。

swift 5.7 release 为例

不是每个版本都能成功编译,也不代表别人能能编译的版本你就能编译,想快速成功尽量保证环境一致。 我的环境 swift-driver version: 1.62.15 Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) Target: arm64-apple-macosx13.0

  1. 安装 cmake、 ninja、 sccache
brew install cmake ninja sccache
  1. 拉取 swift 仓库代码

新建一个仓库文件夹 “swift-build”

cd swift-build
git clone --branch swift-5.7-RELEASE git@github.com:apple/swift.git
  1. 拉取依赖 (大概用了一两个小时,用梯子应该更快)

首先切到 swift 文件夹下,官方提供的脚本、源码都在这里。

cd swift
utils/update-checkout --tag swift-5.7-RELEASE --clone

以上步骤建议终端内全程挂梯子

  1. 编译(1 到 2 小时)
utils/build-script --skip-build-benchmarks \
  --skip-ios --skip-watchos --skip-tvos --swift-darwin-supported-archs "$(uname -m)" \
  --sccache --release-debuginfo --swift-disable-dead-stripping

要理解参数含义,可使用命令查看,或者参考下方提供的博客

utils/build-script --help
  1. 打开工程

编译完成后项目文件夹下会生成一个 build 文件夹。

打开

swift-build/build/Xcode-RelWithDebInfoAssert+stdlib-DebugAssert/swift-macosx-arm64

下的

Swift.xcodeproj

  1. 新建target

自定义一个名称,例如 SwiftBuild。

添加 Target Dependencies

  1. 修改scheme

8.run

20-30分钟左右。

我遇到的问题

发现是目录文件下缺少这两个库,然后在

swift-build/build/Ninja-RelWithDebInfoAssert/swift-macosx-arm64/lib

下找到了这两个库,拷过去就可以了

如果遇到其他问题可参考下面的网站,遇到问题很正常,多查一下资料,检查环境问题,快则3小时搞定,慢的花上一周的也有。

参考网站

How to Set Up an Edit-Build-Test-Debug Loop (推荐)

编译 Swift 5.8 源码(推荐)

Swift源码编译调试(M1/Xcode)

超实用~使用Xcode编译Swift源码

Swift底层探索之Swift源码编译

Swift源码编译-让底层更清晰

Step 2 对象的创建

写一个简单的类,通过断点方式查看对象的调用

step into

此处系统调用了 swift_allocObject 传入了三个参数,分别为

  • metadata
  • requiredSize
  • requiredAlignmentMask
using HeapMetadata = TargetHeapMetadata<InProcess>;

后两者不难理解,分别是对象需要的空间大小(此处为 40 字节) 以及字节对齐掩码(此处为7 即 1000)

而前者 metadata 不难猜测就是对象实例的结构体,查看HeapMetadata,发现它是 TargetHeapMetadata 的别名

深入看看

所有堆分配类型的元数据的共同结构。可以通过加载任何堆对象的’isa’字段来检索其中之一的指针,无论它是由Swift还是Objective-C管理的。但是,当从Objective-C对象加载时,此元数据可能没有堆元数据头,并且可能不是对象动态类型的Swift类型元数据。

TargetHeapMetadata 只有两个初始化方法,继续回溯它的父类 TargetMetadata

观察到 TargetMetadata 只有一个属性 Kind,通过 SetKind 方法可以了解到,它的类型是 MetadataKind,一个uint32_t,为了探究 Kind 的作用,搜索 MetadataKind,找到了一个关键函数

getTypeContextDescriptor()

这个函数根据 TargetMetadata 的 Kind 将自身分为了几种不同的类型,其中 Class 类型为 TargetClassMetadata

根据注释可以了解到 TargetClassMetadata 兼容 swift 与 OC 的类,在 swift 下 TargetClassMetadata 继承自 TargetAnyClassMetadata

至此我们找到了 Class 类型的真实结构

结合TargetAnyClassMetadata,概览一下TargetClassMetadata的成员

struct TargetClassMetadata {
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    
    var classFlags: Int32 // swift特有的标志
    var instanceAddressPoint: UInt32 // 实例独享内存首地址
    var instanceSize: UInt32 // 实例对象内存大小
    var instanceAlignmentMask: UInt16 // 实例对象内存对齐方式
    var reserved: UInt16 // 运行时保留字段
    var classSize: UInt32 // 类的内存大小
    var classAddressPoint: UInt32 //类的内存首地址
    var typeDescriptor: UnsafeMutablePointer<TargetClassDescriptor> // 类型描述器
    var iVarDestroyer: UnsafeRawPointer // 实例销毁器
}

用与上面相同的方法将TargetClassDescriptor的成员梳理出来

// 类描述器
struct TargetClassDescriptor{
    var flags: UInt32
    var parent: TargetRelativeDirectPointer<UnsafeRawPointer>
    
    var name: TargetRelativeDirectPointer<CChar>   // class/struct/enum 的名称
    var accessFunctionPointer: TargetRelativeDirectPointer<UnsafeRawPointer>
    var fieldDescriptor: TargetRelativeDirectPointer<FieldDescriptor> // 属性描述器
    
    var superClassType: TargetRelativeDirectPointer<CChar> // 父类类型
    var metadataNegativeSizeInWords: UInt32
    var metadataPositiveSizeInWords: UInt32
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32 // 每一个属性值距离当前实例对象地址的偏移量
    //var Offset: UInt32
    // var size: UInt32
    // V-Table  (methods)
    
    func getFieldOffsets(_ metadata: UnsafeRawPointer) -> UnsafePointer<Int> {
        return metadata.assumingMemoryBound(to: Int.self).advanced(by: numericCast(self.fieldOffsetVectorOffset))
    }
    
    // 泛型参数的偏移量
    var genericArgumentOffset: Int { return 2 }
}

继续梳理出 TargetRelativeDirectPointer 与 FieldDescriptor 的结构

// 相对地址信息 - 存储的是偏移量
struct TargetRelativeDirectPointer<Pointee> {
    var offset: Int32
    
    mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer<Pointee>{
        let offset = self.offset
        
        return withUnsafePointer(to: &self) { p in
            /*
             获得self,变为raw,然后+offset
             
             - UnsafeRawPointer(p) 表示this
             - advanced(by: numericCast(offset) 表示移动的步长,即offset
             - assumingMemoryBound(to: T.self) 表示假定类型是T,即自己指定的类型
             - UnsafeMutablePointer(mutating:) 表示返回的指针类型
            */
           return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
        }
    }
}

// 属性描述器
struct FieldDescriptor {
    var mangledTypeName: TargetRelativeDirectPointer<CChar>
    var superclass: TargetRelativeDirectPointer<CChar>
    var kind: UInt16
    var fieldRecordSize: UInt16
    var numFields: UInt32 // 属性个数
    var fields: FiledRecordBuffer<FieldRecord> // 属性列表
}

// 属性
struct FieldRecord {
    var flags: Int32
    var mangledTypeName: TargetRelativeDirectPointer<CChar> // 属性的类型
    var fieldName: TargetRelativeDirectPointer<UInt8> // 属性的名称
}

struct FiledRecordBuffer<Element>{
    var element: Element
    
    mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
        return withUnsafePointer(to: &self) {
            let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
                return start
            }
            return UnsafeBufferPointer(start: ptr, count: n)
        }
    }
    
    mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
        return withUnsafePointer(to: &self) {
            return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}

至此我们还原出了 TargetClassMetadata 的完整结构。

接下来将它们声明在常规工程中,我们就可以通过类型打印出它的属性名列表了

验证 TargetClassMetadata 的结构

class Person {
    var name = "steve"
    var age = 56
}

var pClazz = Person.self // 拿到类对象

// 拿到ClassMetadata指针
let classMetadata_ptr = unsafeBitCast(pClazz as Any.Type, to: UnsafeMutablePointer<TargetClassMetadata>.self)

// 属性个数
let numFileds = classMetadata_ptr.pointee.typeDescriptor.pointee.numFields

// 拿到首个属性偏移地址信息
let offsets = classMetadata_ptr.pointee.typeDescriptor.pointee.getFieldOffsets(UnsafeRawPointer(classMetadata_ptr).assumingMemoryBound(to: Int.self))

for i in 0..<numFileds {
    // 获取属性名
    let fieldName = classMetadata_ptr.pointee.typeDescriptor.pointee.fieldDescriptor.getmeasureRelativeOffset().pointee.fields.index(of: Int(i)).pointee.fieldName.getmeasureRelativeOffset()
    print("fieldName:\(String(cString: fieldName))")
    
    // 混写过的类型名称
    let mangledTypeName = classMetadata_ptr.pointee.typeDescriptor.pointee.fieldDescriptor.getmeasureRelativeOffset().pointee.fields.index(of: Int(i)).pointee.mangledTypeName.getmeasureRelativeOffset()
    print("mangledTypeName:\(String(cString: mangledTypeName))")

    print("==================")
}

如何获取属性的类型信息呢?

来到 MetadataLookup.cpp

这个类里提供了一系列 metadata 属性类型的反射方法,其中就包括接下来要用到的

使用@_silgen_name(“swift_getTypeByMangledNameInContext”) 函数映射

@_silgen_name("swift_getTypeByMangledNameInContext")
public func _getTypeByMangledNameInContext(
    _ name: UnsafePointer<CChar>,
    _ nameLength: Int,
    context: UnsafeRawPointer?,
    genericArguments: UnsafeRawPointer?
) -> Any.Type?

即可通过这个方法获取属性的类型

// 泛型参数
let genericVector = UnsafeRawPointer(classMetadata_ptr).advanced(by: classMetadata_ptr.pointee.typeDescriptor.pointee.genericArgumentOffset * MemoryLayout<UnsafeRawPointer>.size).assumingMemoryBound(to: Any.Type.self)

// 获取属性类型:
let fieldType = _getTypeByMangledNameInContext(mangledTypeName,
                                                   256,
                                                   context: UnsafeRawPointer(classMetadata_ptr.pointee.typeDescriptor),
                                                   genericArguments: genericVector)

print(fieldType as Any)

函数表

Swift中的函数调用

来到 GenMeta.cpp

发现 VTable 是拼接在 Metadata 之后。

Swift 属性

  • 存储属性
  • 计算属性
  • 延迟属性
  • 类属性
class Programer {
    var name: String = "steve"
    private var _weight = 0
    var weight: Int {
        get{return 50}
        set{_weight = newValue}
    }
    lazy var age = 56
    static var height = 180
}

通过 SIL 转译后变为

class Programer {
  @_hasStorage @_hasInitialValue var name: String { get set }
  @_hasStorage @_hasInitialValue private var _weight: Int { get set }
  var weight: Int { get set }
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @_hasStorage @_hasInitialValue static var height: Int { get set }
  @objc deinit
  init()
}
  • @_hasStorage 代表属性实际存储了值
  • @_hasInitialValue 代表属性有初始值
  • lazy 类型变为了一个 optional 的变量

继续阅读下面的 SIL 代码

// variable initialization expression of Programer.name
sil hidden [transparent] @$s14Properties_SIL9ProgramerC4nameSSvpfi : $@convention(thin) () -> @owned String {
bb0:
  %0 = string_literal utf8 "steve"                // user: %5
  %1 = integer_literal $Builtin.Word, 5           // user: %5
  %2 = integer_literal $Builtin.Int1, -1          // user: %5
  %3 = metatype $@thin String.Type                // user: %5
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %4 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %5
  %5 = apply %4(%0, %1, %2, %3) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %6
  return %5 : $String                             // id: %6
} // end sil function '$s14Properties_SIL9ProgramerC4nameSSvpfi'

此处为 name 的初始化过程,最终返回的%5寄存器内的值来自对%4的调用,通过注释了解到,它实际是调用了String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)方法,

三个参数分别为

  • string 的字面量值
  • utf8CodeUnitCount 字符串长度
  • isASCII 是否为 ASCII 编码

实际调用的时候还传递了一个 String 元类的参数,即 %3 寄存器存放的值。

继续观察 name 的 getter 与 setter 方法

// Programer.name.getter
sil hidden [transparent] @$s14Properties_SIL9ProgramerC4nameSSvg : $@convention(method) (@guaranteed Programer) -> @owned String {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Programer):
  debug_value %0 : $Programer, let, name "self", argno 1, implicit // id: %1
  %2 = ref_element_addr %0 : $Programer, #Programer.name // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*String // users: %4, %6
  %4 = load %3 : $*String                         // users: %7, %5
  retain_value %4 : $String                       // id: %5
  end_access %3 : $*String                        // id: %6
  return %4 : $String                             // id: %7
} // end sil function '$s14Properties_SIL9ProgramerC4nameSSvg'

// Programer.name.setter
sil hidden [transparent] @$s14Properties_SIL9ProgramerC4nameSSvs : $@convention(method) (@owned String, @guaranteed Programer) -> () {
// %0 "value"                                     // users: %11, %8, %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $String, %1 : $Programer):
  debug_value %0 : $String, let, name "value", argno 1, implicit // id: %2
  debug_value %1 : $Programer, let, name "self", argno 2, implicit // id: %3
  retain_value %0 : $String                       // id: %4
  %5 = ref_element_addr %1 : $Programer, #Programer.name // user: %6
  %6 = begin_access [modify] [dynamic] %5 : $*String // users: %8, %7, %10
  %7 = load %6 : $*String                         // user: %9
  store %0 to %6 : $*String                       // id: %8
  release_value %7 : $String                      // id: %9
  end_access %6 : $*String                        // id: %10
  release_value %0 : $String                      // id: %11
  %12 = tuple ()                                  // user: %13
  return %12 : $()                                // id: %13
} // end sil function '$s14Properties_SIL9ProgramerC4nameSSvs'

通过 SIL 不难理解他们的流程,

  • debug_value 代表将寄存器的值与后面定义的变量绑定
  • ref_element_addr 代表获取元素地址
  • begin_access/end_access 代表开始/结束访问
  • load 将值从内存中放入寄存器
  • store 将寄存器的值放入内存

注意setter方法最后返回了一个空元组,以下是 chatgpt 的解释,谨慎参考

SIL(Swift Intermediate Language)是Swift语言的一种中间表示形式,用于编译器优化和代码生成。在SIL中,所有函数都是以标准化的形式表示的,包括getter和setter方法。
在Swift语言中,setter方法通常用于修改对象的属性。在SIL中,setter方法的返回类型是一个元组类型,即空元组() -> ()。这是因为setter方法的主要目的是修改对应属性的值,并不需要返回任何值。
此外,返回一个空元组也有助于编译器进行优化,减少不必要的内存分配和拷贝操作。

再来看 lazy var 的流程

// Programer.age.getter
sil hidden [lazy_getter] [noinline] @$s14Properties_SIL9ProgramerC3ageSivg : $@convention(method) (@guaranteed Programer) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Programer):
  debug_value %0 : $Programer, let, name "self", argno 1, implicit // id: %1
  %2 = ref_element_addr %0 : $Programer, #Programer.$__lazy_storage_$_age // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  %4 = load %3 : $*Optional<Int>                  // user: %6
  end_access %3 : $*Optional<Int>                 // id: %5
  switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1", implicit // id: %8
  br bb3(%7 : $Int)                               // id: %9

bb2:                                              // Preds: bb0
  %10 = integer_literal $Builtin.Int64, 56        // user: %11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2", implicit // id: %12
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  %14 = ref_element_addr %0 : $Programer, #Programer.$__lazy_storage_$_age // user: %15
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  store %13 to %15 : $*Optional<Int>              // id: %16
  end_access %15 : $*Optional<Int>                // id: %17
  br bb3(%11 : $Int)                              // id: %18

// %19                                            // user: %20
bb3(%19 : $Int):                                  // Preds: bb2 bb1
  return %19 : $Int                               // id: %20
} // end sil function '$s14Properties_SIL9ProgramerC3ageSivg'

它的特殊之处在于取值时对寄存器内的值有一个判断,如果不为空则直接返回值,如果为空则进行初始化流程,注意此处的流程没有线程安全的保证,意味着lazy var的初始化不是线程安全的

最后看 static 属性

// one-time initialization function for height
sil private [global_init_once_fn] @$s14Properties_SIL9ProgramerC6height_WZ : $@convention(c) () -> () {
bb0:
  alloc_global @$s14Properties_SIL9ProgramerC6heightSivpZ // id: %0
  %1 = global_addr @$s14Properties_SIL9ProgramerC6heightSivpZ : $*Int // user: %4
  %2 = integer_literal $Builtin.Int64, 180        // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  store %3 to %1 : $*Int                          // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} // end sil function '$s14Properties_SIL9ProgramerC6height_WZ'

staic 属性使用了alloc_global,意味着它是在全局数据区进行初始化的,此外它自动生成并插入了一个函数 global_init_once_fn ,它能确保全局变量只被初始化一次,因此 static 是线程安全的。


种一棵树最好的时间是在十年前,而后是现在。

Loading Disqus comments...
Table of Contents