Swift —— 属性

Swift —— 属性,第1张

Swift —— 属性 1. 存储属性1.1 let 和 var 的区别代码角度汇编角度SIL角度 2. 计算属性3. 属性观察者4. 延迟存储属性5. 类型属性6. 属性在Mahco文件的位置信息

1. 存储属性

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特 别要强调的,因为随处可⻅。 比如这里的 age 和 name 就是我们所说的存储属性,这里我们需要加以区分的是 let 和 var 两者的区别:从定义上: let 用来声明常量,常量的值一旦设置好便不能再被更改; var 用来声明变量,变量的值可以在将来设置为不同的值。

1.1 let 和 var 的区别 代码角度
class LGTeacher{
    var age: Int
    var name: String
}

这里我们来看几个案例:

class LGTeacher{
    let age: Int
    var name: String
    init( age: Int,  name: String){
        self.age = age
        self.name = name }
}
struct LGStudent{
    let age: Int
    var name: String
    
}
let t = LGTeacher(age: 18, name: "Hello")
t.age = 20
t.name = "Logic"
t = LGTeacher(age: 30, name: "Kody")
var t1 = LGTeacher(age: 18, name: "Hello")
t.age = 20
t.name = "Logic"
t = LGTeacher(age: 30, name: "Kody")
let s = LGStudent(age: 30, name: "Kody")
s.age = 25
s.name = "Doman"
s = LGStudent(age: 30, name: "Kody")
var s1 = LGStudent(age: 30, name: "Kody")
s.age = 25
s.name = "Doman"
s = LGStudent(age: 30, name: "Kody")

从这里可以看到,对于类t来说,t.age是不能被修改的,t.name是可以被修改的,t是不能被修改的。所以这里可以知道,let修饰的变量,只确保当前变量的值是不能够被修改的。这里t存的是实例对象的内存地址,这个地址是不能在变得,所以当在给t重新赋值一个实例对象的时候, 相当于修改了t的值,所以这里是不允许的。age是用let修饰的,所以这里也是不能修改的。

而对于结构体s来说,对于值类型来说,当前的age和name都是不能被修改的,因为s里面存储的就是age和name的值,修改age和name就相当于修改s,由于s是用let声明,所以都不能修改。

汇编角度

从汇编角度来看var 和 let。

  var age = 18

   let x = 20

从这里可以知道var 和 let在汇编角度是没有什么区别的。

SIL角度

再到SIL里面查看var和let 的区别。
这里可以看到两者都是存储属性,都有初始值。但是这里不同的是,age是有setter 的,而x是没有setter的。本质上,对于存储属性来说,当前的编译器都会给存储属性默认合成setter和getter,访问使用getter,赋值使用setter。而对于let来说,是不生成setter的,而无法对x进行赋值也是因为无法找到其的setter。所以从SIL角度来说,var 和 let 也是一种语法糖。

2. 计算属性

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

struct square{
    var width: Double
    
    var area: Double{
        get {
            return width * hegith
        }
        set {
            self.width = newValue
        }   
    }
}

在SIL中查看,发现setter中的 newValue是编译器自动生成的。

这里也可以修改newValue的名字。这里注意setter里面不能给self.area赋值,否则会造成死循环。

 set(newArea){
            self.width = newArea
        }

接下来看一下只读的计算属性和let属性的区别。

struct square{
    var width: Double = 30
    
    var area: Double{
        get {
            return width * width
        }
    }
    
    let height: Double = 20
}

这里明显看到area不是存储属性,area和height都没有setter。但是,area和height本质是不一样的,因为一个要存储值,一个本质是方法。

那么下列的代码是什么意思呢?这个代码说明将set方法私有了,只能在结构体的声明当中访问到。对于外部来说,是无法访问area的setter方法的。

    private(set) var area:Double


在SIL中查看,这个时候area是存储属性了,只不过setter对结构体外部是访问不到的,所以对外部来说是一个只读属性。

3. 属性观察者

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

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

这个时候运行就会调用willSet和didSet,newValue是要设的值也就是swift,而oldValue是原来的值就是空字符串

再来看到SIL文件,这里可以看到,是在setter里面调用的willSet和didSet方法,willSet和didSet中间是赋值的过程。也就是说,在赋值之前,setter方法会先调用willSet方法,然后进行赋值 *** 作,当赋值完成后,在调用didset方法。

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

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
    }
}

let s = SubjectName("swift")

从SIL角度来看,init里面的赋值是没有调用setter方法的,这里的赋值是直接赋值给属性的内存地址的,因为在初始化的时候属性可能还没有初始化完成,就会造成内存泄露。

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

class Square{
    var width: Double
    var area: Double{
        get{
            return width * width
            
        }
        set{
            self.width = sqrt(newValue)
        }
        willSet{
            print("area will set value \(newValue)")
            
        }
        didSet{
            print("area has been changed \(oldValue)")
        }
    }
    init(width: Double) {
        self.width = width
    }
}

这里可以看到willSet和didSet是不能和getter放在一起的,因为这里不需要willSet和didSet,如果需要观察属性,可以直接在set里面添加观察代码。

那么在继承的时候,属性观察者的表现是什么样的呢?输入下面代码

class LGPerson {
    var age: Int {
        willSet{
            print("age will set value \(newValue)")
            
        }
        didSet{
            print("age has been changed \(oldValue)")
        }
    }
    var name: String
    
