本文主要介绍以下几点
- 通过SIL来理解对象的创建
- Swift类结构分析
- 存储属性 & 计算属性
- 延迟存储属性 & 单例创建方式
SIL
在底层流程中,OC代码和SWift代码时通过不同的编译器
进行编译,然后通过LLVM
,生成.o
可执行文件,如下所示
OC
中通过clang
编译器(clang可以参考这篇文章iOS-底层原理 31:LLVM编译流程 & Clang插件开发),编译成IR,然后再生成可执行文件.o(即机器码)swift
中通过swiftc
编译器,编译成IR,然后再生成可执行文件
下面是Swift中的编译流程,其中SIL
(Swift Intermediate Language),是Swift编译过程中的中间代码
,主要用于进一步分析和优化Swift代码。如下图所示,SIL
位于在AST
和LLVM
IR之间
注意:这里需要说明一下,Swift与OC的区别
在于 Swift生成了高级的SIL
我们可以通过swiftc -h
终端命令,查看swiftc的所有命令
例如:在main.swift文件定义如下代码
class CJLTeacher{ var age: Int = 18 var name: String = "CJL" } var t = CJLTeacher()
查看抽象语法树: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`:创建一个`全局变量`,即代码中的`t` alloc_global @$s4main1tAA10CJLTeacherCvp // id: %2 //`global_addr`:获取全局变量地址,并赋值给寄存器%3 %3 = global_addr @$s4main1tAA10CJLTeacherCvp : $*CJLTeacher // user: %7 //`metatype`获取`CJLTeacher`的`MetaData`赋值给%4 %4 = metatype $@thick CJLTeacher.Type // user: %6 //将`__allocating_init`的函数地址赋值给 %5 // function_ref CJLTeacher.__allocating_init() %5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %6 //`apply`调用 `__allocating_init` 初始化一个变量,赋值给%6 %6 = apply %5(%4) : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %7 //将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环) store %6 to %3 : $*CJLTeacher // 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
中做了如下配置,可以在终端中指定软件打开相应文件
$ open .zshrc //****** 添加以下别名 alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl' alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code' //****** 使用 $ code main.sil //如果想SIL文件高亮,需要安装插件:VSCode SIL
从SIL文件中,可以看出,代码是经过混淆的,可以通过以下命令还原,以s4main1tAA10CJLTeacherCvp
为例:xcrun swift-demangle s4main1tAA10CJLTeacherCvp
在SIL文件中搜索s4main10CJLTeacherCACycfC
,其内部实现主要是分配内存+初始化变量
allocing_ref
:创建一个CJLTeacher
的实例对象,当前实例对象的引用计数为1
- 调用
init
方法
//********* main入口函数中代码 ********* %5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // s4main10CJLTeacherCACycfC 实际就是__allocating_init() // CJLTeacher.__allocating_init() sil hidden [exact_self_class] @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher { // %0 "$metatype" bb0(%0 : $@thick CJLTeacher.Type): // 堆上分配内存空间 %1 = alloc_ref $CJLTeacher // user: %3 // function_ref CJLTeacher.init() 初始化当前变量 %2 = function_ref @$s4main10CJLTeacherCACycfc : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %3 // 返回 %3 = apply %2(%1) : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %4 return %3 : $CJLTeacher // id: %4 } // end sil function '$s4main10CJLTeacherCACycfC'
SIL语言对于Swift源码的分析是非常重要的,关于其更多的语法信息,可以在这个网站进行查询
符号断点调试
- 在demo中设置
_allocing_init
符号断点
发现其内部调用的是swift_allocObject
源码调试
下面我们就通过swift_allocObject
来探索swift中对象的创建过程
- 在
REPL
(命令交互行,类似于python的,可以在这里编写代码)中编写如下代码(也可以拷贝),并搜索swift_allocObject
函数加一个断点,然后定义一个实例对象t
断点断住,查看左边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
- 其中
metadata
类型是HeapMetadata
,是一个指针类型,占8
字节 refCounts
(引用计数,类型是InlineRefCounts
,而InlineRefCounts
是一个类RefCounts
的别名,占8
个字节),swift采用arc引用计数
总结
- 对于实例对象
t
来说,其本质是一个HeapObject
结构体,默认16
字节内存大小(metadata
8字节 +refCounts
8字节),与OC的对比如下
- OC中实例对象的本质是
结构体
,是以objc_object
为模板继承的,其中有一个isa指针,占8
字节 - Swift中实例对象,默认的比OC中多了一个
refCounted
引用计数大小,默认属性占16
字节
- 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中是有所区别的
所以这也解释了为什么CJLTeacher
的内存大小等于40
,即40 = metadata(8字节) +refCount(8字节)+ Int(8字节)+ String(16字节)
这里验证了40
的来源,但是metadata
是什么还不知道,继续往下分析
探索Swift中类的结构
在OC中类是从objc_class
模板继承过来的,具体的参考这篇文章iOS-底层原理 08:类 & 类结构分析
而在Swift中,类的结构在底层是HeapObject
,其中有 metadata + refCounts
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
种类总结如下
回到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是Classpo metadata->getClassObject()
、x/8g 0x0000000110efdc70,这个地址中存储的是元数据信息!
所以,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
中的方法存储在objc_class
结构体class_rw_t
的methodList
中swift
中的方法存储在metadata
元数据中
- 引用计数
- OC中的ARC维护的是
散列表
- Swift中的ARC是对象内部有一个
refCounts
属性
Swift属性
在swift中,属性主要分为以下几种
- 存储属性
- 计算属性
- 延迟存储属性
- 类型属性
存储属性
存储属性,又分两种:
- 要么是
常量存储属性
,即let
修饰 - 要么是
变量存储属性
,即var
修饰
定义如下代码
class CJLTeacher{ var age: Int = 18 var name: String = "CJL" } let t = CJLTeacher()
其中代码中的age、name
来说,都是变量存储属性
,这一点可以在SIL
中体现
class CJLTeacher { //_hasStorage 表示是存储属性 @_hasStorage @_hasInitialValue var age: Int { get set } @_hasStorage @_hasInitialValue var name: String { get set } @objc deinit init() }
存储属性特征:会占用占用分配实例对象的内存空间
下面我们同断点调试来验证
- po t
- x/8g 内存地址,即HeapObject存储的地址
计算属性
计算属性:是指不占用内存空间,本质是set/get方法的属性
我们通过一个demo来说明,以下写法正确吗?
class CJLTeacher{ var age: Int{ get{ return 18 } set{ age = newValue } } }
在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set
然后运行发现崩溃了,原因是age的set方法中调用age.set
导致了循环引用,即递归
验证:不占内存
对于其不占用内存空间
这一特征,我们可以通过以下案例来验证
,打印以下类的内存大小
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
方法
属性观察者(didSet、willSet)
willSet
:新值存储之前调用newValue
didSet
:新值存储之后调用oldValue
验证
- 可以通过demo来验证
class CJLTeacher{ var name: String = "测试"{ //新值存储之前调用 willSet{ print("willSet newValue \(newValue)") } //新值存储之后调用 didSet{ print("didSet oldValue \(oldValue)") } } } var t = CJLTeacher() t.name = "CJL" //**********打印结果********* willSet newValue CJL didSet oldValue 测试
也可以通过编译来验证,将main.swift编译成mail.sil,在sil文件中找name
的set
方法
问题1:init方法中是否会触发属性观察者?
以下代码中,init方法中设置name,是否会触发属性观察者?
class CJLTeacher{ var name: String = "测试"{ //新值存储之前调用 willSet{ print("willSet newValue \(newValue)") } //新值存储之后调用 didSet{ print("didSet oldValue \(oldValue)") } } init() { self.name = "CJL" } }
运行结果发现,并没有走willSet、didSet中的打印方法,所以有以下结论:
- 在
init
方法中,如果调用属性,是不会触发
属性观察者的 - init中主要是
初始化当前变量
,除了默认的前16个字节,其他属性会调用memset
清理内存空间(因为有可能是脏数据,即被别人用过),然后才会赋值
【总结】:初始化器(即init
方法设置)和定义时设置默认值(即在didSet
中调用其他属性值)都不会触发
问题2:哪里可以添加属性观察者?
主要有以下三个地方可以添加:
- 1、
类
中定义的存储属性
- 2、通过类
继承的存储属性
class CJLMediumTeacher: CJLTeacher{ override var age: Int{ //新值存储之前调用 willSet{ print("willSet newValue \(newValue)") } //新值存储之后调用 didSet{ print("didSet oldValue \(oldValue)") } } }
- 3、通过类
继承的计算属性
class CJLTeacher{ var age: Int = 18 var age2: Int { get{ return age } set{ self.age = newValue } } } var t = CJLTeacher() class CJLMediumTeacher: CJLTeacher{ 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 CJLTeacher{ var age: Int = 18{ //新值存储之前调用 willSet{ print("父类 willSet newValue \(newValue)") } //新值存储之后调用 didSet{ print("父类 didSet oldValue \(oldValue)") } } var age2: Int { get{ return age } set{ self.age = newValue } } } class CJLMediumTeacher: CJLTeacher{ override var age: Int{ //新值存储之前调用 willSet{ print("子类 newValue \(newValue)") } //新值存储之后调用 didSet{ print("子类 didSet oldValue \(oldValue)") } } } var t = CJLMediumTeacher() t.age = 20
运行结果如下:
结论:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,在父类didset, 子类的didset,即:子父 父子
问题4:子类调用了父类的init,是否会触发观察属性?
在问题3的基础,修改CJLMediumTeacher
类
class CJLMediumTeacher: CJLTeacher{ override var age: Int{ //新值存储之前调用 willSet{ print("子类 willSet newValue \(newValue)") } //新值存储之后调用 didSet{ print("子类 didSet oldValue \(oldValue)") } } override init() { super.init() self.age = 20 } } //****** 打印结果 ****** 子类 willSet newValue 20 父类 willSet newValue 20 父类 didSet oldValue 18 子类 didSet oldValue 18
从打印结果发现,会触发属性观察者,主要是因为子类
调用了父类
的init
,已经初始化过了,而初始化流程保证了所有属性都有值(即super.init
确保变量初始化完成了),所以可以观察属性了
延迟属性
延迟属性主要有以下几点说明:
- 1、使用
lazy
修饰的存储属性 - 2、延迟属性必须有一个默认的初始值
- 3、延迟存储在第一次访问的时候才被赋值
- 4、延迟存储属性并不能保证线程安全
- 5、延迟存储属性对实例对象大小的影响
下面来一一进行分析
1、使用lazy修饰的存储属性
class CJLTeacher{ lazy var age: Int = 18 }
2、延迟属性必须有一个默认的初始值
如果定义为可选类型,则会报错,如下所示
3、延迟存储在第一次访问的时候才被赋值
可以通过调试,来查看实例变量的内存变化
- age
第一次访问前
的内存情况:此时的age是没值
的,为0x0
ge第一次访问后
的内存情况:此时age是有值
的,为30
- 从而可以验证,懒加载存储属性只有在第一次访问时才会被赋值
我们也可以通过sil
文件来查看,这里可以在生成sil文件时,加上还原swift中混淆名称
的命令(即xcrun swift-demangle
):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil
,demo代码如下
class CJLTeacher{ lazy var age: Int = 18 } var t = CJLTeacher() t.age = 30
类+main
:lazy修饰的存储属性在底层是一个optional
类型
setter+getter
:从getter方法中可以验证,在第一次访问时,就从没值变成了有值的操作
通过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
使用lazy
修饰的情况下,类的内存大小是32
从而可以证明,使用lazy和不使用lazy,其实例对象的内存大小是不一样的
类型属性
类型属性,主要有以下几点说明:
- 1、使用关键字
static
修饰,且是一个全局变量 - 2、类型属性必须有一个
默认的初始值
- 3、类型属性只会被
初始化一次
1、使用关键字static修饰
class CJLTeacher{ static var age: Int = 18 } // **** 使用 **** var age = CJLTeacher.age
生成SIL文件
- 查看定义,发现多了一个
全局变量
,说以,类型属性是一个全局变量
查看入口函数中age的获取
查看age的getter方法
其中 globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
是全局变量初始化函数
builtin "once"
,通过断点调试,发现调用的是swift_once
,表示属性只初始化一次
- 源码中搜索
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、类型属性必须有一个默认的初始值
如下图所示,如果没有给默认的初始值,会报错
所以对于类型属性
来说,一是全局变量
,只初始化一次,二是线程安全的
单例的创建
//****** Swift单例 ****** class CJLTeacher{ //1、使用 static + let 创建声明一个实例对象 static let shareInstance = CJLTeacher.init() //2、给当前init添加private访问权限 private init(){ } } //使用(只能通过单例,不能通过init) var t = CJLTeacher.shareInstance //****** OC单例 ****** @implementation CJLTeacher + (instancetype)shareInstance{ static CJLTeacher *shareInstance = nil; dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shareInstance = [[CJLTeacher alloc] init]; }); return shareInstance; } @end
总结
存储属性
会占用实例变量的内存空间,且计算属性
不会占用内存空间,其本质是set/get
方法- 属性观察者
willset
:新值存储之前调用,先通知子类,再通知父类(因为父类中可能需要做一些额外的操作),即子父
didSet
:新值存储完成后,先告诉父类,再通知子类(父类的操作优先于子类),即父子
- 类中的
init
方法赋值不会触发
属性观察 - 属性可以添加在
类定义的存储属性、继承的存储属性、继承的计算属性
中 - 子类调用父类的
init
方法,会触发
观察属性
- 延迟存储属性
- 使用
lazy
修饰存储属性,且必须有一个默认值
- 只有在
第一次被访问时才会被赋值
,且是线程不安全
的 - 使用lazy和不使用lazy,会
对实例对象的内存大小有影响
,主要是因为lazy在底层是optional
类型,optional的本质是enum
,除了存储属性本身的内存大小,还需要一个字节用于存储case
- 类型属性
- 使用
static
修饰,且必须有一个默认初始值
- 是一个全局变量,只会被
初始化一次
,是线程安全
的 - 用于创建
单例
对象:
- 使用
static + let
创建实例变量 init
方法的访问权限为private