swift属性

186 阅读6分钟

一、存储属性

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

class LGTeacher{
    var age: Int
    var name: String
}

比如这里的 age 和 name 就是我们所说的存储属性,这里我们需要加以区分的是 let 和 var 两者的区别:从定义上:

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

结构体

image.png

class image.png

let 和 var 的区别:

从汇编的角度看


var age = 19
let x = 20

image.png

从sil角度 let和var两个全局的变成了存储属性, let只有get方法,没有set方法,本质上let和var也是一种语法糖

image.png

private(set) var age = 19

sil代码如下

@_hasStorage @_hasInitialValue private(set) var age: Int { get set } 不改变存储属性,只是私有了set方法,相当于对外是只读的,在声明的范围是不影响的,set get都可以用 image.png

二、计算属性

image.png 错误

存储的属性是最常见的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不 存储值,他们提供 getter 和 setter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需 要知道期望返回值是什么。

正确

class NBBankClass {
    var age = 18
    let name = ""
    
    var address:String {
        get{
            return "ningbo"
        }
        set {
            self.name = newValue
        }
    }
}

获取对象的内存(证明计算属性不占用内存)

# Swift5中的枚举、结构体和类在内存中的布局

# 40_iOS干货27_查看OC对象占用至少多少字节的方法

class NBCBBank{
    var age :Int = 18
    var name:Int = 20
    var address:String {
        set{
            self.age = 20
        }
        get{
            return "NINGBO"
        }
    }
    let tex = ""
    func test() {
        print("准备调用")
        var bank = NBCBBank()
        //对象实际大小
        print(class_getInstanceSize(NBCBBank.self))
        //开辟的内存大小
        print(malloc_size(Unmanaged.passUnretained(bank).toOpaque()))
    }
}

对bank的memory操作只是得到栈区指针,不是堆区对象的大小,对象的对齐是16字节 image.png

image.png

image.png

image.png sil中间代码

class NBBankClass {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue final let name: String { get }
  var address: String { get set }  计算属性没有@_hasStorage的标识
  @objc deinit
  init()
}

结构体中证明计算属性不占内存

存储属性占用内存 image.png

2个存储属性 16字节 image.png 在增加计算属性 不会变化

image.png

三、属性观察者

原类中的属性观察者

class NBBankClass {
    var age = 18 {
        willSet{
            print("age will set value \(newValue)")
        }
        
        didSet {
            print("age has been changed \(oldValue)")
        }
    }
   
    func test(){
        self.age = 20
    }
}

属性观察者会观察用来观察属性值的变化,一个 willSet 当属性将被改变调用,即使这个值与 原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 getter 和 setter。

image.png 查看sil代码 image.png

可以看到原来 都是在 setter 方法中先调用willset 再赋值 最后调用 didset 。

这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSet 和 didSet 观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这 段代码,你会发现当前并不会有任何的输出。

class NBBankClass {
    var age = 18 {
        willSet{
            print("age will set value \(newValue)")
        }
        
        didSet {
            print("age has been changed \(oldValue)")
        }
    }
    init(_ age:Int){
        self.age = age
    }
}

image.png

上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需 将相关代码添加到属性的 setter

var age = 18 {
        willSet{
            print("age will set value \(newValue)")
        }
        
        didSet {
            print("age has been changed \(oldValue)")
        }
    }
    
    var address:String {
        set{
            //我即将修改xx
             age = 10
            //我修改完了
        }
        
        get{
            return "ningbo"
        }
    }

子类中的属性观察者

image.png

image.png

顺序是 子类中属性的willset -> 父类中属性的willset -> 父类中属性的didset -> 子类中属性的didset

四、延迟存储属性

  • 用关键字 lazy 来标识一个延迟存储属性,必须有初始值。
class NBBankClass {
    lazy var age :Int = 18
}
  • 延迟存储属性的初始值在其第一次使用时才进行计算。

    @_hasStorage @_hasInitialValue final var __lazy_storage__age: Int? { get set } 可选项

// NBBankClass.age.getter
sil hidden [lazy_getter] [noinline] @$s8NBCBBank11NBBankClassC3ageSivg : $@convention(method) (@guaranteed NBBankClass) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $NBBankClass):
  debug_value %0 : $NBBankClass, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $NBBankClass, #NBBankClass.$__lazy_storage_$_age // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  %4 = load %3 : $*Optional<Int>                  // user: %6
  end_access %3 : $*Optional<Int>                 // id: %5
  switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  br bb3(%7 : $Int)                               // id: %9

bb2:                                              // Preds: bb0
  %10 = integer_literal $Builtin.Int64, 18        // user: %11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  %14 = ref_element_addr %0 : $NBBankClass, #NBBankClass.$__lazy_storage_$_age // user: %15
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  store %13 to %15 : $*Optional<Int>              // id: %16
  end_access %15 : $*Optional<Int>                // id: %17
  br bb3(%11 : $Int)                              // id: %18

// %19                                            // user: %20
bb3(%19 : $Int):                                  // Preds: bb2 bb1
  return %19 : $Int                               // id: %20
} // end sil function '$s8NBCBBank11NBBankClassC3ageSivg'

switch_enum %4 : $Optional, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

从地址 取值之前先判断是否为空,为空的话就去走bb2 去创建赋值,不为空就直接返回当前的值

从lldb查看

image.png 使用之后

image.png

Version:0.9 StartHTML:0000000105 EndHTML:0000000302 StartFragment:0000000141 EndFragment:0000000262

五、类型属性

class NBBankClass {
    static var age :Int = 18
}
  • 类型属性其实就是一个全局变量

sil中间代码

 @_hasStorage @_hasInitialValue `static` var age: Int { get set }

// one-time initialization token for age
sil_global private @$s8NBCBBank11NBBankClassC3age_Wz : $Builtin.Word

// static NBBankClass.age
sil_global hidden @$s8NBCBBank11NBBankClassC3ageSivpZ : $Int
  • 类型属性只会被初始化一次

image.png

swiftc -emit-ir 转为IR语法 builtin once 初始化一次

image.png swift_once是什么呢?其实就是我们gcd里面的dispatch_once 只执行一次

image.png

单利的实现

class NBCBBankClass {
    
    static let sharedInstance = NBCBBankClass()
    
    // 指定初始化器私有化,外界访问不到
    private init(){}
}

NBCBBankClass.sharedInstance

六、在macho中定位到属性的地址

第一篇文章我们讲了类的数据结构

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在第二篇文章也有 ,这里面记录了 V-Table 的相关信息,接下来我们需要认识一下 typeDescriptor 中的 fieldDescripto

struct TargetClassDescriptor{ 
    var flags: UInt32 
    var parent: UInt32 
    var name: Int32 
    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 
}

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 
}

题外话:

写博客的时候鼠标触摸不小心让网页突然变大,谷歌网页整体的缩放都正常,只有当前的页面有问题。 解决方式: 1 、用 Magic Mouse 的,在 Mouse 表面轻触两下,不是按下两下,是触摸两下,不能让机械按键动作,放大和缩小都是这样操作; 2 、机身触摸板,用两手指同时轻触两下,也是不让机械按键动作,放大和缩小都是一样操作。