Swift :属性-汇编分析inout本质

属性

image-20210406140721732

一: 存储属性

存储属性类似于成员变量,定义方式很简单:

//存储属性
class Person{
    var name: String = "张三"
}

存储属性(Stored Property

  • 类似于成员变量这个概念

  • 存储在实例的内存中

    struct Point {
        var x:Int, y :Int
    }
    var p = Point(x:10,y:20)
    
  • 结构体、类可以定义存储属性

    class Point {
        var x:Int = 10, y :Int = 20
    }
    var p = Point()
    
  • 枚举不可以定义存储属性

    我们知道枚举的内存里面可以存放的是所有的case以及关联值,并没有所谓的成员变量概念,可因此也不存在所谓的存储属性

存储属性存储在成员变量中;结构体Struct和类Class都可以定义存储属性,唯独枚举不可以.因为枚举变量的内存中只用于存储case 值和关联值,没有用来存储存储属性的内存.

另外需要注意的是,在初始化类和结构体的实例时,必须为所有的存储属性设置初始值.

二: 计算属性

image-20210406141757557

计算属性的定义方式需要用到set , get关键字:

本质就是方法(函数

class Circle {
    //存储属性
    var radius: Int = 0
    //计算属性
    var diameter: Int {
        set {
            radius = newValue / 2
        }
        get {
            return radius * 2
        }
    }
}

计算属性

set传入的新值默认叫做newValue,也可以自定义

定义计算属性只能用var, 不能用let

  • let代表常量,也就是值是一成不变的
  • 计算属性的值是可能发生变化的(即使是只读计算属性)

只读计算属性:只有get, 没有set

本质就是方法(函数)这个也可以通过汇编来证明一下

var circle = Circle(radius:5)
circle.radius = 10
circle.diameter = 20
let test = circle.diameter
image-20210406160039359
image-20210406160558637

si 进入TestSwift`Circle.diameter.setter:

image-20210406161134734
image-20210406161600434

枚举rawValue原理

  • 枚举原始值

    rawValue
    

    的本质是:

    只读计算属性

    直接看汇编就可以证明

    img
img
image-20210406161931611

延迟存储属性(Lazy Stored Property)

image-20210406142819776
class Car {
    init() {
        print("Car init")
    }
    func run() {
        print("Car is running!")
    }
}

class Person {
    var car = Car()
    init() {
        print("Person init")
    }
    func  goOut() {
        car.run()
    }
}

let p = Person()
print("-----------")
p.goOut()

运行结果如下

Car init
Person init
-----------
Car is running!
Program ended with exit code: 0

我们给上面代码的car属性增加一个关键字lazy修饰

class Car {
    init() {
        print("Car init")
    }
    func run() {
        print("Car is running!")
    }
}

class Person {
    lazy var car = Car()
    init() {
        print("Person init")
    }
    func  goOut() {
        car.run()
    }
}

let p = Person()
print("-----------")
p.goOut()

再看下现在的运行结果

Person init
-----------
Car init
Car is running!
Program ended with exit code: 0

可以看出,lazy的作用,是将属性var car的初始化延迟到了它首次使用的时候进行,例子中也就是p.goOut()这句代码执行的时候,才回去初始化属性car

通过lazy 关键字修饰的存储属性就要做延迟存储属性,这个功能的好处是显而易见的,因为有些属性可能需要花费很多资源进行初始化,而很可能在某些极少情况下才会被触发使用,所以lazy关键字就可以用在这种情况下,让核心对象的初始化变得快速而轻量。比如下面这个例子

class PhotoView {
    lazy var image: Image = {
        let url = "https://www.520it.com/xx.png"
        let data = Data(url: url)
        return Image(dada: data)
    }()
}

网络图片的加载往往是需要一些时间的,上面例子里面图片的加载过程封装在闭包表达式里面,并且将其返回值作为了image属性的初始化赋值,通过lazy,就讲这个加载的过程推迟到了image在实际被用到的时候去执行,这样就可以提升app顺滑度,改善卡顿情况。

  • 使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化

  • lazy
    

    属性必须是

    var
    

    不能是

    let
    
    • 这个要求很容易理解,let必须在实例的初始化方法完成之前就拥有值,而lazy恰好是为了在实例创建并初始化之后的某个时刻对其某个属性进行初始化赋值,所以lazy只能作用域var属性
  • 如果多线程同时第一次访问lazy属性,无法保证属性只被初始化1

延迟存储属性注意点

image-20210406143035029

因为p是常量,所以内存的内容初始化之后不可以变化,但是p.z会使得结构体Pointlazy var z属性进行初始化,因为结构体的成员是在结构体的内存里面的,因此就需要改变结构体的内存,因此便产生了后面的报错

属性观察器(Property Observer)

image-20210406143823958
  • 可以为非lazyvar存储属性设置属性观察器

  • willSet会传递新值,默认叫做newValue

  • didSet会传递旧值,默认叫做oldValue

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

  • 在属性定义时设置初始值也不会出发willSetdidSet

struct Circle {
    var radius: Double {
        willSet {
            print("willSet", newValue)
        }

        didSet {
            print("didSet", oldValue, radius)
        }
    }

    init() {
        self.radius = 1.0
        print("Circle init!")
    }
}

var circle = Circle()
circle.radius = 10.5
print(circle.radius)

运行结果

Circle init!
willSet 10.5
didSet 1.0 10.5
10.5

全局变量、局部变量

image-20210406143927245

inout

首先看下面的代码

func test(_ num: inout Int) {
    num = 20
}

var age = 10
test(&age) // 此处加断点

将程序运行至断点处,观察汇编

image-20210406172004543

对于上述比较简单的情况,我们知道inout的本质就是进行引用传递,接下来,我们考虑一些更加复杂的情况

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}


func test(_ num: inout Int) {
    num = 20
}

var s = Shape(width: 10, side: 4)
test(&s.width)  // 断点1
s.show()
print("-------------")
test(&s.side)   //断点2
s.show()
print("-------------")
test(&s.girth)  //断点3
s.show()
print("-------------")
getGirth
width= 20, side= 4, girth= 80
-------------
willSetSide 20
didSetSide 4 20
getGirth
width= 20, side= 20, girth= 400
-------------
getGirth
setGirth 20
getGirth
width= 1, side= 20, girth= 20
-------------

看得出来,inout对于三种属性都产生了作用,那么它的底层到底是如何处理和实现的呢?我们还是要通过汇编来一探究竟。便于汇编分析,我们截取部分代码进行编译运行

首先看普通的属性

var s = Shape(width: 10, side: 4)
test(&s.width) // 断点处,传入普通属性width作为test的inout参数

汇编结果如下

image-20210406173730343

所以对于普通的存储属性test函数是直接将它的地址值传入。

接下来便于直观的对比,我们再看一下计算属性的情况

var s = Shape(width: 10, side: 4)
test(&s.girth)
image-20210406175450430
img

可以看出,由于计算属性在实例内部没有对应的内存空间,编译器通过在函数栈里面==开辟一个局部变量的方法==,利用它作为计算属性的值的临时宿主,并且将该局部变量的地址作为test函数的inout参数传入函数,所以本质上,仍然是引用传递

test函数调用前,计算属性值给复制到局部变量上,以及test函数调用之后,局部变量的值传递给setter函数的这两个过程,被苹果成为 Copy In Copy Out,上面案例代码的运行结果也验证了这个结论

来看对于带有属性观察器的存储属性,处理过程会有哪些独到之处

var s = Shape(width: 10, side: 4)
test(&s.side) //side是带属性观察期的存储属性, 断点在这里

汇编结果如下

image-20210406201334736

这次,我们发现跟计算属性有些类似,这里也用到了函数栈的局部变量,它的作用是用来承载计算属性的值,然后被传入test函数的同样是这个局部变量的地址(引用),但是我很好奇为何要多此一举,计算属性因为本身没有固定的内存,所以很好理解必须借助局部变脸作为临时宿主,但是计算属性是有固定内存的,可以猜的到,这么设计的原因肯定跟属性观察器有关,但是目前的代码还不足以解释这么设计的意图,但是我们看到这里最后一步,调用了side.setter函数,side是存储属性,怎么会有setter函数呢?那我们就进入它内部看看喽,它的汇编如下

image-20210406202633608

原来,这个side的两个属性观察器willSetdidSet被包裹在了这个setter函数里面,而且,对于属性side的赋值真正发生在这个setter函数里面。

因此我们看出了一个细节,属性side内存里的值被修改的时间点,是在test函数之后,也就是这个setter函数里,也就是test函数其实并没有修改side的值。

因为test函数的功能拿到一段==main函数栈变量临时内存==,并且修改里面的值,如果当前我们将side的地址提交给test,除了能够修改side内存里值以外,它是无法触发side的属性观察器的。所以看得出局部变量以及setter函数出现在这里的意义就是为了能够去触发属性side的属性观察器。因为我们使用了局部变量,因此对于带有属性观察器的存储属性,也可以说inout对其采用了Copy In Copy Out的做法。

通过程序运行之后的输出结果,也可以验证我们已上的结论

开始test函数
willSetSide 20
didSetSide 4 20
Program ended with exit code: 0

inout的本质总结

  • 如果实参有物理内存地址,且没有设置属性观察器
    则直接将实参的内存地址传入函数(实参进行引用传递
  • 如果实参是计算属性 或者 设置了属性观察器
    则采取了 Copy In Copy Out的做法
    • 调用该函数时,先复制实参的值,产生副本【可以理解成get操作】
    • 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
    • 函数返回后,再将副本的值覆盖实参的值【可以理解成set操作】

总结:inout的本质就是引用传递(地址传递)

类型属性(Type Property)

image-20210406150833805
  • 严格来说,属性可以划分为:
    • 实例属性(Instance Property):只能通过实例去访问
      • 存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有一份
      • 计算实例属性(Computed Instance Property):
    • 类型属性(Type Property):只能通过类型去访问
      • 存储类型属性(Stored Type Property):整个程序的运行过程中,就只有一份内存,它的本质就是全局变量
      • 计算类型属性(Computed Type Property
  • 可以通过static定义类型属性,对于类来说,还可以用关键字class

类型属性细节

image-20210406151024645

不同于存储实例属性,你必须给存储类型属性设定初始值
因为类型没有像实例那样的init初始化器来初始化存储属性

img

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

  • 就算被多个线程同时访问,保证只会初始化一次,可以保证线程安全(系统底层会有加锁处理)
  • 存储类型属性可以时let,因为这里压根不存在实例初始化的过程

枚举类型也可以定义类型属性(存储类型属性计算类型属性

单例模式

public class FileManager {
    
    public static let shared = FileManager()
    
    private init(){
        
    }
}
  • public static let shared = FileManager()
    
    • 通过static定义了一个类型存储属性
    • public确保在任何场景下,外界都能访问,
    • let保证了FileManager()只会被赋值给shared一次,并且确保了线程安全,也就是说init()方法只会被调用一次,这样就确保FileManager只会存在唯一一个实例,这就是Swift中的单例
  • private init()private确保了外界是无法手动调用FileManager()来创建实例,因此通过shared属性得到的FileManager实例永远是相同的一份,这也符合了我们对与单例的要求。

你可能感兴趣的:(Swift :属性-汇编分析inout本质)