Swift底层探索(二):类型属性、值类型&引用类型

延迟存储属性

class HotpotCat {
    lazy var age: Int = 20
}

var hotpot = HotpotCat()
hotpot.age = 30

延迟属性赋值&大小

在hotpot.age赋值前和赋值后各打个断点,x/8g看下内存地址,可以看到赋值前是0x0,没有值。


image.png

我们知道不加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赋值,调用了agesetter方法

  %14 = class_method %9 : $HotpotCat, #HotpotCat.age!setter : (HotpotCat) -> (Int) -> (), $@convention(method) (Int, @guaranteed HotpotCat) -> () // user: %15

agesetter方法中,可以看到是个可选值。

image.png

getter同理,有值走bb1,没有值(Optional.none)走bb2赋默认值。
image.png

那么延迟加载属性本质上是可选类型,在没有被访问前,默认值是nil(0x0) 。在getter方法中枚举值分支进行赋值操作。
那这里就解释了为什么这里存储属性加上lazy就变成32字节了。因为要存储枚举信息,实际大小为9字节,字节对齐16字节。

print(MemoryLayout>.size)
print(MemoryLayout>.stride)
9
16

swift enum默认大小为Int8类型,编译器会根据枚举数量调整(Int8Int16Int32Int64)。如果只有一个枚举值系统也会优化size0stride1
获取实例的大小可以通过MemoryLayout.size(ofValue:)获取。

延迟存储属性不能保证线程安全

上面我们分析了在getter方法中,会有bb1bb2两个分支,假设此时线程1在访问bb2并且没有结束,线程2也调用getter那么也可能会走bb2分支。这里就不能保证线程安全。

image.png

  • 用关键字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

image.png

ageHotpotCat.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

image.png

可以看到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
}

image.png

对于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

image.png

可以看到地址存储的确实是ageage2
再将hotpot赋值给hotpot2
image.png

再看下SIL,发现直接调用了init,没有alloc
image.png

我们再看下init
image.png

可以看到默认alloc_stack自身,在ageage2赋值的时候是从self往下找的。init返回分配在satck的自身。所以他是一个值类型。

引用类型

那么如果是class呢?

class HotpotClass {
    var age: Int = 1
    var age2: Int = 18
}

var hotpotClass = HotpotClass()

(lldb) po hotpotClass

po直接显示地址。

image.png

对于引用类型存储的是地址。
如果值类型中有引用类型呢?

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
image.png

可以看出依然传递的是地址,所以应当避免值类型中包含引用类型。
我们再通过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处。

image.png

我们打印一下看看

(lldb) po CFGetRetainCount(hotpotStruct.hotpotClass)
3

mutating

image.png
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)
    }
}

对应的SILclear实现如下,可以看到隐藏参数selflet修饰,修改count就为修改地址也就是修改值类型self本身,所以方法中不能修改count

image.png

init中为什么能修改呢?initalloc_stackselfvar
image.png

那如果self我们用var接收再修改呢?修改是可以修改,只不过修改的是self的另外一份拷贝(值传递)。

    func clear() {
        var s = self
        s.count = 10
    }

加了mutaing进行了什么操作呢?可以看到self是用var修饰的添加了inout关键字,debug_value也变成了debug_value_addr

image.png

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)
image.png

struct函数调用方式

struct HotpotStruct {
    func clear() {
       print("clear")
    }
}

var hotpotStruct =  HotpotStruct()
hotpotStruct.clear()

在函数调用处打个断点,直接看下汇编代码看到是直接调用的函数地址(0x100003da0)

image.png

control +step into跟进去可以看到执行的就是HotpotStruct.clear()
接着我们直接分析下可执行文件
image.png

可以确定编译链接完成后,可执行文件调用的函数地址就已经确定了。
结构体中的方法是静态调度(编译、链接完成后函数地址就已经确定存放在了代码段)
刚才可执行程序分析的时候并没有对应的符号SwiftStructMutating.HotpotStruct.clear(),符号从哪里来的呢?符号表Symbol Table 存储符号位于字符串表的偏移)。clear在字符串表偏移0x2静态函数只在debug下有符号,release下会生成dsym文件。不能确定地址的当然还会有。Debug模式下只是方便我们调试
image.png

符号表不直接存放字符串,字符串存放在字符串表(String Table,存放了所有的变量名、函数名)。
image.png

偏移两个字节,从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的调度方式

image.png

image.png

c/oc中方法的调度

void test(){
    
}

int main(int argc, const char * argv[]) {
    test();
    return 0;
}

可以看到c语言中符号直接就是_test,所以这也是c语言不允许函数重载的原因。OC同理-[Class selector]

image.png

这也就解释了Swift命名重整规则复杂的原因(确保符号的唯一)。

struct HotpotCat {
    func pot123() {
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var hotpot = HotpotCat()
        hotpot.pot123()
    }
}

打下断点看下函数地址0x10ff42bb0

image.png

image.png

可以看到两个地址有偏差,这里偏差的就是ALSR(随机地址偏移)
可以通过image list查看,这里首地址0x0ff3f000就是ALSR

[  0] A3D54669-3D78-3CA1-A48F-958F7718E05B 0x000000010ff3f000

VM Address静态基地址,首地址就是根据静态及地址+偏移量确定的。

image.png

pot123的地址0000000100003BB0+随机偏移地址(0x0ff3f000)=运行时地址(0x10ff42bb0)
image.png

你可能感兴趣的:(Swift底层探索(二):类型属性、值类型&引用类型)