Swift基础7(汇编分析属性)

Swift中跟实例相关的属性可以分为2大类

  • 1、存储属性
    • 类似于成员变量这个概念
    • 存储在实例的内存中
    • 结构体,类可以定义存储属性
    • 枚举不可以定义存储属性
  • 2、计算属性
    • 本质就是一个函数
    • 不占用实例的内存
    • 枚举,结构体,类都可以定义计算属性
struct Circle {
    var radius:Double
    var diameter:Double{
        set{
            radius = newValue / 2
        }
        get{
            radius * 2
        }
    }
}
 

print(MemoryLayout<Double>.size) //8
print(MemoryLayout<Circle>.size) //8

radius是存储属性,diameter是计算属性,我们打印Circle的内存大小发现和Double一样

存储属性

关于存储属性,Swift有一个明确的规定

  • 在创建类或者结构体实例时,必须为所有的存储属性设置一个合适的初始值
    • 可以在初始化器里为存储属性设置一个初始值
    • 可以分配一个默认的属性值作为属性定义的一部分

计算属性

除了存储属性,类、结构体和枚举也能够定义计算属性,而它实际并不存储值。相反,他们提供一个读取器和一个可选的设置器来间接得到和设置其他的属性和值

struct Circle {
    var radius:Double
    var diameter:Double{
        set{
            radius = newValue / 2
        }
        get{
            radius * 2
        }
    }
}
 

diameter就是一个简单的计算属性,如果一个计算属性的设置器没有为将要被设置的值定义一个名字,那么他将被默认命名为 newValue

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

  • 定义计算属性只能使用var,不能使用let
  • let代表常量:值是不变的
  • 计算属性的值是可能发生变化的(即使是只读属性)
struct Circle {
    var radius:Double
    var diameter:Double{
        get{
            radius * 2
        }
    }
}

var c = Circle(radius: 10)  
print(c.diameter)   //20
c.radius = 5
print(c.diameter) //10

枚举rawValue的原理

我们研究发现,其实枚举原始值中rawValue的本质就是:只读计算属性

enum TestEnum:Int {
    case test1 = 1, test2 = 2, test3 = 3
}

print(TestEnum.test1.rawValue) //1

对于这样的我们打印就是1

enum TestEnum:Int {
    case test1 = 1, test2 = 2, test3 = 3
    var rawValue:Int{
        switch self {
        case .test1:
            return 10
        case .test2:
            return 20
        case .test3:
            return 30
        }
    }
}
print(TestEnum.test1.rawValue)  //10

对于这个打印就是10

延迟存储属性

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

你必须把延迟存储属性声明为变量(使用 var 关键字),因为它的初始值可能在实例初始化完成之前无法取得。常量属性则必须在初始化完成之前有值,因此不能声明为延迟

如果多条线程同时第一次访问lazy属性,无法保证属性只能被初始化一次

class PhotoView {
    lazy var image:UIImage = {
        let url = "xxx.png"
        let data = Data(url)
        return UIImage(data:data)
    }
}

当结构体包含一个延迟存储属性时,只有var才能访问演出属性

  • 因为延迟属性初始化时需要改变结构体的内存
延迟属性.png

属性观察器(Property Observer)

属性观察者会观察并对属性值的变化做出回应。每当一个属性的值被设置时,属性观察者都会被调用,即使这个值与该属性当前的值相同

你可以为你定义的任意存储属性添加属性观察者,除了延迟存储属性。你也可以通过在子类里重写属性来为任何继承属性(无论是存储属性还是计算属性)添加属性观察者

你不需要为非重写的计算属性定义属性观察者,因为你可以在计算属性的设置器里直接观察和相应它们值的改变。

  • willSet 会在该值被存储之前被调用。
  • didSet 会在一个新值被存储后被调用。

如果你实现了一个 willSet 观察者,新的属性值会以常量形式参数传递。你可以在你的 willSet 实现中为这个参数定义名字。如果你没有为它命名,那么它会使用默认的名字newValue

