Files
knowledge-kit/Chapter1 - iOS/1.115.md
2024-04-27 13:01:58 +08:00

14 KiB
Raw Blame History

属性

计算属性的本质是方法。

struct Circle {
    var radius: Int
    var diameter: Int {
        set {
            radius = newValue/2
        }
        get {
            2 * radius
        }
    }
}
var circle = Circle(radius: 10)
// print(circle.diameter) // 20
circle.diameter = 24
//  print(circle.radius) // 12
let diameter = circle.diameter

计算属性 y 等价于下面的代码:

setDiameter (newValue: Int) {
	radius = newValue/2
}
getDiameter () {
	return 2*radius
}

然后通过汇编来窥探下,在 circle.diameter = 22 处加断点,可以看到本质上调用的就是 setter 方法,setter 内部的实现就不一一窥探了

然后断点继续,在 let diameter = circle.diameter 处加断点,可以看到调用了 getter 方法

异同点

存储属性:

  • 类似于成员变量

  • 存储在实例的内存中

  • 结构体、类可以定义存储属性

  • 枚举不可以定义存储属性

  • 在创建类、结构体的实例时,必须为所有的存储属性设置一个合适的初始值

  • 延迟存储属性必须是 var,不能是 let。 因为 Swift 规定 let 必须在实例的初始化方法完成之前就拥有值

  • lazy 在多线程情况下无法保证属性只被初始化1次。

    struct Point {
        var x:Int
        lazy var y = 0
        init(_ x: Int = 0) {
            self.x = x
        }
    }
    var p = Point(2)
    print(p.y)
    
  • 当结构体包含一个延迟存储属性时,只有 var 才能访问延迟存储属性因为延迟属性初始化的时候需要改变结构体内存。Class 的话,实例可以用 let 修饰,访问延迟存储属性是可以的。

    QA为什么结构体包含一个延迟存储属性时只有 var 才能访问延迟存储属性?

    之前在 Swift 结构体和类的内存布局 探究过 struct 的内存布局,struct 的成员变量在内存中是连续存储的。由于是延迟存储属性,等真正使用的时候才会执行延迟属性的初始化逻辑,才会更改 struct 的内存,所以 let 无法满足更改内存的需求。

    struct Point {
        var x = 0
        lazy var y = 0
    }
    
    let p = Point()
    print(p.y) // Cannot use mutating getter on immutable value: 'p' is a 'let' constant
    
    var p2 = Point()
    print(p2.y) // 0 
    
    
    class Point {
        var x:Int
        lazy var y = 0
        init(_ x: Int = 0) {
            self.x = x
        }
    }
    
    let p = Point(2)
    print(p.y)	// 0
    

计算属性:

  • 本质就是方法
  • 不占用实例内存
  • 枚举、结构体、类都可以定义计算属性
  • 计算属性只能用 var不能用 let

枚举 rawValue 的原理

枚举原始值 rawValue 的本质:只读的计算属性,不占用实例内存。

enum Season: Int {
    case spring = 10
    case summer = 20
    case autumn = 30
    case winter = 40
}

let season = Season.summer
// season.rawValue = 22 // Cannot assign to property: 'rawValue' is immutable
print(season.rawValue)

通过汇编可以可以看到,在调用 enumrawvalue 的时候本质是通过计算属性调用 getter 来实现的。

属性观察器

  • 可以为非 lazyvar 存储属性设置属性观察期

  • 在初始化器中设置属性值不会触发 willSetdidSet

  • 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上

    var num: Int {
        get {
            return 10
        }
        set {
            print("newValue", newValue)
        }
    }
    num = 11
    print(num)
    // console
    newValue 11
    10
    
    func test () {
        var age: Int {
            set {
                print("new age is ", newValue)
            }
            get {
                28
            }
        }
        age = 29
        print(age)
    }
    test()
    // console
    new age is  29
    28
    

Inout 核心原理

普通的存储属性

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willset side", newValue)
        }
        didSet {
            print("didset side", oldValue, side)
        }
    }
    var girth: Int {
        set {
            width = newValue/side
            print("set girth ", newValue)
        }
        get {
            print("get girth")
            return width * side
        }
    }
    func show() {
        print("width is \(width), side is \(side), girth is \(girth)")
    }
}

func changeValue(_ value: inout Int) {
    value = 20
}

