要想深入的学习Swift
,就得从最基本的类,对象,属性开始深入分析,本文将使用SIL
文件以及Swift
源码,来对类、对象和属性的底层结构进行探究。
编译流程
-
在了解
SIL
之前,先来了解iOS
的编译过程:iOS
分OC
和Swift
两个语言,但他们后端都是通过LLVM
编译的,如下图:
OC
是通过Clang
编译器编译成IR
,然后再生成可执行文件.o
Swift
是通过swiftc
编译器编译生成IR
,然后再生成可执行文件.o
Swift编译流程
-
根据在 Swift Compiler 中对
swift编译器
主要组件的描述,可以得到下图:- 主要分为以下几个过程:
-
Parsing(解析)
:解析器是一个简单的递归下降解析器(在 lib/Parse 中实现),带有一个集成的、手工编码的词法分析器。解析器负责生成没有任何语义或类型信息的抽象语法树(AST)
,并针对输入源的语法问题发出警告或错误
-
Semantic analysis(语义分析)
:语义分析(在 lib/Sema 中实现)负责获取解析后的AST
并将其转换为格式良好、经过完全类型检查的AST
形式,针对源代码中的语义问题发出警告或错误。语义分析包括类型推断,如果成功,则表明从生成的、类型检查的AST
生成代码是安全的
-
Clang importer(Clang导入器)
:Clang
导入器(在 lib/ClangImporter 中实现)导入Clang
并将它们导出的C
或Objective-C API
映射到相应的Swift API
。生成的导入AST
可以通过语义分析进行引用
-
SIL generation(SIL生成)
:Swift
中间语言(SIL)
是一种高级的、特定于Swift
的中间语言,适用于进一步分析和优化Swift
代码。SIL
生成阶段(在 lib/SILGen 中实现)将经过类型检查的AST
降低为所谓的“原始”SIL
。SIL
的设计在docs/SIL.rst 中进行了描述
-
SIL guaranteed transformations(SIL保证转换)
:SIL
保证转换(在 lib/SILOptimizer/Mandatory 中实现)执行影响程序正确性的附加数据流诊断(例如使用未初始化的变量
)。这些转换的最终结果是“规范的”SIL
。
-
SIL Optimizations(SIL优化)
:SIL
优化(在 lib/Analysis、lib/ARC、lib/LoopTransforms和 lib/Transforms 中实现)对程序执行额外的高级、特定于Swift
的优化,包括(例如)自动引用计数优化,去虚拟化和泛型专业化
-
LLVM IR Generation(LLVM IR 生成)
:IR
生成(在 lib/IRGen 中实现)将SIL
降低到LLVM IR,此时LLVM
可以继续优化它并生成机器代码。
-
- 主要分为以下几个过程:
其中SIL
是Swift
编译过程中的中间代码
,位于在AST
和LLVM IR
之间
生成SIL
- 下面将
swift
代码生成SIL
文件:// main.swift class WSPerson { var age: Int = 18 var name: String = "wushuang" } var ws = WSPerson()
swift
在编译过程中使⽤的前端编译器是swiftc
,可以使用命令swiftc -emit-sil main.swift >> ./main.sil
将swift
文件生成SIL
文件:
初步分析SIL文件
-
类和对象在
SIL
文件中的形式:class WSPerson { // 存储属性 age @_hasStorage @_hasInitialValue var age: Int { get set } // 存储属性 name @_hasStorage @_hasInitialValue var name: String { get set } // @objc标记的deinit方法 @objc deinit // 构造方法init init() } // 存储属性ws @_hasStorage @_hasInitialValue var ws: WSPerson { get set } // ws // s4main2wsAA8WSPersonCvp是混淆之后的,main.ws sil_global hidden @$s4main2wsAA8WSPersonCvp : $WSPerson
-
其中
s4main2wsAA8WSPersonCvp
是混淆后的变量,可以使用命令xcrun swift-demangle s4main2wsAA8WSPersonCvp
进行还原,结果如下:$s4main2wsAA8WSPersonCvp ---> main.ws : main.WSPerson
-
-
main
函数:// @main 入口函数 // @convention(c)代表c函数 // main函数有两个参数Int32类型和 UnsafeMutablePointer指针类型,并且返回值是Int32类型 sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { // `%0、%1` 是SIL中的寄存器,赋值后就不会改变可以理解为常量,是虚拟的寄存器,与register read 中的寄存器不同 bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): // 1. 创建一个 main.WSPerson类型的全局变量并存入 %2 alloc_global @$s4main2wsAA8WSPersonCvp // id: %2 // 2. 获取 全局变量的地址,并将地址赋值给 %3 %3 = global_addr @$s4main2wsAA8WSPersonCvp : $*WSPerson // user: %7 // 3. 获取WSPerson元数据类型,并赋值给%4 %4 = metatype $@thick WSPerson.Type // user: %6 // 4. 将WSPerson.__allocating_init()函数赋值给 %5 // function_ref WSPerson.__allocating_init() %5 = function_ref @$s4main8WSPersonCACycfC : $@convention(method) (@thick WSPerson.Type) -> @owned WSPerson // user: %6 // 5. apply方法调用函数%5 __allocating_init,并将结果赋值给 %6 %6 = apply %5(%4) : $@convention(method) (@thick WSPerson.Type) -> @owned WSPerson // user: %7 // 6. 将%6 存储到 地址%3, store %6 to %3 : $*WSPerson // id: %7 // 创建整形变量 0 并返回 %8 = integer_literal $Builtin.Int32, 0 // user: %9 %9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10 return %9 : $Int32 // id: %10 } // end sil function 'main'
@main
函数是main.swift
文件的入口函数,%0、%1
是SIL
中的寄存器,赋值后就不会改变可以理解为常量,是虚拟的寄存器,与看汇编时用到的register read
中的寄存器不同main
函数中主要是做了些创建对象的一些工作,主要有以下几个步骤-
- 创建一个全局变量
ws
并赋值%2
- 创建一个全局变量
-
- 获取全局变量
ws
的地址,并赋值给%3
- 获取全局变量
-
- 获取
WSPerson
元数据类型,并赋值给%4
- 获取
-
- 将
WSPerson.__allocating_init()
函数赋值给%5
- 将
-
- 根据元数据类型调用
__allocating_init
函数创建对象,并将结果赋值给%6
- 根据元数据类型调用
-
- 将创建的对象
%6
存入全局变量%3
地址,也就是对全局变量ws
进行赋值
- 将创建的对象
-
return
结束main
函数
-
创建对象
- 在上面分析中,我们知道创建对象的核心是调用了
__allocating_init
函数,它的代码如下:
源码分析
// WSPerson.__allocating_init()
sil hidden [exact_self_class] @$s4main8WSPersonCACycfC : $@convention(method) (@thick WSPerson.Type) -> @owned WSPerson {
// %0 "$metatype"
bb0(%0 : $@thick WSPerson.Type):
// 1. 在堆上创建WSPerson型的对象,并赋值给%1
%1 = alloc_ref $WSPerson // user: %3
// 2. 获取WSPerson.init()函数并赋值给 %2
// function_ref WSPerson.init()
%2 = function_ref @$s4main8WSPersonCACycfc : $@convention(method) (@owned WSPerson) -> @owned WSPerson // user: %3
// 3. 对象调用init方法并返回当前对象
%3 = apply %2(%1) : $@convention(method) (@owned WSPerson) -> @owned WSPerson // user: %4
return %3 : $WSPerson // id: %4
} // end sil function '$s4main8WSPersonCACycfC'
- 创建对象的核心过程主要有以下三步:
-
- 在堆上创建
WSPerson
型的对象,并赋值给%1
- 在堆上创建
-
- 获取
WSPerson.init()
函数并赋值给%2
- 获取
-
- 对象
%1
调用init
方法初始化并返回当前对象
- 对象
-
汇编分析
-
在
xcode
代码中添加符号断点__allocating_init
,然后分析汇编: -
__allocating_init
在底层实质上是调用了swift_allocObject
函数,此时需要去 Swift源码(需要进行源码编译)
查看:static HeapObject *_swift_allocObject_(HeapMetadata const *metadata, size_t requiredSize, size_t requiredAlignmentMask) { assert(isAlignmentMask(requiredAlignmentMask)); auto object = reinterpret_cast<HeapObject *>( // 计算内存大小 swift_slowAlloc(requiredSize, requiredAlignmentMask)); // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer // check on the placement new allocator which we have observed on Windows, // Linux, and macOS. // 根据元数据类型创建实例对象 new (object) HeapObject(metadata); // If leak tracking is enabled, start tracking this object. SWIFT_LEAKS_START_TRACKING_OBJECT(object); SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject); return object; }
-
_swift_allocObject_
函数的主要进行两个步骤:-
- 调用
swift_slowAlloc
函数计算内存大小
- 调用
-
- 根据内存和元数据类型在
堆区
创建对象
- 根据内存和元数据类型在
swift_slowAlloc
计算内存的源码如下:
// Linux malloc is 16-byte aligned on 64-bit, and 8-byte aligned on 32-bit. # if defined(__LP64) # define MALLOC_ALIGN_MASK 15 # else # define MALLOC_ALIGN_MASK 7 # endif void *swift::swift_slowAlloc(size_t size, size_t alignMask) { void *p; // This check also forces "default" alignment to use AlignedAlloc. if (alignMask <= MALLOC_ALIGN_MASK) { #if defined(__APPLE__) p = malloc_zone_malloc(DEFAULT_ZONE(), size); #else p = malloc(size); #endif } else { size_t alignment = (alignMask == ~(size_t(0))) ? _swift_MinAllocationAlignment : alignMask + 1; p = AlignedAlloc(size, alignment); } if (!p) swift::crash("Could not allocate memory."); return p; }
alignMask
是内存对齐的mask
,在64位
是16字节
对齐,而32位
是8字节
对齐,关于对齐的方式可以参考 iOS底层-内存对齐- 至于
MALLOC_ALIGN_MASK
为什么是对齐位数减去1
,是因为计算对齐时,要&
上~MALLOC_ALIGN_MASK
,例如7
,~7
就是8
的倍数。
-
-
HeapObject
方法的源码如下:struct HeapObject { /// This is always a valid pointer to a metadata object. // metadata 是指针指向 HeapMetadata HeapMetadata const *__ptrauth_objc_isa_pointer metadata; SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; #ifndef __swift__ HeapObject() = default; // Initialize a HeapObject header as appropriate for a newly-allocated object. // 构造方法 constexpr HeapObject(HeapMetadata const *newMetadata) : metadata(newMetadata) , refCounts(InlineRefCounts::Initialized) { } // Initialize a HeapObject header for an immortal object constexpr HeapObject(HeapMetadata const *newMetadata, InlineRefCounts::Immortal_t immortal) : metadata(newMetadata) , refCounts(InlineRefCounts::Immortal) { } #endif // __swift__ };
- 在上面
HeapObject
构造函数中有两个参数metadata
是HeapMetadata
类型的指针,占8字节
。- 通过查找
refCounts
得到它是InlineRefCounts
类型,占用8字节
- 此时可以得出结论,
swift对象
本质为HeapObject
,占用16字节
。
- 在上面
内存分配
-
通过上面分析,我们知道对象的内存,那么类
WSPerson
的内存是多少呢,下面使用class_getInstanceSize
来打印下类的内存:- 结果占用的
40字节
,这是为什么?再使用MemoryLayout<类型>.stride
去分别打印Int
和String
所占用内存:
-
打印得出
Int
占用8字节
,String
占用16字节
,通过源码搜索发现Int
和String
都是结构体类型:// InterTypes.swift @frozen public struct Int : FixedWidthInteger, SignedInteger, _ExpressibleByBuiltinIntegerLiteral {...} // String.swift @frozen public struct String {...}
-
即
40 = metadata(8字节)+ refCount(8字节)+ Int(8字节)+ String(16字节)
,但metadata
还不知道是什么,下面将对它进行研究
- 结果占用的
总结
-
- 对象内存分配流程:
__allocating_init
->swift_allocObject_
->swift_slowAlloc
->malloc
- 对象内存分配流程:
-
Swift
中实例对象
占用16字节
,比OC
中多了refCounted(引用计数大小)
类
-
通过上面的分析我们知道
metadata
是HeapMetadata
类型的指针,去源码查看:template <typename Target> struct TargetHeapMetadata; using HeapMetadata = TargetHeapMetadata<InProcess>;
HeapMetadata
是TargetHeapMetadata
模版函数的别名
-
在查看
TargetHeapMetadata
的代码:template <typename Runtime> struct TargetHeapMetadata : TargetMetadata<Runtime> { using HeaderType = TargetHeapMetadataHeader<Runtime>; TargetHeapMetadata() = default; // 初始化方法 constexpr TargetHeapMetadata(MetadataKind kind) : TargetMetadata<Runtime>(kind) {} #if SWIFT_OBJC_INTEROP constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa) : TargetMetadata<Runtime>(isa) {} #endif };
- 代码中
TargetHeapMetadata
继承TargetMetadata
,方法核心是是根据参数kind(传入的InProcess)
调用TargetMetadata
的构造方法。下面继续查看代码
- 代码中
-
TargetMetadata
的代码中主要是对kind
的一些操作,kind
类型如下:struct TargetMetadata { ... private: StoredPointer Kind; ... }
- 它实际就是传入
InProcess
结构体中的uintptr_t
,继续查看类型得知为unsigned long
类型:
struct InProcess { ... using StoredPointer = uintptr_t; ... } typedef unsigned long uintptr_t;
- 所以
kind
是unsigned long
类型,它主要是区分当前是那种类型的数据
- 它实际就是传入
-
进入
TargetMetadata
函数中的MetadataKind
函数,然后在点击#include "MetadataKind.def"
中的MetadataKind
,可以进入MetadataKind.def
文件夹,可以看到很多类型,类型对应的kind
值如下:class
:0x0
Struct
:0x200
Enum
:0x201
Optional
:0x202
ForeignClass
:0x203
Opaque
:0x300
Tuple
:0x301
Function
:0x302
Existential
:0x303
Metatype
:0x304
ObjCClassWrapper
:0x305
ExistentialMetatype
:0x306
HeapLocalVariable
:0x400
HeapGenericLocalVariable
:0x500
ErrorObject
:0x501
Task
:0x502
Job
:0x503
LastEnumerated
:0x7FF
类的结构分析
-
在
TargetMetadata
中有获取class
的方法const TargetClassMetadata<Runtime> *getClassObject() const;
- 其中的
getClassObject
函数是根据kind
获取object
类型:
template<> inline const ClassMetadata * Metadata::getClassObject() const { switch (getKind()) { case MetadataKind::Class: { // Native Swift class metadata is also the class object. // 如果是class,则强转成 ClassMetadata类型 return static_cast<const ClassMetadata *>(this); } case MetadataKind::ObjCClassWrapper: { // Objective-C class objects are referenced by their Swift metadata wrapper. auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this); return wrapper->Class; } // Other kinds of types don't have class objects. default: return nullptr; } }
-
如果
kind
是Class
类型,则将当前的metadata
强转成ClassMetadata
,而ClassMetadata
是TargetClassMetadata
根据类型的别名using ClassMetadata = TargetClassMetadata<InProcess>;
- 其中的
-
继续跟进
TargetClassMetadata
查看-
在函数中我们得知
TargetClassMetadata
继承TargetAnyClassMetadata
,并且有自己的构造方法,以及相关的字段,它的结构和 objc4-818.2 源码中的swift_class_t
结构体是一样的struct swift_class_t : objc_class { uint32_t flags; uint32_t instanceAddressOffset; uint32_t instanceSize; uint16_t instanceAlignMask; uint16_t reserved; uint32_t classSize; uint32_t classAddressOffset; void *description; // ... void *baseAddress() { return (void *)((uint8_t *)this - classAddressOffset); } };
-
-
再继续查看
TargetAnyClassMetadata
中的代码:- 此处的结构与
OC
中的objc_class
的结构一样,有isa
,有父类,有cacheData
,Data
类似于objc_class
中的bits
- 此处的结构与
-
根据上面分析我们可以得到结论:当
metadata
的kind
为Class
时,有如下的继承关系:
属性
在swift
中有四种属性,分别是:存储属性
、计算属性
、延迟属性
和类型属性
,下面对他们进行详细的讲解
存储属性
-
存储属性又分为两种: 常量存储属性(
let
修饰) 和 变量存储属性(var
修饰),具体代码如下:class WSPerson { let age: Int = 18 // 常量存储属性 var name: String = "wushuang" // 变量存储属性 }
- 常量存储属性的值不可修改,而变量存储属性的值可修改,在
SIL
文件中可以看的更直白:
class WSPerson { @_hasStorage @_hasInitialValue final let age: Int { get } @_hasStorage @_hasInitialValue var name: String { get set } @objc deinit init() }
- 在
Sil
文件中常量存储属性只能使用getter
方法,变量存储属性可以setter
和getter
方法
- 常量存储属性的值不可修改,而变量存储属性的值可修改,在
计算属性
-
计算属性不占用内存,它本身不存储值,通过
getter
间接访问值,如下两个案例class WSName { var familyName: String = "Bryant" var givenName: String = "Kobe" var fullName: String { get { return givenName + familyName } } } class Square { var width: Double = 20.0 var area: Double { get { return pow(width, 2) } set { width = sqrt(newValue) } } }
-
查看
SIL
文件:- 可以看到计算属性没有计算属性所有的
_hasStorage @_hasInitialValue
标识符
- 可以看到计算属性没有计算属性所有的
-
-
再打印这两个类的占用内存:
- 结果
WSName
占用的内存为48字节
,familyName
占用16字节
,givenName
占用16字节
,metadata
占用8字节
,refCount
占用8字节
,加起来刚好48字节
,fullName
没有占用内存。 Square
中的width
占用8字节
,metadata
占用8字节
,refCount
占用8字节
,加起来刚好24字节
,area
没有占用内存- 所以得出结论:计算属性不占用内存
- 结果
属性观察者(willSet/didSet)
-
属性观察者可以理解为
OC
中的KVO
,在属性调用setter
方法时:-
- 新值存储前会调用
willSet
方法,可以获取新值newValue
,
- 新值存储前会调用
-
- 新值存储后会调用
didSet
方法,可以获取旧值oldValue
- 新值存储后会调用
- 如下面案例所示:
- 在
SIL
文件查看:
- 在
name.setter
函数中主要进行以下几个操作:-
- 获取name旧值并存入寄存器
%6
- 获取name旧值并存入寄存器
-
- 调用
name.willSet
函数并传入新值%0
- 调用
-
- 将新值存入
name
,并释放旧值
- 将新值存入
-
- 调用
name.didSet
函数并传入旧值%6
- 调用
-
-
-
问题1:init方法中修改属性的值,是否会触发观察者?
- 在案例添加
init
再运行
- 结果并没有触发
willSet
和didSet
方法,在SIL
文件中分析得知,init
时修改属性的值只是修改属性初始化时的值,由于此时对象创建的过程还没有完成,所以并不会触发setter
方法,进而不会触发属性观察者
- 在案例添加
-
问题2:哪里可以添加属性观察
- 可以在以下三个地方添加观察者:
-
- 类中定义的存储属性
-
- 通过类继承的存储属性
-
- 通过类继承的计算属性
-
- 可以在以下三个地方添加观察者:
-
问题3:子类和父类的计算属性同时存在
didSet、willSet
时,其调用顺序是什么?- 使用以下案例打印:
- 通过案例得知:子类父类的计算属性同时存在
didSet、willSet
时,当属性值改变父类和子类两个方法的调用顺序如下:- 先
willSet
:先子类,后父类 - 后
didSet
:先父类,后子类
- 先
-
问题4:子类调用了父类的init方法,并且在里面修改属性值是否会触发观察者?
- 案例代码运行如下:
- 结果可以触发观察者,因为子类调用了父类的
init
,已经初始化
了,而初始化
流程保证
了所有属性都有值
,所以可以触发观察属性
了。
延迟属性
-
- 延迟存储属性的初始值在其第⼀次使⽤时才能访问,使用关键字
lazy
来标识一个延迟属性,延迟属性必须要设置一个初始值
,代码如下:
- 延迟存储属性的初始值在其第⼀次使⽤时才能访问,使用关键字
-
- 下面在
Sil
中分析可选类型:
class WSPerson { lazy var age: Int { get set } @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set } @objc deinit init() }
- 在
Sil
中延迟属性是可选类型,在继续分析getter
方法
- 分析得之在访问
getter
时,才会对age
赋值
- 下面在
-
下面来看下延迟属性的内存情况:
- 结果发现使用延迟属性后会导致内存增大
类型属性
-
类型属性属于这个类的本身,不管有多少个实例,类型属性
只有⼀份
,我们可以使⽤static
来声明⼀个类型属性:class WSPerson { static let name: String = "wushuang" } // 访问 let name = WSPerson.name
- 查看
Sil
文件观察它的变化:
- 使用
static
修饰变量后,在Sil
文件中会生成一个全局变量,而name
是线程不安全的 - 在
name
的global_init
中可以看到,name
只初始化一次:
- 可以在代码中打断点,然后查看汇编,再
进入(step into)
函数WSPerson.name.unsafeMutableAddressor
中可以看到它调用了swift_once
swift_once
在源码中最后调用的dispatch_once_f
,也就是单例
- 查看
-
我们可以使用
static
来创建单例class WSPerson { var name: String = "wushuang" var age: Int = 18 static let share = WSPerson() private init() {} } // 调用方法 let ws = WSPerson.share
总结
-
- 存储属性:有常量存储属性和变量存储属性两种,他们都占用内存
-
- 计算属性:不占用内存
-
- 属性观察者:
-
- 属性观察可以添加在
类的存储属性
、继承的存储属性
、继承的计算属性
中
- 属性观察可以添加在
-
- 父类在调用
init
中改变属性值不会触发
属性观察,子类调用父类的init
会触发
属性观察
- 父类在调用
-
- 统一属性在父类和子类都添加观察,在触发观察时:
willSet
方法,先子类后父类didSet
方法,先父类后子类
-
- 延迟属性(lazy):延迟属性必须有初始值,只有在
访问后
内存中才有值,延迟属性对内存有影响,
- 延迟属性(lazy):延迟属性必须有初始值,只有在
-
- 类型属性:类型属性必须有初始值,内存只分配一次,是线程安全的,可以用于单例
写在最后
由于下载的Swift-source 5.5.1
在xcode13.1
中没有编译成功,所以导致很多的调试都不方便,如果有不正确的地方欢迎指正🙏