同样,如果你实现了一个 didSet观察者,一个包含旧属性值的常量形式参数将会被传递。你可以为它命名,也可以使用默认的形式参数名 oldValue 。如果你在属性自己的 didSet观察者里给自己赋值,你赋值的新值就会取代刚刚设置的值。

  • 初始化器中设置属性值的时候不会触发willSet 和 didSet
  • 在属性定义时设置初始值也不会触发willSet 和 didSet
struct Circle {
    var radius:Double{
        willSet{
            print("willSet",newValue)
        }
        didSet{
            print("didSet",oldValue)
        }
    }
    
}

inout再次研究

struct Shape {
    var width:Int
    var side:Int {
        willSet{
            print("willSet",newValue)
        }
        didSet{
            print("didSet",oldValue)
        }
    }
    
    var grith:Int{
        set{
            width = newValue / side
            print("setGrith",newValue)
        }
        get{
            print("getGrith")
            return width * side
        }
    }
    
    func show() {
        print("width:\(width)--side:\(side)--grith:\(grith)")
    }
    
}



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

var s = Shape(width: 10, side: 4)
s.show()
print("-------")

test(&s.width)
s.show()
print("-------")


test(&s.grith)
s.show()
print("-------")

test(&s.side)
s.show()
print("-------")

我们对这个进行分析

1、在test(&s.width)处打断点

struct Shape {
    var width:Int
    var side:Int {
        willSet{
            print("willSet",newValue)
        }
        didSet{
            print("didSet",oldValue)
        }
    }
    
    var grith:Int{
        set{
            width = newValue / side
            print("setGrith",newValue)
        }
        get{
            print("getGrith")
            return width * side
        }
    }
    
    func show() {
        print("width:\(width)--side:\(side)--grith:\(grith)")
    }
    
}

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

var s = Shape(width: 10, side: 4)
test(&s.width)

想要具体的分析,我们可以查看汇编实现。提前需要了解的知识点

  • 1、汇编中callq表示函数的调用,我们先找到callq 0x1000025a0 ; 属性.test(inout Swift.Int) -> () 这句话,表示调用函数test

  • 2、leaq 0x1abe(%rip), %rdi ; 属性.s : 属性.Shape我们向上看一句,这一句表示传递参数

    • 在汇编语言中rdi、rsi、rdx、rcx、r8、r9等寄存器常用于存放函数参数,这里我们找到了rdi
    • leaq 0x1abe(%rip), %rdi在汇编语言中的意思就是:将0x1abe(%rip)这个地址值赋值给了rdi
    • leaq 0x1abe(%rip), %rdi ; 属性.s : 属性.Shape我们可以看到后面的注释,是把s的地址给了rdi
    • 我们知道结构体第一个属性的地址值就是结构体的地址,所有rip就是存放的width的地址

2、在test(&s.grith)处打断点

struct Shape {
    var width:Int
    var side:Int {
        willSet{
            print("willSet",newValue)
        }
        didSet{
            print("didSet",oldValue)
        }
    }
    
    var grith:Int{
        set{
            width = newValue / side
            print("setGrith",newValue)
        }
        get{
            print("getGrith")
            return width * side
        }
    }
    
    func show() {
        print("width:\(width)--side:\(side)--grith:\(grith)")
    }
    
}

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

var s = Shape(width: 10, side: 4)
test(&s.grith)

我们知道inout本质就是引用传递,而在上面我们知道计算属性本质就是一个函数s.grith没有内存地址,这里为什么不报错呢?我们先来看一下打印结果

getGrith
test
setGrith 20

我们先大概猜测一下实现原理

  • 1、首先调用get方法,他会返回一个临时的存储空间,也就是局部变量
  • 2、把临时的存储空间的地址值传递到test函数中去,test函数能够拿到临时的存储空间,然后把20放到临时的存储空间中
  • 3、把20传到set方法的newValue

我们来看一下汇编实现

【需要了解的小知识】

  • 内存地址格式为:0X4dbc(%rip)、一般是全局变量、在数据段
  • 内存地址格式为:-0X78(%rbp)、一般是局部变量,栈空间
  • 内存地址格式为:0X10(%rax)、一般是堆空间
  • 1、我们来看movq %rax, -0x28(%rbp),这个是第24行代码,意思就是把get方法的返回值放到-0x28(%rbp)的局部变量中
  • 2、第25-26行leaq -0x28(%rbp), %rdi-0x28(%rbp)赋值给%rdi,然后把%rdi作为参数传递到test方法中
  • 3、movq -0x28(%rbp), %rdi ,取-0x28(%rbp)的内存地址值给%rdi,然后把%rdi传递给set方法