var shape = Shape(width: 10, side: 4)
changeValue(&shape.width)
shape.show()

// console
get girth
width is 20, side is 4, girth is 80

changeValue(&shape.width) 处加汇编可以看到断点停在第10行 leaq 0x953c(%rip), %rdi 即将 rip + 0x953c = 0x100002cbc + 0x953c = 0x10000C1F8 赋值给 rdi

第16行也是一样leaq 0x9523(%rip), %rdi 即将 rip + 0x9523 = 0x100002cd5 + 0x953c = 0x10000C1F8 赋值给 rdi

然后看到17行的关键代码LLDB 输入 si可以看到在第6行 movq $0x14, (%rdi)将16进制的 0x14 也就是20移动到指定的内存地址 rdi

因为 struct 结构体内存布局中,成员变量在内存中是连续存储的。所以 struct 的地址也就是 struct 中第一个成员变量 width 的地址。

总结:普通的存储属性,在调用方法的时候,如果参数是 inout 传递引用,则直接传递存储属性的地址值即可。在方法内部,对该地址对应的值进行修改即可。

计算属性

对调用的代码进行调整

var shape = Shape(width: 10, side: 4)
changeValue(&shape.girth)
shape.show()
// console
get girth
set girth  20
get girth
width is 5, side is 4, girth is 20

changeValue(&shape.girth) 处下断点,查看汇编

核心思路:方法参数用 inout修饰,则传递的是引用(内存地址)。

  • 汇编19行 callq 0x1000030e0 ; SwiftDemo.Shape.girth.getter : Swift.Int at main.swift:16 调用了 girth 计算属性的 gettergetter 的返回值存放在寄存器 rax

  • 20行将 movq %rax, -0x28(%rbp) 函数返回值 rax 存放在 main 函数的栈空间上(虽然没有写调用函数,但是代码主入口就是 main 函数),且 rbprsp 之间都是函数的栈空间

  • 21行 leaq -0x28(%rbp), %rdi 将栈空间上 -0x28(%rbp) 的地址值赋值给 rdi 寄存器

  • 22行 callq 0x1000037b0 ; SwiftDemo.changeValue(inout Swift.Int) -> () at main.swift:26 调用 changeValue 方法,参数通过寄存器 rdi 传递,里面是栈空间 getter 值的地址。

  • LLDB 输入 si 查看 changeValue 内部。可以看到第6行 movq $0x14, (%rdi) 直接将20赋值给 rdi

  • LLDB 输入 finish 结束 changeValue 细节查看外部23行汇编 movq -0x28(%rbp), %rdi ,将 main 函数栈空间上 getter 返回值的内存对应的值,保存到寄存器 rdi 上。

  • 25行 callq 0x100003250 ; SwiftDemo.Shape.girth.setter : Swift.Int at main.swift:12 调用计算属性的 setter,函数参数为 rid 寄存器里的值也就是20

总结:带有计算属性的存储属性,如果调用的方法参数是 inout 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很多简单,满足了修改值的需求。但是属性观察器的 setget 就没办法触发了。所以为了触发属性观察器系统的设计是:

  • 第一步:先将传递进去的属性调用 getter ,保存在函数的栈地址空间内的某个内存上
  • 第二步:然后调用带有 inout 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
  • 第三步:将步骤二得到的值后,调用 setter 方法。

这个流程下来,满足了修改值,且触发了原始属性观察器的需求。

带有属性观察器的存储属性

对调用的代码进行调整

var shape = Shape(width: 10, side: 4)
changeValue(&shape.side)
shape.show()
// console
willset side 20
didset side 4 20
get girth
width is 10, side is 20, girth is 200

changeValue(&shape.side) 处添加断点,查看汇编

分析:

  • 17行 movq 0x9549(%rip), %rax ; SwiftDemo.shape : SwiftDemo.Shape + 8 将地址格式为 0x9549(%rip) 一个全局变量,也就是 shape 的地址 + 8 的值,赋值给 rax 寄存器

  • 18行 movq %rax, -0x28(%rbp) 将寄存器 rax 里的值,赋值给 main 函数的栈空间上的局部变量 -0x28(%rbp)

  • 19行 将 main 函数的栈空间上的局部变量 -0x28(%rbp) 的地址值,赋值给寄存器 rdi

  • 20行 callq 0x1000037b0 ; SwiftDemo.changeValue(inout Swift.Int) -> () at main.swift:26 调用 changeValue 方法。函数参数通过寄存器 rdi 传递,函数内部修改了该内存上的值

  • 21行 movq -0x28(%rbp), %rdimain 函数的栈空间上的局部变量 -0x28(%rbp) 的值,赋值给寄存器 rdi。也就是修改后的20

  • 23行 callq 0x100002db0 ; SwiftDemo.Shape.side.setter : Swift.Int at main.swift:3 调用 setter

  • LLDB 输入 si,可以看到 setter 方法内部,调用了 willSetdidSet

