第一节课:类、对象、属性
准备工具:编译过的Swift源码、Vscode、Xcode、终端
主要内容:
Swift编译简介(了解)
SIL分析(掌握)
类结构探索
Swift属性
Swift编译简介
创建一个项目,写一个类,并通过默认的初始化,创建一个实例对象赋值给t。
class HZMTeacher{
var age: Int = 18
var name: String = "HZM"
}
let t = HZMTeacher()
接下来主要看默认的初始化器到底做了什么操作
我们使用SIL(Swift intermediate language)来查看分析。
首先了解下什么是SIL
iOS开发语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:
可以看到:
OC
通过clang
编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们的机械码)
Swift
则是通过Swift编译器
编译成IR,然后再生成可执行文件。
我们再来看一下,一个swift
文件的编译过程都经历了什么步骤:
swift在编译过程中使用的前端编译器是swiftc
,和我们在OC
中使用的Clang
是有区别的
我们可以通过如下命令查看swiftc
都能做什么样的事情:
swiftc -h
以上只是作为了解,扩充一下自己知识点,了解下Swift底层的原理,其实第一次看起来一脸懵,后面再结合分析代码多看看了解下就好
SIL分析(要求掌握)
首先我们简单写一段代码
然后通过终端执行 swiftc -dump-ast main.swift
查看抽象语法树
这里我们简单把之前项目通过命令生成SIL文件并打开:swiftc -emit-sil main.swift >> ./main.sil && open main.sil
- 在SIL文件中搜索
s4main10HZMTeacherCACycfC
,其内部实现主要是分配内存
+初始化变量
-
allocing_ref
实际创建一个HZMTeacher的实例对象,当前实例对象的引用计数为1 - 调用
init
方法
// HZMTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main10HZMTeacherCACycfC : $@convention(method) (@thick HZMTeacher.Type) -> @owned HZMTeacher {
// %0 "$metatype"
bb0(%0 : $@thick HZMTeacher.Type):
// 堆上分配内存空间
%1 = alloc_ref $HZMTeacher // user: %3
// function_ref HZMTeacher.init() 初始化当前变量
%2 = function_ref @$s4main10HZMTeacherCACycfc : $@convention(method) (@owned HZMTeacher) -> @owned HZMTeacher // user: %3
%3 = apply %2(%1) : $@convention(method) (@owned HZMTeacher) -> @owned HZMTeacher // user: %4
// 返回
return %3 : $HZMTeacher // id: %4
} // end sil function '$s4main10HZMTeacherCACycfC'
SIL语言对于Swift源码的分析是非常重要的,关于其更多的语法信息,可以在这个网站进行查询
swift_allocObject 源码分析
swift_allocObject的源码如下,主要有以下几部分:
- 通过swift_slowAlloc分配内存,并进行内存字节对齐
- 通过new + HeapObject + metadata初始化一个实例对象
- 函数的返回值是
HeapObject
类型,所以当前对象的内存结构
就是 -
HeapObject
的内存结构
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_slowAlloc
函数,其内部主要是通过malloc
在堆
中分配size大小
的内存空间,并返回内存地址
,主要是用于存储实例变量
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);// 堆中创建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;
}
这里我们简单过了一下Swift内存分配过程中发生的事情:
- _allocating_init --> swift_allocObject --> _ swift_allocObject --> swift_slowAlloc --> Malloc
- Swift对象的内存结构
HrepObject
,有两个属性:一个是Metadata
,一个是Refcount
,默认占用16
字节大小,就是对象中没有任何东西也是16
字节。OC中实例对象的本质是结构体,是以objc_object为模板继承的,其中有一个isa指针,占8
字节。Swift比OC中多了一个refCounted
引用计数大小,也就是多了8字节。 - init在这里扮演了初始化变量的职责,这和我们OC中的认知是一样的。
类结构探索
看了上面的内存分配之后,我们应该注意到了一个Metadata
,它的类型是HeapMetadata
,我们来看下它的具体内存结构是什么?
进入HeapMetadata
定义,是TargetHeapMetaData
类型的别名,接收了一个参数Inprocess
using HeapMetadata = TargetHeapMetaData<Inprocess>;
进入TargetHeapMetaData
定义,其本质是一个模板类型,其中定义了一些所需的数据结构。这个结构体中没有属性,只有初始化方法,传入了一个MetadataKind
类型的参数(该结构体没有,那么只有在父类中了)这里的kind就是传入的Inprocess
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
};
进入TargetMetaData
定义,有一个kind
属性,kind的类型就是之前传入的Inprocess
。从这里可以得出,对于kind
,其类型就是unsigned long
,主要用于区分是哪种类型的元数据
struct TargetMetaData{
using StoredPointer = typename Runtime: StoredPointer;
...
StoredPointer kind;
}
//******** Inprocess 定义 ********
struct Inprocess{
...
using StoredPointer = uintptr_t;
...
}
//******** uintptr_t 定义 ********
typedef unsigned long uintptr_t;
从TargetHeapMetadata
、TargetMetaData
定义中,均可以看出初始化方法中参数kind的类型是MetadataKind
进入MetadataKind
定义,里面有一个#include "MetadataKind.def",点击进入,其中记录了所有类型的元数据,所以kind种类总结如下:
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
//******** 具体实现 ********
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;
}
}
这一点,我们可以通过lldb来验证
po metadata->getKind()
,得到其kind是Class
po metadata->getClassObject()
、x/8g 0x0000000110efdc70,这个地址中存储的是元数据信息!
所以,TargetMetadata
和 TargetClassMetadata
本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以可以说,我们认为的结构体,其实就是TargetClassMetadata
进入TargetClassMetadata
定义,继承自TargetAnyClassMetadata
,有以下这些属性,这也是类结构的部分
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
...
//swift特有的标志
ClassFlags Flags;
//实力对象内存大小
uint32_t InstanceSize;
//实例对象内存对齐方式
uint16_t InstanceAlignMask;
//运行时保留字段
uint16_t Reserved;
//类的内存大小
uint32_t ClassSize;
//类的内存首地址
uint32_t ClassAddressPoint;
...
}
进入TargetAnyClassMetadata
定义,继承自TargetHeapMetadata
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
...
ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
TargetPointer<Runtime, void> CacheData[2];
StoredSize Data;
...
}
总结:
当前类返回的实际类型是 TargetClassMetadata
,而TargetMetaData
中只有一个属性kind
,TargetAnyClassMetaData
中有3个属性,分别是kind
, superclass
,cacheData
当前Class在内存中所存放的属性
由 TargetClassMetadata
属性 + TargetAnyClassMetaData
属性 + TargetMetaData
属性 构成,所以得出的metadata的数据结构体如下所示
void *kind;//相当于OC中的isa,kind的实际类型是unsigned long
void *superClass;
void *cacheData;
void *data;
uint32_t flags; //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;
...
}
Swift属性
- 存储属性:
常量: let 修饰
变量: var 修饰
let age: Int = 18
var name: String = "HZM"
}
let t = HZMTeacher()
对于上面的age、name来说,都是我们的变量存储属性,这点我们在SIL文件中可以看出来
特征:会占用实例对象分配的内存空间
- 计算属性:不占用空间,本质是get、set方法
验证方式:使用print(class_getInstanceSize(HZMTeacher.self))
来打印输出 -
属性观察者:
willSet:新值存储之前调用oldValue
didSet:新值存储之后调用newValue
*注意:在init()方法中调用修改属性不触发属性观察者
注:子类与父类的调用顺序如上图:子父父子(听到一个挺恰当的比喻,儿子从父亲那继承了遗产,但是想变卖的时候还是需要跟父亲说一声,父亲操作一番之后再告诉儿子进行变卖)
-
延迟存储属性:
- 使用lazy修饰的存储属性
class HZMTeacher{ lazy var age: Int = 10 }
-
延迟属性必须有一个默认的初始值
-
延迟存储在第一次访问的时候才被赋值
从图中我们可以看出,第一个断点的时候第一次访问时值为0 第二个断点的时候代表访问过后,进行了赋值
- 延迟存储属性并不能保证线程安全
我们也可以通过sil
文件来查看,这里可以在生成sil
文件时,加上还原swift中混淆名称的命令(即xcrun swift-demangl
e):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
通过分析SIL文
件我们可以看到,最终走分支的时候是需要判断值的,假定有两个线程进入,第一个因为值为空走了bb2
,然后第二个线程进来的时候还没有赋值,判断还是走了bb2
流程。
所以,在此时,线程1会走一遍赋值,线程2也会走一遍赋值,并不能保证属性只初始化了一次
-
延迟存储属性对实例对象大小的影响
从打印类的内存大小可以看出,使用懒加载后内存变大(空间换时间)
- 类型属性:
类型属性属于这个类的本身,不管有多少个实例,类型属性只有一份,我们使用static来修饰一个类型属性
- 使用
static
修饰,且是全局变量 - 类型属性必须有一个
默认初始值
- 类型属性只会被
初始化一次