3、在test(&s.side)处打断点

struct Shape {
    var width:Int
    var side:Int {
        willSet{
            print("willSet",newValue)
        }
        didSet{
            print("didSet",oldValue)
        }
    }
    
    var grith:Int{
        set{
            width = newValue / side
            print("setGrith",newValue)
        }
        get{
            print("getGrith")
            return width * side
        }
    }
    
    func show() {
        print("width:\(width)--side:\(side)--grith:\(grith)")
    }
    
}

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

var s = Shape(width: 10, side: 4)
test(&s.side)
inout3.png
inout4.png

inout本质总结

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

**inout的本质就是引用传递(地址传递)**

类型属性

例属性是属于特定类型实例的属性。每次你创建这个类型的新实例,它就拥有一堆属性值,与其他实例不同。

你同样可以定义属于类型本身的属性,不是这个类型的某一个实例的属性。这个属性只有一个拷贝,无论你创建了多少个类对应的实例。这样的属性叫做类型属性。

类型属性在定义那些对特定类型的所有实例都通用的值的时候很有用,比如实例要使用的常量属性(类似 C 里的静态常量),或者储存对这个类型的所有实例全局可见的值的存储属性(类似 C 里的静态变量)。

存储类型属性可以是变量或者常量。计算类型属性总要被声明为变量属性,与计算实例属性一致。

注意
不同于存储实例属性,你必须总是给存储类型属性一个默认值。这是因为类型本身不能拥有能够在初始化时给存储类型属性赋值的初始化器。
存储类型属性是在它们第一次访问时延迟初始化的。它们保证只会初始化一次,就算被多个线程同时访问,他们也不需要使用 lazy 修饰符标记。

严格来说,属性可以分为

  • 1、实例属性:只能通说实例去访问
    • 计算属性:存储在实例的内存中,每个实例都有一份
    • 存储属性
  • 2、类属性:只能通过类型去访问
    • 存储属性:整个程序运行中,就只有1份
    • 计算属性

类属性可以通过static定义属性,

类型属性细节

  • 1、不同于存储实例属性,你必须给存储类型属性设置初始值,因为类型没有像实例那样有init方法
  • 2、存储类型属性默认就是lazy会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次

下面我们证明两个问题

  • 1、类属性是全局变量
  • 2、类属性实例化的时候调用了dispath_once

类属性是全局变量

测试1

我们先来研究直接赋值的 num1 num2 num3

var num1 = 10
var num2 = 11
var num3 = 12
print("-------")

我们在print出打断点,然后进入汇编

类属性是全局变量1.png

我们计算出地址值:

num1  0x100001188
num2  0x100001190
num3  0x100001198

是一段连续的栈空间地址。

测试2

接下来我们把num2设置为类属性

struct Test {
    static var num = 11
}
var num1 = 10
var num2 = Test.num
var num3 = 12
print("----")

我们在print出打断点,然后进入汇编

类属性是全局变量.png

我们计算出地址值:

num1 0x1000021D0
num2 0x1000021D8
num3 0x1000021E0

也是一段连续的栈空间地址。

struct Test {
     var num = 11
}
var num1 = 10
var num2 = Test().num
var num3 = 12
print("----")

测试3

我们把num2设置为实例属性

struct Test {
     var num = 11
}
var num1 = 10
var num2 = Test().num
var num3 = 12
print("----")
类属性是全局变量3.png

我们来打印一下num2 num3的地址值

num2 0x000000000000000b
num3 0x1000021A8 

我们可以看出实例属性是在堆空间

类属性实例化的时候调用了dispath_once

struct Test {
     static var num = 11
}
var num1 = Test.num

我们在static var num = 11处打断点

dispath_once1.png

我们在swift_once处打断点,然后si进入,一直si,知道进入到

dispath_once2.png

在上面汇编中的最后一句话打一个断点,继续si

dispath_once3.png

最终找到了dispath_once,类属性保持只创建了一次就是调用了dispath_once方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容