在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存储的地址
属性-1
属性-2
计算属性
计算属性:是指不占用内存空间,本质是set/get方法的属性
我们通过一个demo来说明,以下写法正确吗?
class CJLTeacher{
var age: Int{
get{
return 18
}
set{
age = newValue
}
}
}
在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set
属性-3
然后运行发现崩溃了,原因是age的set方法中调用age.set
导致了循环引用,即递归
属性-4
验证:不占内存
对于其不占用内存空间
这一特征,我们可以通过以下案例来验证
,打印以下类的内存大小
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
方法属性-5
属性观察者(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
方法属性-6
问题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
运行结果如下:
属性-7
结论:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类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、延迟属性必须有一个默认的初始值
如果定义为可选类型,则会报错,如下所示
属性-8
3、延迟存储在第一次访问的时候才被赋值
可以通过调试,来查看实例变量的内存变化
-
age
第一次访问前
的内存情况:此时的age是没值
的,为0x0
属性-9
-
age
第一次访问后
的内存情况:此时age是有值
的,为30属性-10
从而可以验证,懒加载存储属性只有在第一次访问时才会被赋值
我们也可以通过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
类型属性-11
-
setter+getter
:从getter方法中可以验证,在第一次访问时,就从没值变成了有值的操作属性-12
通过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文件
-
查看定义,发现多了一个
全局变量
,说以,类型属性是一个全局变量属性-15
-
查看入口函数中age的获取
属性-16
-
查看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、类型属性必须有一个默认的初始值
如下图所示,如果没有给默认的初始值,会报错
属性-20
所以对于类型属性
来说,一是全局变量
,只初始化一次,二是线程安全的
单例的创建
//****** 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 + let
创建实例变量 -
init
方法的访问权限为private
-
使用
static
修饰,且必须有一个默认初始值
-
是一个全局变量,只会被
初始化一次
,是线程安全
的 -
用于创建
单例
对象:
-