总结:带有属性观察器的存储属性,如果调用的方法参数是 inout 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很多简单,满足了修改值的需求。但是属性观察器的 willSetdidSet 就没办法触发了。所以为了触发属性观察器系统的设计是:

  • 第一步:先将传递进去的地址,保存在函数的栈地址空间内的某个内存上
  • 第二步:然后调用带有 inout 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
  • 第三步:将步骤二得到的值后,调用一个 setter 方法,setter 方法内,先调用 willSet,再修改真正的带有观察属性的存储属性的值,最后调用 didSet 方法。

这个流程下来,满足了修改值,且触发了原始属性观察器的需求。

总结

  1. 如果实参有内存地址,且没有设置属性观察器和计算属性,实现是直接将实参的内存地址传入带 inout 函数

  2. 如果实参是计算属性或者设置了属性观察器:系统采用了 Copy In Copy Out 的策略。

    1. 调用带 inout 函数时,先复制实参的值,产生副本 getter栈空间上的局部变量
    2. 将副本的内存地址传入带 inout 函数(副本进行引用传递),在函数内部修改副本的值
    3. 函数返回后再将副本的值覆盖实参的值setterwillSet、set

类型属性

  • 不同于存储属性,类型属性必须设置初始值,永伟类型属性不像存储属性那样有 init 初始化器来初始化存储属性

  • 类型属性就不存储在每个实例的内存里

  • 存储属性默认就是 lazy,会在第一次使用的时候才初始化

  • 存储属性就算被多个线程同时访问但系统会保证只初始化1次

  • 存储类型属性可以是 let

  • 存储属性可以用 classstatic 修饰

  • 枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性

    enum Season {
        static let age: Int = 0
        case spring, summer, antumn, winter
    }
    var season = Season.summer 
    
  • 类型属性的经典场景就是单例模式

    class FileManager {
        private init() {}
        public static let sharedInstance: FileManager = FileManager()
    }
    
    var manager1 = FileManager.sharedInstance
    var manager2 = FileManager.sharedInstance
    var manager3 = FileManager.sharedInstance
    print(Mems.ptr(ofRef: manager1))	// 0x0000600000008030	
    print(Mems.ptr(ofRef: manager2))	// 0x0000600000008030
    print(Mems.ptr(ofRef: manager3))	// 0x0000600000008030
    

Demo1:

movq $0xa, 0x86d1(%rip) num1 的地址为: rip + 0x86d1 = 0x100003b0f + 0x86d1 = 0x10000C1E0

movq $0xb, 0x86ce(%rip) num2 的地址为: rip + 0x86ce = 0x100003b1a + 0x86ce = 0x10000C1E8

movq $0xc, 0x86cb(%rip) num3 的地址为: rip + 0x86cb = 0x100003b25 + 0x86cb = 0x10000C1F0

可以看到 0x10000C1E0 0x10000C1E8 0x10000C1F0 在内存上是连续的间隔8Byte。可见分配的3个全局变量内存是连续的

Demo2

movq $0xa, 0x8b65(%rip) num1 的内存为 rip + 0x8b65 = 0x1000037c3 + 0x8b65 = 0x10000C328

可以看到15行将11赋值给 rax所以直接读取 rax 的地址:0x000000010000c330

movq $0xc, 0x8b38(%rip) num1 的内存为 rip + 0x8b38 = 0x100003800 + 0x8b38 = 0x10000C338

可以看到 0x10000C328 0x10000c330 0x10000C338 也是内存连续的。所以类型属性就是带有访问控制(必须通过类来访问)的全局变量

Demo3

类型属性如何保证线程安全的?如何保证只会初始化一次。

底层会调用 swift_once 进而调用 dispatch_once_tdispatch_once_t 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 dispatch_once_t 保证线程安全和只初始化1次。