    init (age:Int,name:String){
        self.age = age
        self.name = name
    }
 
}

class LGTeacher:LGPerson {
    override var age: Int {
        willSet{
            print("child age will set value \(newValue)")
            
        }
        didSet{
            print("child age has been changed \(oldValue)")
        }
    }
    var subject:String
    init (subject:String) {
        self.subject = subject
        super.init(age: 18, name: "hihi")
    }
}

运行后发现,这里是先调用子类的willSet,然后调用父类的willSet和didSet,最后调用子类的didSet。

从SIL来看,这里是先调用子类的willSet,然后调用父类的setter方法,调用父类的setter方法就会调用父类的willSet和didSet,最后调用子类的didSet。

4. 延迟存储属性

延迟存储属性,也可以理解为懒加载,必须要有一个初始值,初始值在其第一次使用时才进行计算,在Swift中用关键字 lazy 来标识一个延迟存储属性。

class Subject {
    lazy var name:String = ""
}

那么如何理解初始值在其第一次使用时才进行计算呢?创建一个s对象,然后打印s.name

var s = Subject()
print(s.name)
print("end")

在print(s.name)打下断点后运行。这里可以知道前面十六个字节分别是metadata和refCount,然后0x10389ab60是没有东西的,

而当运行到print(“end”)之后重新打印,发现这里有值了。所以这里可以知道第一次使用的时候才进行初始化。


那么添加lazy是否影响实例对象大小呢?打开SIL文件查看,发现这里name变成了可选值,并且是一个final。

从这里可以看到,name的默认值为空是因为这里给了一个枚举值Optional.none。

当去访问name的时候,就需要调用name的getter方法,而这前面的class_method表面了是VTable的调用。

这里看到name的getter方法进行枚举的模式匹配,如果没有有值则调用bb1代码块,没值调用bb2代码块。第一次访问时没有值的,所以此时调用的是bb2代码块。bb2把String的值构建出来,然后把值给到枚举变量,在把枚举变量的值赋值给name属性的地址。 而下次进来的话就有值了,就会调用bb1代码段,bb1代码段就会直接返回属性的值。

延迟存储属性不能保证变量只被访问一次。当有两个线程访问这个属性的时候,那么两个线程访问的时候,有可能都是没有值的,那么都会调用到bb2代码块,那么就会做两次赋值 *** 作。所以 延迟存储属性不是线程安全的。

延迟存储属性还可以使用闭包表达式赋值,这和原本的方式没有什么区别,只是需要调用闭包表达式把值给到name属性,需要用到函数调用。

class Subject {
   
   lazy var name:String {
       return "ssss"
   }()
}
5. 类型属性

类型属性其实就是一个全局变量,类型属性只会被初始化一次。

class LGTeacher {
    static var age: Int = 18
}

LGTeacher.age  = 30

从SIL角度来看,其还是存储属性,前面多了个static。

看到这里多了两个东西,一个token以及变成了全局变量的age。所以这里默认在全局声明了age变量,所以static本质上是全局变量。

那么是如何访问age变量的呢?看到这里是访问内存地址。

接下来看到unsafeMutableAddressor方法,这里先拿到了token,拿到了内存地址。接下来做了一个指针类型的转换然后调用了global_init函数,然后返回全局变量的内存地址。

看到global_init函数,这里先是创建一个age的全部变量,然后拿到其地址,构建int结构体,然后把结构体值存入到全局变量age里面,也就是在初始化age变量。


而builtin "once"实际上调用了swift_once,而swift_once在源码中测试调用了dispatch_once_f,使用了GCD的单例写法确保全局变量只被初始化一次。

那么,我们就可以使用类型属性来创建单例实例对象,并且这里私有化初始化器使外界只能通过单例实例对象来访问LGTeacher。

class LGTeacher {
    static let sharedInstance = LGTeacher()
    static var age: Int = 18
    private init(){}
}
6. 属性在Mahco文件的位置信息

之前探索的 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,这里面记录了V-Table的相关信息,接下来我们需要认识一下typeDescriptor中的fieldDescriptor。

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的结构体如下:

Flags:标识位MangledTypeName:当前属性类型信息FieldName: 属性名称
struct FieldRecord {
    Flags                        uint32
    MangledTypeName  int32
    FieldName                int32
}

根据macho的swift5_types算出类的位置位0XFFFFFF2C + 0X3F4C = 0x100003E78

接着在const里面找到类的位置,然后根据fieldDescriptor在TargetClassDescriptor中的位置知道需要位移4个4字节,得到 9C 00 00 00,这个是FieldDescriptor的offset在machO文件中的偏移信息这里不直接存储地址。那么使用0x100003E88 + 9C 得到0x100003F24。

到swift5_fieldmd里面找到0x100003F24,那么根据FieldDescriptor结构知道要偏移4个4字节找到FieldRecords。那么3F34开始就是FieldRecords这个数组。根据FieldRecord结构就可以知道,02 00 00 00 是Flags, E0 FF FF FF 是MangledTypeName, DF FF FF FF 是FieldName,以此类推。这里的FieldName存储也是偏移信息。

然后使用 0X3F34 + 0X8 + 0X FFFFFFDF = 0x100003F1B,然后到reflstr里面找到FieldName,分别为age 和 age1.

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/996788.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存