Swift进阶-属性

Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析

一、存储属性

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。

class Teacher { 
      let age: Int 
      var name: String 

      init(_ age: Int, _ name: String) { 
        self.age = age 
        self.name = name 
      }
 }

struct Student { 
      let age: Int 
      var name: String
 }

let 和 var 的区别:

从定义上:

  • let 用来声明常量,常量的值一旦设置好便不能再被更改;
  • var 用来声明变量,变量的值可以在将来设置为不同的值。

汇编角度:
看不出有什么不同的,无非把值赋值给对应的寄存器,大可自行调试看看。
开启汇编模式:Debug -> Debug Workflow -> Always Show Disassembly

var age = 18
let name = 20
print("断点打在这,运行")

从 SIL的角度:

swiftc xxx.swift -emit-sil // 生成sil(已优化)
sil的常量变量声明部分

存储属性在编译的时候,编译器默认会合成get/set方式,而我们访问/赋值 存储属性的时候,实际上就是调用get/set

let声明的属性默认不会提供setter

二、计算属性

类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 gettersetter 来修改和获取值。

计算属性必须定义为变量计算属性时候必须包含类型,因为编译器需 要知道期望返回值是什么。

struct Square {
    var width: Double // 存储属性,在实例中Double占据8字节
    var area: Double{ // 计算属性,不占据内存空间
        get {
            return width * width
        }
        set {
            self.width = newValue
        }
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        var s = Square(width: 10.0)
        s.area = 30.0 //断点打在着,看汇编
    }
}

看看汇编给计算属性赋值是个什么过程

image.png

可以看到这是一个静态调用,按住 control 点击下一步进去

image.png

给计算属性赋值,其实是调用setter

来看看sil的Square声明

image.png

计算属性area的本质就是getter和setter,不一样的,不占用实例的内存。

以下这种声明是存储属性:

struct Square {
    var width: Double
    private(set) var area: Double = 10.0
}
sil

虽然有set方法,但是超过了类作用域,外界是访问不到的。

三、属性观察者

属性观察者会观察用来观察属性值的变化:
willSet 当属性将被改变调用,即使这个值与 原有的值相同;
didSet 在属性已经改变之后调用。

class SubjectName{
    // 它是存储属性
    var subjectName: String = "" {
        willSet{
            print("subjectName will set value \(newValue)")
        }
        didSet{
            print("subjectName has been changed \(oldValue)")
        }
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        let n = SubjectName()
        n.subjectName = "wj"
    }
}
sil

在赋值的前后去调用willSetdidSet方法。

class SubjectName{
    // 它是存储属性
    var subjectName: String = "" {
        willSet{
            print("subjectName will set value \(newValue)")
        }
        didSet{
            print("subjectName has been changed \(oldValue)")
        }
    }

    init(_ subjectName: String) {
        self.subjectName = subjectName
    }
}
sil

在初始化期间设置属性时不会调用 willSetdidSet 观察者。

继承关系demo:

class Teacher {
    var name: String {
        willSet {
            print("will set newValue: \(newValue)")
        }
        didSet {
            print("did set oldValue \(oldValue)")
        }
    }
    
    init(name: String) {
        self.name = name
    }
}

class ParTimeTeacher: Teacher {
    override var name: String {
        willSet {
            print("override will set newValue: \(newValue)")
        }
        didSet {
            print("override did set oldValue \(oldValue)")
        }
    }
    
    override init(name: String) {
        super.init(name: name)
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        let t = ParTimeTeacher(name: "未知")
        t.name = "林老师"
    }
}

打印结果:

override will set newValue: 林老师   // 子类will set
will set newValue: 林老师            // 父类will set
did set oldValue 未知                // 父类 did set
override did set oldValue 未知       // 子类did set
sil

实际上ParTimeTeacher实例调用getter/setter实际就是调用父类Teachergetter/setter

sil

四、延迟存储属性

  • 必须有初始值
  • 延迟存储属性的初始值是在它第一次被使用时才进行计算;
  • 用关键字 lazy 来标识一个延迟存储属性。
image.png

在没有第一次访问延迟存储属性时,它的值是0

image.png

访问过一次后,它才会有值。
再去看看sil:

sil

所以苹果做到懒加载属性第一次访问前没有值,访问后就有值?
本质就是在编译后生成一个可选型存储属性

iinitialization

在初始化的时候给的是 nil

getter

拿到生成的属性 $__lazy_storage_$_name的地址,然后去switch:如果有值,走bb1的代码块;如果没有值走bb2代码块

image.png

bb2没有加载过:去构建初始值,给到变量$__lazy_storage_$_name
bb1已加载过:已经有值了,直接给他返回出去

setter

没什么好说的,给属性$__lazy_storage_$_name赋值

五、类型属性

  • 类型属性本质上就是一个全局变量
  • 类型属性只会被初始化一次
class Teacher {
    static var name: String = "未知"
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        Teacher.name = "林老师"
    }
}
sil

编译后的sil里多了两个全局变量
一个是@$s14ViewController7TeacherC4name_Wz - token
一个是@$s14ViewController7TeacherC4nameSSvpZ - Teacher.name

看看viewDidLoad里面

viewDidLoad

做了一个全局变量内存地址的转换,它是怎么转换的呢?
搜一下这个函数名 @$s14ViewController7TeacherC4nameSSvau

image.png
初始化Teacher.name

builtin "once"在sil降级之后,其实底层调用的就是swift_once,而swift_once的源码底层调用的就是GCD的dispatch_once

