本文主要对以下几点进行介绍:
- 通过SIL来理解对象的创建
- Swift类结构分析
- 存储属性 & 计算属性
- 延迟存储属性 & 单例创建方式
SIL
在底层流程中,OC代码和Swift代码是通过不同的编译器进行编译,然后通过LLVM,生成.o可执行文件,流程如下图所示
Swift与OC编译流程图
-
OC通过clang编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们所说的机器码) -
Swift则是通过Swift编译器,编译成IR,然后再生成可执行文件.o
下面是Swift中的编译流程,其中SIL(Swift Intermediate Language)是Swift编译过程中的中间代码,主要用于进一步分析和优化Swift代码。如下图所示,SIL位于在AST和LLVM IR之间
Swift编译流程
注意:Swift与OC的
区别在于Swift生成了高级的SIL
我们可以通过swiftc -h终端命令,查看swiftc的所有命令
swiftc命令
如果想要详细对SIL的内容进行探索,可以参考这个视频,也可以参考这个文档
例如:在main.swift文件定义如下代码
class SunriseTeacher {
var age: Int = 18
var name: String = "Sunrise"
}
let sunrise = SunriseTeacher()
- 查看抽象语法树:
swiftc -dump-ast main.swift
抽象语法树
- 生成SIL文件:
swiftc -emit-sil main.swift >> ./main.sil && code main.sil,其中main的入口函数如下:
// main
//`@main`:标识当前main.swift的`入口函数`,SIL中的标识符名称以`@`作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
//`%0、%1` 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果还想继续使用,就需要不断的累加数字(注意:这里的寄存器,与`register read`中的寄存器是有所区别的,这里是指`虚拟寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
//`alloc_global`:创建一个`全局变量`,即代码中的`sunrise`
alloc_global @$s4main7sunriseAA14SunriseTeacherCvp // id: %2
//`global_addr`:获取全局变量地址,并赋值给寄存器%3
%3 = global_addr @$s4main7sunriseAA14SunriseTeacherCvp : $*SunriseTeacher // user: %7
//`metatype`获取`SunriseTeacher`的`MetaData`赋值给%4
%4 = metatype $@thick SunriseTeacher.Type // user: %6
//将`__allocating_init`的函数地址赋值给 %5
// function_ref SunriseTeacher.__allocating_init()
%5 = function_ref @$s4main14SunriseTeacherCACycfC : $@convention(method) (@thick SunriseTeacher.Type) -> @owned SunriseTeacher // user: %6
//`apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
%6 = apply %5(%4) : $@convention(method) (@thick SunriseTeacher.Type) -> @owned SunriseTeacher // user: %7
//将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
store %6 to %3 : $*SunriseTeacher // id: %7
//构建`Int`,并`return
%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'
注意:
code命令生效是在.zshrc文件中添加如下配置:
alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'
- SIL文件中,可以看出,代码是经过混淆的,可以通过以下命令还原,以
s4main7sunriseAA14SunriseTeacherCvp为例:xcrun swift-demangle s4main7sunriseAA14SunriseTeacherCvp
混淆代码还原
- 在SIL文件中搜索
s4main14SunriseTeacherCACycfC,其内部实现主要是分配内存+初始化变量-
alloc_ref:创建一个SunriseTeacher的实例对象,当前实例对象的引用计数为1 - 调用
init方法
-
// s4main14SunriseTeacherCACycfC 实际就是__allocating_init()
// SunriseTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main14SunriseTeacherCACycfC : $@convention(method) (@thick SunriseTeacher.Type) -> @owned SunriseTeacher {
// %0 "$metatype"
bb0(%0 : $@thick SunriseTeacher.Type):
// 堆上分配内存空间
%1 = alloc_ref $SunriseTeacher // user: %3
// function_ref SunriseTeacher.init() 初始化当前变量
%2 = function_ref @$s4main14SunriseTeacherCACycfc : $@convention(method) (@owned SunriseTeacher) -> @owned SunriseTeacher // user: %3
// 返回
%3 = apply %2(%1) : $@convention(method) (@owned SunriseTeacher) -> @owned SunriseTeacher // user: %4
return %3 : $SunriseTeacher // id: %4
} // end sil function '$s4main14SunriseTeacherCACycfC'
符号断点调试
- 在demo中添加
__allocating_init符号断点
_ _allocating_init
- 发现其内部调用的是
swift_allocObject
swift_allocObject
源码调试
下面我们就通过swift_allocObject来探索Swift中对象的创建过程
- 在
REPL(命令交互行,类似于Python,可以在这里编写代码)中编写如下代码(也可以拷贝),并搜索swift_allocObject函数加一个断点,然后定义一个实例对象sunrise
创建实例对象
- 断点断住,查看左边Local有详细的信息
实例化断点断住
- 其中
requiredSize是分配的实际内存大小,为40 -
requiredAlignmentMask是Swift中的字节对齐,这个和OC中是一样的,必须是8的倍数,不足的会自动补齐,目的是以空间换时间,来提高内存操作效率
swift_allocObject 源码分析
swift_allocObject的源码如下,主要有以下几部分:
- 通过
swift_slowAlloc分配内存,并进行内存字节对齐 - 通过
new + HeapObject + metadata初始化一个实例对象 - 函数的返回值是
HeapObject类型,所以当前对象的内存结构就是HeapObject的内存结构
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_slowAlloc函数,其内部主要是通过malloc在堆中分配size大小的内存空间,并返回内存地址,主要是用于存储实例变量
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); // 堆中创建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;
}
- 进入
HeapObject初始化方法,需要两个参数:metadata、refCounts
HeapObject初始化方法
- 其中
metadata类型是HeapMetadata,是一个指针类型,占8个字节 -
refCounts(引用计数,类型是InlineRefCounts,而InlineRefCounts是一个类RefCounts的别名,占8个字节),Swift采用ARC 引用计数
RefCounts
总结
- 对于实例对象
sunrise来说,其实质是一个HeapObject结构体,默认16字节内存大小(metadata8字节 +refCounts8字节),与OC的对比如下:- OC中的实例对象的本质是
结构体,是以objc_object为模板继承的,其中有一个isa指针,占8个字节 - Swift中的实例对象,比OC中的多了一个
refCounts引用计数大小,默认属性占内存16个字节
- OC中的实例对象的本质是
- Swift中对象的内存分配流程是:
__allocating_init --> swift_allocObject_ --> _swift_allocObject --> swift_slowAlloc --> malloc - init在其中的职责就是初始化变量,这点与OC中是一致的
针对上面分析,我们还遗留了两个问题:metadata是什么,40是怎么计算的?下面来继续探索
在demo中,我们可以通过Runtime方法获取类的内存大小
类对象内存
这点与在源码调试时左边Local的requiredSize值是相等的,从HeapObject的分析中我们知道了,一个类在没有任何属性的情况下,默认占用16字节大小
对于Int、String类型,进入其底层定义,两个都是结构体类型,那么是否都是8字节呢?可以通过打印其内存大小来验证
//********* Int底层定义 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}
//********* String底层定义 *********
@frozen public struct String {...}
//********* 验证 *********
print(MemoryLayout<Int>.stride)
print(MemoryLayout<String>.stride)
//********* 打印结果 *********
8
16
从打印的结果中可以看出,Int类型占8字节,String类型占16字节(后面文章会进行详细讲解),这点与OC中是有所区别的
所以这也解释了为什么SunriseTeacher的内存大小等于40,即 40 = metadata(8字节) + refCount(8字节)+ Int(8字节)+ String(16字节)
这里验证了40的来源,但是metadata是什么还不知道,继续往下分析
HeapMetadata类型分析
下面就来分析metadata,看看它到底是什么?
- 进入
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,主要用于区分是哪种类型的元数据
//******** TargetMetaData 定义 ********
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
- 如果是
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;
}
}
这一点,我们可以通过lldb来验证
-
po metadata->getKind(),得到其kind是Class -
po metadata->getClassObject()、x/8g 0x0000000110efdc70,这个地址中存储的是元数据信息
metadata数据信息
所以,TargetMetadata 和 TargetClassMetadata 本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以可以说,我们认为的结构体,其实就是TargetClassMetadata
- 进入
TargetClassMetadata定义,继承自TargetAnyClassMetadata,有以下这些属性,这也是类结构的部分
template <typename Runtime>
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;
...
}
总结
综上所述,当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 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;
...
}
与OC对比
- 实例对象 & 类
- OC中的
实例对象本质是结构体,是通过底层的objc_object模板创建,类是继承自objc_class - Swift中的
实例对象本质也是结构体,类型是HeapObject,比OC多了一个refCounts
- OC中的
- 方法列表
-
OC中的方法存储在objc_class结构体class_rw_t的methodList中 -
Swift中的方法存储在metadata元数据中
-
- 引用计数
-
OC中的ARC维护的是散列表 -
Swift中的ARC是对象内部有一个refCounts属性
Swift属性
在swift中,属性主要分为以下几种
- 存储属性
- 计算属性
- 延迟存储属性
- 类型属性
存储属性
存储属性,又分两种:
- 要么是
常量存储属性,即let修饰 - 要么是
变量存储属性,即var修饰
定义如下代码
class SunriseTeacher {
var age: Int = 18
var name: String = "Sunrise"
}
let sunrise = SunriseTeacher()
其中代码中的age、name来说,都是变量存储属性,这一点可以在SIL中体现
class SunriseTeacher {
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
存储属性特征:会占用分配实例对象的内存空间
下面我们同断点调试来验证
po sunrise-
x/8g 内存地址,即HeapObject存储的地址
属性内存分布
HeapObject
计算属性
计算属性:是指不占用内存空间,本质是set/get方法的属性
我们通过一个demo来说明,以下写法正确吗?
class SunriseTeacher {
var age: Int{
get{
return 18
}
set{
age = newValue
}
}
}
在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set
计算属性-1
然后运行发现崩溃了,原因是age的set方法中调用age.set导致了循环引用,即递归
计算属性-2
验证:不占内存
对于其不占用内存空间这一特征,我们可以通过以下案例来验证,打印以下类的内存大小
class Square{
var width: Double = 8.0
var area: Double{
get{
//这里的return可以省略,编译器会自动推导
return width * width
}
set{
width = sqrt(newValue)
}
}
}
print(class_getInstanceSize(Square.self))
//********* 打印结果 *********
24
从结果可以看出类Square的内存大小是24,等于 (metadata + refCounts)类自带16字节 + width(8字节) = 24,是没有加上area的。从这里可以证明area属性没有占有内存空间。
验证:本质是set/get方法
- 将main.swift转换为SIL文件:
swiftc -emit-sil main.swift >> ./main.sil - 查看SIL文件,对于
存储属性,有_hasStorage的标识符
class Square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get set }
@objc deinit
init()
}
- 对于计算属性,
SIL中只有setter、getter方法
计算属性-3
属性观察者(didSet、willSet)
-
willSet:新值存储之前调用newValue -
didSet:新值存储之后调用oldValue
验证
- 可以通过demo来验证
class SunriseTeacher {
var name: String = "旧值-wrs"{
// 新值存储之前调用
willSet{
print("willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("didSet oldValue \(oldValue)")
}
}
}
let sunrise = SunriseTeacher()
sunrise.name = "新值-sun"
//**********打印结果*********
willSet newValue 新值-sun
didSet oldValue 旧值-wrs
- 也可以通过编译来验证,将main.swift编译成mail.sil,在sil文件中找
name的set方法
属性观察者-1
问题1:init方法中是否会触发属性观察者?
以下代码中,init方法中设置name,是否会触发属性观察者?
class SunriseTeacher {
var name: String = "旧值-wrs"{
// 新值存储之前调用
willSet{
print("willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("didSet oldValue \(oldValue)")
}
}
init() {
self.name = "sunrise"
}
}
运行结果发现,并没有走willSet、didSet中的打印方法,所以有以下结论:
- 在
init方法中,如果调用属性,是不会触发属性观察者的 - init中主要是
初始化当前变量,除了默认的前16个字节,其他属性会调用memset清理内存空间(因为有可能是脏数据,即被别人用过),然后才会赋值
总结:初始化器(即
init方法设置)和定义时设置默认值(即在didSet中调用其他属性值)都不会触发
问题2:哪里可以添加属性观察者?
主要有以下三个地方可以添加:
- 1、类中定义的存储属性
- 2、通过类继承的存储属性
class SunriseMediumTeacher: SunriseTeacher{
override var age: Int{
// 新值存储之前调用
willSet{
print("willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("didSet oldValue \(oldValue)")
}
}
}
- 3、通过类
继承的计算属性
class SunriseTeacher {
var age: Int = 18
var age2: Int{
get{
18
}
set{
self.age = newValue
}
}
}
let sunrise = SunriseTeacher()
class SunriseMediumTeacher: SunriseTeacher{
override var age: Int{
// 新值存储之前调用
willSet{
print("willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("didSet oldValue \(oldValue)")
}
}
override var age2: Int{
// 新值存储之前调用
willSet{
print("willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("didSet oldValue \(oldValue)")
}
}
}
问题3:子类和父类的计算属性同时存在didset、willset时,其调用顺序是什么?
有以下代码,其调用顺序是什么?
class SunriseTeacher {
var age: Int = 18{
// 新值存储之前调用
willSet{
print("父类 willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("父类 didSet oldValue \(oldValue)")
}
}
var age2: Int{
get{
18
}
set{
self.age = newValue
}
}
}
class SunriseMediumTeacher: SunriseTeacher{
override var age: Int{
// 新值存储之前调用
willSet{
print("子类 willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("子类 didSet oldValue \(oldValue)")
}
}
override var age2: Int{
// 新值存储之前调用
willSet{
print("子类 willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("子类 didSet oldValue \(oldValue)")
}
}
}
let sunrise = SunriseMediumTeacher()
sunrise.age = 20
//**********打印结果*********
子类 willSet newValue 20
父类 willSet newValue 20
父类 didSet oldValue 18
子类 didSet oldValue 18
总结:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,在父类didset, 子类的didset,即:
子父 父子
问题4:子类调用了父类的init,是否会触发观察属性?
在问题3的基础,修改SunriseMediumTeacher类
class SunriseMediumTeacher: SunriseTeacher{
override var age: Int{
// 新值存储之前调用
willSet{
print("子类 willSet newValue \(newValue)")
}
// 新值存储之后调用
didSet{
print("子类 didSet oldValue \(oldValue)")
}
}
override init() {
super.init()
self.age = 19
}
}
//**********打印结果*********
子类 willSet newValue 19
父类 willSet newValue 19
父类 didSet oldValue 18
子类 didSet oldValue 18
从打印结果发现,会触发属性观察者,主要是因为子类调用了父类的init,已经初始化过了,而初始化流程保证了所有属性都有值(即super.init确保变量初始化完成了),所以可以观察属性了
延迟属性
延迟属性主要有以下几点说明:
- 1、使用
lazy修饰的存储属性 - 2、延迟属性必须有一个
默认的初始值 - 3、延迟存储在第一次访问的时候才被赋值
- 4、延迟存储属性并不能保证线程安全
- 5、延迟存储属性对实例对象大小的影响
1、使用lazy修饰的存储属性
class SunriseTeacher{
lazy var age: Int = 18
}
2、延迟属性必须有一个默认的初始值
如果定义为可选类型,则会报错,如下所示
延迟属性-1
3、延迟存储在第一次访问的时候才被赋值
可以通过调试,来查看实例变量的内存变化
- age
第一次访问前的内存情况:此时的age是没值的,为0x0
延迟属性-2
- age
第一次访问后的内存情况:此时age是有值的,为20
延迟属性-3
从而可以验证,懒加载存储属性只有在第一次访问时才会被赋值
我们也可以通过sil文件来查看,这里可以在生成sil文件时,加上还原swift中混淆名称的命令(即xcrun swift-demangle):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil,demo代码如下
class SunriseTeacher{
lazy var age: Int = 18
}
var sunrise = SunriseTeacher()
sunrise.age = 20
-
类+main:lazy修饰的存储属性在底层是一个optional类型
延迟属性-4
-
setter+getter:从getter方法中可以验证,在第一次访问时,就从没值变成了有值的操作
延迟属性-5
通过sil,有以下两点说明:
-
1、
lazy修饰的属性,在底层默认是optional,在没有被访问时,默认是nil,在内存中的表现就是0x0。在第一次访问过程中,调用的是属性的getter方法,其内部实现是通过当前enum的分支,来进行一个赋值操作 -
2、可选类型是16字节吗?可以通过
MemoryLayout打印- size:实际大小
- stride:分配大小(主要是由于内存对齐)
print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)
//*********** 打印结果 ***********
16
9
为什么实际大小是9?Optional其本质是一个enum,其中Int占8字节,另一个字节主要用于存储case值(这个后续会详细讲解)
4、延迟存储属性并不能保证线程安全
继续分析3中sil文件,主要是查看age的getter方法,如果此时有两个线程:
-
线程1此时访问age,其age是没有值的,进入bb2流程 - 然后时间片将CPU分配给了
线程2,对于optional来说,依然是none,同样可以走到bb2流程 - 所以,在此时,
线程1会走一遍赋值,线程2也会走一遍赋值,并不能保证属性只初始化了一次
5、延迟存储属性对实例对象大小的影响
下面来继续看下不使用lazy的内存与使用lazy的内存是否有变化?
-
不使用lazy修饰的情况,类的内存大小是24
延迟属性-6
-
使用lazy修饰的情况下,类的内存大小是32
延迟属性-7
从而可以证明,使用lazy和不使用lazy,其实例对象的内存大小是不一样的
类型属性
类型属性,主要有以下几点说明:
- 1、使用关键字
static修饰,且是一个全局变量 - 2、类型属性必须有一个
默认的初始值 - 3、类型属性只会被
初始化一次
1、使用关键字static修饰
class SunriseTeacher{
static var age: Int = 18
}
// **** 使用 ****
var age = SunriseTeacher.age
生成SIL文件
- 查看定义,发现多了一个
全局变量,说以,类型属性是一个全局变量
类型属性-1
- 查看入口函数中age的获取
类型属性-2
- 查看age的getter方法
类型属性-3
- 其中 globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0是全局变量初始化函数
类型属性-4
-
builtin "once",通过断点调试,发现调用的是swift_once,表示属性只初始化一次
类型属性-5
- 源码中搜索
swift_once,其内部是通过GCD的dispatch_once_f单例实现。从这里可以验证上面的第3点
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#if defined(__APPLE__)
dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
_swift_once_f(predicate, context, fn);
#else
std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}
2、类型属性必须有一个默认的初始值
如下图所示,如果没有给默认的初始值,会报错
类型属性-6
所以对于类型属性来说,一是全局变量,只初始化一次,二是线程安全的
单例的创建
//****** Swift单例 ******
class SunriseTeacher{
//1、使用 static + let 创建声明一个实例对象
static let shareInstance = SunriseTeacher.init()
//2、给当前init添加private访问权限
private init(){ }
}
//使用(只能通过单例,不能通过init)
var t = SunriseTeacher.shareInstance
//****** OC单例 ******
@implementation SunriseTeacher
+ (instancetype)shareInstance{
static SunriseTeacher *shareInstance = nil;
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[SunriseTeacher alloc] init];
});
return shareInstance;
}
@end
总结
-
存储属性会占用实例变量的内存空间 -
计算属性不会占用内存空间,其本质是set/get方法 -
属性观察者-
willset:新值存储之前调用,先通知子类,再通知父类(因为父类中可能需要做一些额外的操作),即子父 -
didSet:新值存储完成后,先告诉父类,再通知子类(父类的操作优先于子类),即父子 - 类中的
init方法赋值不会触发属性观察 - 属性可以添加在
类定义的存储属性、继承的存储属性、继承的计算属性中 - 子类调用父类的
init方法,会触发观察属性
-
-
延迟存储属性- 使用
lazy修饰存储属性,且必须有一个默认值 - 只有在
第一次被访问时才会被赋值,且是线程不安全的 - 使用lazy和不使用lazy,会
对实例对象的内存大小有影响,主要是因为lazy在底层是optional类型,optional的本质是enum,除了存储属性本身的内存大小,还需要一个字节用于存储case
- 使用
-
类型属性- 使用
static修饰,且必须有一个默认初始值 - 是一个全局变量,只会被
初始化一次,是线程安全的 - 用于创建
单例对象:- 使用
static + let创建实例变量 -
init方法的访问权限为private
- 使用
- 使用













网友评论