Swift进阶(二)--- 类,对象
Swift编译简介
我们先来开一段简单的代码:
class SuperMan {
var age:Int = 18
var name:String = "Aaron"
}
let t = SuperMan()
我们创建一个SuperMan
类,并通过默认的初始化器,创建一个实例对象并赋值给了t
。
那么问题来了:这个默认的初始化器到底做了一个什么样的操作?
这里我们引入SIL(Swift intermediate language)
,在阅读SIL
的代码之前,我们先来了解一下什么是SIL
首先不管是OC
还是Swift
,后端都是通过LLVM
进行编译的,如下图所示:
可以看到:
OC
通过clang
编译器,编译成IR
,然后再生成可执行文件.o
(这里也就是我们的机器码)Swift
则是通过Swift 编译器
编译成IR
,然后再生成可执行文件我们再来看一下,一个
swift
文件的编译过程都经历了哪些步骤:image.png
image.png
swift
在编译过程中使用的前端编译器是swiftc
,和我们之前在OC
中使用的Clang
是有所区别的。我们可以通过swiftc -h
命令,来查看swiftc
都能做什么事情
SIL
- 首先进入工程所在的文件夹,然后执行
swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil
指令。对该指令有疑问的同学,可以点击这里,链接文章的最后,有对该指令的详细讲解。下方代码是用VSCode
打开的main.sil
文件,
// t
sil_global hidden @main.t : main.SuperMan : $SuperMan
// main
// '@main': 标识当前main.swift的入口函数。SIL中的标识符名称以‘@’作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
//'%0、%1' 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果想继续使用,就需要不算的累加数字(注意:这里的寄存器指的是‘虚拟寄存器’,与汇编指令‘register read %x’读取的寄存器不同,汇编指令读取的是真实的寄存器)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
//‘alloc_global’ 创建一个‘全局变量’ --> ‘t
alloc_global @main.t : main.SuperMan // id: %2
//‘global_addr’ 获取全局变量的地址,并赋值给%3
%3 = global_addr @main.t : main.SuperMan : $*SuperMan // user: %7
//‘metatype’ 获取‘Superman’ 的 ‘MetaData’ 赋值给 %4
%4 = metatype $@thick SuperMan.Type // user: %6
// function_ref SuperMan.__allocating_init()
//将 ‘__allocating_init()’ 函数的地址赋值给 %5
%5 = function_ref @main.SuperMan.__allocating_init() -> main.SuperMan : $@convention(method) (@thick SuperMan.Type) -> @owned SuperMan // user: %6
//’apply‘ 调用 ’__allocating_init()‘ 并将返回值 赋值给 %6
%6 = apply %5(%4) : $@convention(method) (@thick SuperMan.Type) -> @owned SuperMan // user: %7
//’store‘ 将 %6 的值 存储到 %3,也就是前面提到的全局变量的地址
store %6 to %3 : $*SuperMan // id: %7
//创建一个整数文字值,类型为Builtin.Int32,该类型必须为内置整数类型。文字值是使用Swift的整数文字语法指定的,值为0,使用者%8
%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'
- 下面我们结合汇编进行一下断点调试。查看一下
SuperMan
的创建流程,如下图打断点
image.png
在Dubge
中选择Always Show Disassembly
image0.png
接下来我们会在汇编代码中发现一个SuperMan.__allocating_init()
image1.png
我们使用si
指令跟进去会发现该方法的内部会调用SuperMan.init()
函数,并且会先调用swift_allocObject
函数。
image2.png
源码调试
根据上面的分析,接下来我们通过swift_allocObject
来探索swift中对象的创建过程,通过搜索swift_allocObject
我们找到了该函数的实现
-
接下来我们来进行断点调试
image4.png - 可以看到左侧local有详细的信息
- 其中
requiredSize
是分配的实际内存大小,为40。其中metadata:8, refCounts:8, age:8, name:16。 -
requiredAligmentMask
是swift中的字节对其方式,这个和OC中的是一样的,必须是8
的倍数,不足的会自动补齐,目的是以空间换时间
,来提高内存的操作效率。
注意:这里的requiredSize
和requiredAligmentMask
都是上层函数传进来的
swift_allocObject 源码分析
swift_allocObject
的源码如下,主要有一下几部分组成
- 通过
swift_slowAlloc
分配内存,并进行内存字节对齐。 - 通过
new + HeapObject + metadata
初始化一个实例对象 - 函数的返回值是
HeapObject
类型,所以当前对象的内存结构
就是HeapObject
的内存结构
image5.png - 进入
swift_slowAlloc
函数,其内部主要是通过malloc
在堆
中分配size大小的内存空间
,并返回内存地址
,主要是用于存储实例变量
image6.png - 进入
HeapObject
初始化方法,可以看到两个参数:metadata、refCounts
image7.png - 其中
metadata
类型是HeapMetadata
,是一个指针类型,占8
个字节 -
refCounts
(引用计数,类型是InlineRefCounts
,而InlineRefCounts
是一个RefCounts
的别名,占8
个字节)
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
image8.png
总结
- 对于实例对象
t
来说,其本质是一个HeapObject
结构体,默认16
字节内存大小(metadata
8字节 +refCounts
8字节),与OC对比如下:- OC中实例对象的本质是
结构体
,是以objc_object
为模板继承的,其中有一个isa
指针,占8
字节。 - Swift中实例对象,默认的比OC中多一个
refCounts
引用计数,默认属性占16
字节。
- OC中实例对象的本质是
- Swift中对象的分配流程是:
__allocating_init
--->swift_allocObject
--->_swift_allocObject_
--->swift_slowAlloc
-->malloc
。 - init在其中的职责就是初始化变量,这点与OC中的是一致的。
看到这里,有的同学可能会有一些疑问❓:为什么 age
和 name
分别是 8字节
和 16字节
?
接下来我们详细讲解一下:
对于Int
、String
类型,在Swift中,两个都是结构体类型。我可以通过内存打印来验证一下
//********* Int底层定义 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}
//********* String底层定义 *********
@frozen public struct String {...}
//********* 验证 *********
print("Int类型所占内存大小:\(MemoryLayout<Int>.stride)")
print("String类型所占内存大小:\(MemoryLayout<String>.stride)")
print("t.age所占内存大小:\(MemoryLayout.size(ofValue: t.age))")
print("t.name所占内存大小:\(MemoryLayout.size(ofValue: t.name))")
//********* 打印结果 *********
Int类型所占内存大小:8
String类型所占内存大小:16
t.age所占内存大小:8
t.name所占内存大小:16
通过上面的打印结果,可以清楚的看到Int
和 String
所占内存大小。
但是到这里,可能同学问了,那如果
String
的值非常大的话,内存里面分配的16个自己又该怎么存储呢?
swift的内存分配的问题,之后会单独写一篇文章来详细讲一下,这里就不做详细的赘述,这里记住Int
占8个字节,String
占16个字节就可以了。其它的类型,可自行打印出来看一下。
下面我们来看另一个问题。我们上面提到了metadata
,那它到底是什么呢?我们继续往下分析
Swift中 类结构的探索
在OC中类是从objc_class
模板继承过来的。
在Swift中,类的结构在底层是HeapObject
,包含metadata
和 refCounts
。
而metadata
的类型是HeapMetadata
HeapMetadata类型分析
- 通过Swift源码,跟进去我们会发现
using HeapMetadata = TargetHeapMetadata<InProcess>
- 我们接着跟进
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
定义后,我们发现其本质是一个模板类型
。这个结构体中没有属性,只有初始化
方法,传入一个MetadataKind
类型的参数,这里kind
就是传入的Inprocess
-
接下来,我们继续跟进到
TargetMetadata
中,有一个kind
的属性,kind
的类型就是前面传入的Inprocess
。然后我们再跟进Inprocess
,最总得到结果是:对于kind
,其类型的本质是unsigned long
//******** TargetMetaData 定义 ********
struct TargetMetadata {
using StoredPointer = typename Runtime::StoredPointer
...
private:
/// The kind. Only valid for non-class metadata; getKind() must be used to get
/// the kind value.
StoredPointer Kind
}
//******** Inprocess 定义 ********
struct InProcess {
static constexpr size_t PointerSize = sizeof(uintptr_t);
using StoredPointer = uintptr_t;
...
}
//******** uintptr_t 定义 ********
typedef unsigned long uintptr_t;
- 从
TargetHeapMetadata
、TargetMetadata
定义中,我可以看出kind
的类型是MetadataKind
(此处是强制转化,与上文并不冲突) - 跟进
MetadataKind
,可以看到一个#include "MetadataKind.def"
,再次跟进,会看到所有类型的元数据
image9.png
总结之后得到如下表格:
name | Value |
---|---|
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 |
LastEnumerated | 0x7FF |
- 回到
TargetMetadata
结构体定义中,找到方法getClassObject
, 在该方法中去匹配kind
,返回值TargetClassMetadata
类型 - 如果是
Class
,则直接对this
(当前指针,即metadata) 强转为ClassMetadata
/// Get the class object for this type if it has one, or return null if the
/// type is not a class (or not a class with a class object).
const TargetClassMetadata<Runtime> *getClassObject() const
//************* 方法的实现 **************
template<> inline const ClassMetadata *
Metadata::getClassObject() const {
//匹配kind
switch (getKind()) {
//如果kind是class
case MetadataKind::Class: {
// Native Swift class metadata is also the class object.
// 将当前指针强转为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;
}
//************** ClassMetadata ****************
using ClassMetadata = TargetClassMetadata<InProcess>;
所以,TargetMetadata
和 TargetClassMetadata
本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以我们可以认为:结构体
其实就是TargetClassMetadata
- 进入
TargetClassMetadata
定义,发现其继承自TargetAnyClassMetadata
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
...
/// Swift-specific class flags.
///Swift 特有的标识
ClassFlags Flags;
/// The address point of instances of this type.
///此类型的实例对象的地址
uint32_t InstanceAddressPoint;
/// The required size of instances of this type.
/// 'InstanceAddressPoint' bytes go before the address point;
/// 'InstanceSize - InstanceAddressPoint' bytes go after it.
/// 实例对象内存大小
uint32_t InstanceSize;
/// The alignment mask of the address point of instances of this type.
/// 实例对象内存对齐
uint16_t InstanceAlignMask;
/// Reserved for runtime use.
/// 运行时保留字段
uint16_t Reserved;
/// The total size of the class object, including prefix and suffix
/// extents.
/// 类的内存大小
uint32_t ClassSize;
/// The offset of the address point within the class object.
/// 类的内存首地址
uint32_t ClassAddressPoint;
...
}
- 进入
TargetAnyClassMetadata
,又会发现,其继承自TargetHeapMetadata
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
using StoredPointer = typename Runtime::StoredPointer;
using StoredSize = typename Runtime::StoredSize
...
}
总结
综上所述,当metadata
的kind
为Class时,有如下继承关系:
- 当前类返回的实际类型是
TargetClassMetadata
;从上面继承链可以看出TargetMetadata
只有一个属性kind
,TargetAnyClassMetaData
中有4个属性:kind
、superClass
、cacheData
、data
。
- 当前
Class
在内存中存放的属性,是由TargetClassMetadata
+TargetAnyClassMetadata
+TargetMetadata
的属性加起来构成的,因此得出metadata
的数据结构体如下:
struct swift_class_t:NSObject {
void *kind;//相当于OC中的isa,kind的实际类型是unsigned long
void *superClass;
void *cacheData;
void *data;
uint32_t flages; //4字节
uint32_t instanceAddressOffset; //4字节
uint32_t instanceSize; //4字节
uint16_t instanceAlignMask; //2字节
uint16_t reserved; //2字节
uint32_t classSize; //4字节
uint32_t classAddressOffset; //4字节
void *description
...
}
与OC对比
- 实例对象 & 类
- OC中的
实例对象
本质是结构体
,是通过底层的objc_object
模板创建的,类是继承自objc_class
- Swift中的
实例对象
本质也是结构体
,类型是HeapObject
,比OC多了一个refCounts
- OC中的
- 方法列表
- OC中的方法储存在
objc_class
结构体class_rw_t
的methodList
中 - Swift中的方法储存在
metadata
元数据中
- OC中的方法储存在
- 引用计数
- OC中的ARC维护的是
散列表
- Swift中的ARC是对象内部有一个
refCounts
属性
- OC中的ARC维护的是