swift_once
单例的写法
class Teacher {
    static let share = Teacher()
    private init() {}
}
单例sil

六、属性在Mach-O文件的位置信息

上一篇文章介绍了Metadata的元数据结构

struct Metadata { 
      var kind: Int 
      var superClass: Any.Type 
      var cacheData: (Int, Int) 
      var data: Int 
      var classFlags: Int32 
      var instanceAddressPoint: UInt32 
      var instanceSize: UInt32 
      var instanceAlignmentMask: UInt16 
      var reserved: UInt16 
      var classSize: UInt32 
      var classAddressPoint: UInt32 
      var typeDescriptor: UnsafeMutableRawPointer 
      var iVarDestroyer: UnsafeRawPointer
 }

typeDescriptor是记录类的描述:

struct TargetClassDescriptor{ 
      var flags: UInt32 
      var parent: UInt32 
      var name: Int32   // class/struct/enum 的名称
      var accessFunctionPointer: Int32 
      var fieldDescriptor: Int32  // 属性描述
      var superClassType: Int32 
      var metadataNegativeSizeInWords: UInt32 
      var metadataPositiveSizeInWords: UInt32 
      var numImmediateMembers: UInt32 
      var numFields: UInt32 
      var fieldOffsetVectorOffset: UInt32 
      var Offset: UInt32 
      var size: UInt32 
      // V-Table  (methods) 
}

其中 fieldDescriptor 记录了当前的属性信息,经分析后fieldDescriptor 在源码中的数据结构如下:

struct FieldDescriptor { 
    MangledTypeName int32 // 混写后的类型名称
    Superclass      int32 
    Kind            uint16 
    FieldRecordSize uint16 
    NumFields       uint32 // 属性个数
    FieldRecords [FieldRecord]  // 记录了每个属性的信息
 }

其中 NumFields 代表当前有多少个属性, FieldRecords 记录了每个属性的信息, FieldRecords 的结构体如下:

struct FieldRecord{ 
    Flags           uint32 // 标志位
    MangledTypeName int32 // 属性的类型名称
    FieldName       int32 // 属性名称
 }

了解完数据结构之后,现在从Mach-O来分析属性在Mach-O文件的位置信息

main.swift 声明一个对象

class Teacher {
    var age = 18
    var age1 = 20
}

编译后,找到其可执行文件exe,将其拖拽到 MachOView 软件中

__TEXT,__swift5_types

在data区找到__TEXT,__swift5_types 这是记录所有的 struct/enum/类的Descriptor的信息,现在我们只有一个Teacher类
可以拿到 Teacher类的DescriptorMach-O 里的位置:

0xFFFFFE28 + 0x3ED8 = 0x100003D00

所以Dscriptor在mach-o的data区的偏移量,需要再减去虚拟内存地址:

0x100003D00 - 0x100000000 = 0x3D00

然后再data区找到 __TEXT,__const里边,去找0x3D00的位置:

__TEXT,__const

接下来要找到 fieldDescriptor它的地址,因为fieldDescriptorTargetClassDescriptor的数据结构前面还有4个UInt32的成员:

struct TargetClassDescriptor{ 
      var flags: UInt32 
      var parent: UInt32 
      var name: Int32   // class/struct/enum 的名称
      var accessFunctionPointer: Int32 
      var fieldDescriptor: Int32  // 属性描述
      var superClassType: Int32 
      var metadataNegativeSizeInWords: UInt32 
      var metadataPositiveSizeInWords: UInt32 
      var numImmediateMembers: UInt32 
      var numFields: UInt32 
      var fieldOffsetVectorOffset: UInt32 
      var Offset: UInt32 
      var size: UInt32 
      // V-Table  (methods) 
}

所以还需要向后偏移4个4字节:

fieldDescriptor在Mach-O的偏移信息

得到的位置也就是fieldDescriptorMach-O的偏移信息
求出fieldDescriptorMach-O的的信息:

0x3D10 + 0x1A0 = 0x3EB0

而属性存放在data区的 __TEXT,__swift5_fieldmd,并在上面找到0x3EB0:

__TEXT,__swift5_fieldmd

从0x3EB0开始后面的就是 FieldDescriptor属性描述的成员内容,而我要找到 FieldRecords [FieldRecord]

struct FieldDescriptor { 
    MangledTypeName int32 // 混写后的类型名称
    Superclass      int32 
    Kind            uint16 
    FieldRecordSize uint16 
    NumFields       uint32 // 属性个数
    FieldRecords [FieldRecord]  // 记录了每个属性的信息
 }

就要往后偏移4个4字节(其中有2个uint16,3个int32):

FieldRecords

因为我们只有一个Teacher类,这后面的连续的存储空间就是 FieldRecords数组的内容:

FieldRecord 的数据结构:

struct FieldRecord{ 
    Flags           uint32 // 标志位
    MangledTypeName int32 // 属性的类型名称
    FieldName       int32 // 属性名称
 }
`FieldRecord` 对应成员的位置

我这里画出找到 第一个FieldRecord 对应成员的位置
我拿到 第一个FieldRecordFieldName属性名称在Mach-O上的信息

0x3EC0 + 0x8 + 0xFFFFFFDF = 0x100003EA7

需要减去虚拟内存地址:

0x100003EA7 - 0x100000000 = 0x3EA7

再去找data区中的 __TEXT,__swift5_reflstr找到0x3EA7:

__TEXT,__swift5_reflstr

直接可以找到Teacher类的第一个成员变量age的16进制

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

推荐阅读更多精彩内容