延迟存储属性
class HotpotCat {
lazy var age: Int = 20
}
var hotpot = HotpotCat()
hotpot.age = 30
延迟属性赋值&大小
在hotpot.age赋值前和赋值后各打个断点,x/8g看下内存地址,可以看到赋值前是0x0,没有值。
我们知道不加lazy的情况下内存大小为24,那么加了lazy后呢?
print(class_getInstanceSize(HotpotCat.self))
32
(lldb)
发现变为32了,多了8字节。那么分析下SIL:
这里有个小细节是加xcrun swift-demangle,会还原sil中swift的符号,方便阅读
swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
我们可以看到在main函数中,会给age赋值,调用了age
的setter
方法
%14 = class_method %9 : $HotpotCat, #HotpotCat.age!setter : (HotpotCat) -> (Int) -> (), $@convention(method) (Int, @guaranteed HotpotCat) -> () // user: %15
在age
的setter
方法中,可以看到是个可选值。
getter同理,有值走bb1,没有值(
Optional.none
)走bb2赋默认值。
那么延迟加载属性本质上是可选类型,在没有被访问前,默认值是nil(0x0) 。在getter方法中枚举值分支进行赋值操作。
那这里就解释了为什么这里存储属性加上lazy
就变成32
字节了。因为要存储枚举信息,实际大小为9
字节,字节对齐16
字节。
print(MemoryLayout>.size)
print(MemoryLayout>.stride)
9
16
swift enum
默认大小为Int8
类型,编译器会根据枚举数量调整(Int8
、Int16
、Int32
、Int64
)。如果只有一个枚举值系统也会优化size
为0
,stride
为1
。
获取实例的大小可以通过MemoryLayout.size(ofValue:)
获取。
延迟存储属性不能保证线程安全
上面我们分析了在getter方法中,会有bb1
和bb2
两个分支,假设此时线程1在访问bb2
并且没有结束,线程2也调用getter那么也可能会走bb2
分支。这里就不能保证线程安全。
- 用关键字
lazy
来标识一个延迟存储属性。 - 延迟存储属性必须有一个默认的初始值。
- 延迟存储属性在第一次访问的时候才被赋值。
- 延迟存储属性并不能保证线程安全。
- 延迟存储属性对实例对象大小有影响。
类型属性
class HotpotCat {
static var age: Int = 20
}
var age = HotpotCat.age
// globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
sil_global private @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $Builtin.Word
// static HotpotCat.age
sil_global hidden @static main.HotpotCat.age : Swift.Int : $Int
生成的age
变成了全局的变量sil_global
age
是HotpotCat.age.unsafeMutableAddressor
生成的。
// HotpotCat.age.unsafeMutableAddressor
sil hidden [global_init] @main.HotpotCat.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
%0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
%2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
%4 = global_addr @static main.HotpotCat.age : Swift.Int : $*Int // user: %5
%5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
return %5 : $Builtin.RawPointer // id: %6
} // end sil function 'main.HotpotCat.age.unsafeMutableAddressor : Swift.Int'
这个方法执行了全局函数globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC
,并且这里有个builtin "once"
。
// globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
sil private @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () {
bb0:
alloc_global @static main.HotpotCat.age : Swift.Int // id: %0
%1 = global_addr @static main.HotpotCat.age : Swift.Int : $*Int // user: %4
%2 = integer_literal $Builtin.Int64, 20 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function 'globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0'
这个函数将初始值赋值给了全局变量age
。
前面说到的once
就是swift_once
,我们源码里面搜索一下swift_once
可以看到
dispatch_once_f
,这也就是我们的GCD
,所以static类型属性只会被初始化一次。
- 用static声明类型属性
- 类型属性属于类本身,不论有多少个实例,类型属性只有一份
- 全局的
- 线程安全的
单例
首先回顾下OC
单类的写法,需要dispatch_once
确保线程安全并初始化一次,并且为了保证安全还要重写allocWithZone
+ (instancetype)sharedInstance {
static HotpotCat *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[HotpotCat alloc] init];
});
return instance;
}
swift 2.0
的时候swift
的单类也是翻译的oc
的写法,现在已经不用了。
swift
单类只需要两点:
-
static
修饰一个常量。 - 给
init
添加一个访问控制权限。
class HotpotCat {
static let sharedInstance = HotpotCat()
private init(){
}
}
var hotpot = HotpotCat.sharedInstance
值类型
struct
struct HotpotCatStruct {
var age: Int
var name: String
}
class HotpotCatClass {
var age: Int
var name: String
}
对于
HotpotCatStruct
而言,已经有自动合成的初始化方法,对于HotpotCatClass
而言"Class 'HotpotCatClass' has no initializers"。
通过
SIL
查看下
struct HotpotCatStruct {
@_hasStorage var age: Int { get set }
@_hasStorage var name: String { get set }
init(age: Int, name: String)
}
已经有了init(age: Int, name: String)
方法。
那么我们给age
一个默认值呢?
struct HotpotCatStruct {
var age: Int = 1
}
//sil
struct HotpotCatStruct {
@_hasStorage @_hasInitialValue var age: Int { get set }
init(age: Int = 1)
init()
}
可以看到既有默认的初始化方法,也有age初始化方法。
那么我们自己实现了init
方法呢?
struct HotpotCatStruct {
var age: Int = 1
init(age: Int) {
self.age = age
}
}
//sil
struct HotpotCatStruct {
@_hasStorage @_hasInitialValue var age: Int { get set }
init(age: Int)
}
可以看到编译器不会帮我们生成了。
- 结构体不需要自定义初始化方法,系统自动合成。
- 如果属性有默认值,系统会提供不同的默认初始化方法。
- 如果自定义初始化方法,系统不会帮我们生成。
struct值类型
首先明白下基本概念
- 地址存储的就是值
- 传递过程中传递副本
struct HotpotCat {
var age: Int = 1
var age2: Int = 18
}
var hotpot = HotpotCat()
//lldb
(lldb) po hotpot
▿ HotpotCat
- age : 1
- age2 : 18
po
直接打印出来是值,不是地址。说明是值类型,我们再打印一下内存地址看下存储的内容
(lldb) po withUnsafeMutablePointer(to: &hotpot){print($0)}
0x0000000100008028
0 elements
可以看到地址存储的确实是
age
和age2
。
再将
hotpot
赋值给hotpot2
再看下SIL,发现直接调用了
init
,没有alloc
我们再看下
init
可以看到默认
alloc_stack
自身,在age
和age2
赋值的时候是从self
往下找的。init
返回分配在satck
的自身。所以他是一个值类型。
引用类型
那么如果是class
呢?
class HotpotClass {
var age: Int = 1
var age2: Int = 18
}
var hotpotClass = HotpotClass()
(lldb) po hotpotClass
po
直接显示地址。
对于引用类型存储的是地址。
如果值类型中有引用类型呢?
struct HotpotStruct {
var age: Int = 1
var age2: Int = 18
var hotpotClass: HotpotClass = HotpotClass()
}
class HotpotClass {
var age: Int = 1
var age2: Int = 18
}
var hotpotStruct = HotpotStruct()
var hotpotStruct2 = hotpotStruct
可以看出依然传递的是地址,所以应当避免值类型中包含引用类型。
我们再通过SIL看一下
struct HotpotStruct {
var age: Int = 1
var age2: Int = 18
var hotpotClass: HotpotClass
}
class HotpotClass {
var name: String = "HotpotCat"
}
var hotpotClass = HotpotClass()
var hotpotStruct = HotpotStruct(hotpotClass: hotpotClass)
var hotpotStruct2 = hotpotStruct
看到这里有引用计数了,strong_retain
搜索查看下发现一共有3
处。
我们打印一下看看
(lldb) po CFGetRetainCount(hotpotStruct.hotpotClass)
3
mutating
struct HotpotStruct {
var count: Int = 0
init() {
self.count = 10
}
mutating func clear() {
self.count = 0
}
}
值类型本身创建后不允许修改,要修改需要加上mutating
关键字
那么mutating
到底干了什么呢?
先看个正常访问变量的例子
struct HotpotStruct {
var count: Int = 0
func clear() {
print(count)
}
}
对应的SIL
中clear
实现如下,可以看到隐藏参数self
为let
修饰,修改count
就为修改地址也就是修改值类型self
本身,所以方法中不能修改count
。
init
中为什么能修改呢?init
中alloc_stack
的self
为var
。
那如果self
我们用var
接收再修改呢?修改是可以修改,只不过修改的是self
的另外一份拷贝(值传递)。
func clear() {
var s = self
s.count = 10
}
加了mutaing
进行了什么操作呢?可以看到self
是用var
修饰的添加了inout
关键字,debug_value
也变成了debug_value_addr
。
mutaing本质就是给参数添加了
inout
关键字传递地址,只用于值类型,因为引用类型本身就是对地址的操作
再用
&
操作验证下
func swap(_ a:inout Int, _ b:inout Int) {
a = a ^ b
b = a ^ b
a = a ^ b
}
var valueA = 1
var valueB = 2
swap(&valueA, &valueB)
struct函数调用方式
struct HotpotStruct {
func clear() {
print("clear")
}
}
var hotpotStruct = HotpotStruct()
hotpotStruct.clear()
在函数调用处打个断点,直接看下汇编代码看到是直接调用的函数地址(0x100003da0
)
control +step into
跟进去可以看到执行的就是HotpotStruct.clear()
。
接着我们直接分析下可执行文件
可以确定编译链接完成后,可执行文件调用的函数地址就已经确定了。
结构体中的方法是静态调度(编译、链接完成后函数地址就已经确定存放在了代码段)
刚才可执行程序分析的时候并没有对应的符号
SwiftStructMutating.HotpotStruct.clear()
,符号从哪里来的呢?符号表(Symbol Table
存储符号位于字符串表的偏移)。clear
在字符串表偏移0x2
。静态函数只在debug下有符号,release下会生成dsym文件。不能确定地址的当然还会有。Debug模式下只是方便我们调试
符号表不直接存放字符串,字符串存放在字符串表(
String Table
,存放了所有的变量名、函数名)。
偏移两个字节,从
005F开始~005F结束
,都是clear
的符号。这里的字符串经过了命名重整。
我们直接在终端
nm 可执行文件路径
dump下符号
0000000100003da0 T _$s19SwiftStructMutating06HotpotB0V5clearyyF
0000000100003e90 T _$s19SwiftStructMutating06HotpotB0VACycfC
0000000100003f64 s _$s19SwiftStructMutating06HotpotB0VMF
0000000100003ea0 T _$s19SwiftStructMutating06HotpotB0VMa
0000000100004018 s _$s19SwiftStructMutating06HotpotB0VMf
0000000100003f40 S _$s19SwiftStructMutating06HotpotB0VMn
0000000100004020 S _$s19SwiftStructMutating06HotpotB0VN
0000000100003f24 s _$s19SwiftStructMutatingMXM
还原一下符号名称
xcrun swift-demangle s19SwiftStructMutating06HotpotB0V5clearyyF
$s19SwiftStructMutating06HotpotB0V5clearyyF ---> SwiftStructMutating.HotpotStruct.clear() -> ()
搜索符号nm 可执行文件路径 | grep 地址
这里地址不带0x
nm /Users/***/Library/Developer/Xcode/DerivedData/SwiftStructMutating-bdtkkopulnojomaeuxaauwsnscxz/Build/Products/Debug/SwiftStructMutating | grep 0100003da0
0000000100003da0 T _$s19SwiftStructMutating06HotpotB0V5clearyyF
与之对应的我们看一下print
的调度方式
c/oc中方法的调度
void test(){
}
int main(int argc, const char * argv[]) {
test();
return 0;
}
可以看到c语言中符号直接就是_test
,所以这也是c语言不允许函数重载的原因。OC
同理-[Class selector]
。
这也就解释了
Swift
命名重整规则复杂的原因(确保符号的唯一)。
struct HotpotCat {
func pot123() {
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var hotpot = HotpotCat()
hotpot.pot123()
}
}
打下断点看下函数地址0x10ff42bb0
可以看到两个地址有偏差,这里偏差的就是
ALSR
(随机地址偏移)
可以通过
image list
查看,这里首地址0x0ff3f000
就是ALSR
。
[ 0] A3D54669-3D78-3CA1-A48F-958F7718E05B 0x000000010ff3f000
VM Address
静态基地址,首地址就是根据静态及地址+偏移量确定的。
pot123
的地址0000000100003BB0
+随机偏移地址(0x0ff3f000
)=运行时地址(0x10ff42bb0
)