Swift的属性

一、存储属性

  存储属性 是一个类或者值类型(结构体,enum等)实例一部分的常量或变量,其存储属性一般有两种引入方式:

  1. var关键字定义的,叫变量存储属性
  2. let关键字定义的,叫常量存储属性

那么这两种有什么区别呢?在汇编层面,他们其实没有什么区别,但是在编译时他们主要的区别是:

  • let修饰的常量,其值一旦设置就无法再修改,否则编译不通过
  • var修饰的常量,其值可以随处设置不同的值

eg:

sil分析:

class PSYModel {
  @_hasStorage var age: Int { get set }
   @hasStorage final let name: String { get }
   init(
age: Int, _ name: String)
   @objc deinit
}
  
// PSYModel.age.getter    //age的getter方法
// PSYModel.age.setter    //age的setter方法
// PSYModel.name.getter   //name的getter方法

通过上面可以看出var修饰的变量,具有 存储属性,并且具有gettersetter方法,所以其设置值之后还可被设置新值。而let修饰的变量,被添加了final标识,并且只有getter方法,没有setter方法,所以在被设置初值之后就不能再被设置其值。但是为啥在指定初始化器中怎么能够给它设置值呢?因为在init方法中,是直接操作内存,存储到对应的内存的,并不需要调用setter方法

二、计算属性

  在swift中存储属性很常见,当然除了存储属性,类、结构体和枚举也能够定义计算属性。计算属性并不存储值,他们提供gettersetter方法来获取和修改值,并且不同于存储属性可以使用varlet关键字修饰,计算属性必须用var修饰,并且定义变量的类型。如下:

struct square {
  var width: Double
  
  var area: Double{
     get{
       return width * width
     }
    set(newValue){
      self.width = newValue
    }
   }
}
var s = square(width: 10.0)
s.area = 20

sil文件

sil文件area具有settergetter方法,但是并没有 hasStoreage标识,也就是没有存储属性。直接打印其结构体的大小也可以发现area并不占用内存。

如果private (set)修饰一个变量,说明其实具有存储属性的私有变量,只能在类或结构体代码块内可以访问,并且有settergetter方法,只是将setter方法设置为私有,外部只能调用getter方法。

三、属性观察者

  属性观察者会观察用来观察属性值的变化,当属性值即将改变时调用willSet,当属性值改变之后调用didSet。其方法类似于gettersetter方法,在初始化器内修改其值并不会触发调用willSetdidSet方法,因为初始化器是直接内存的操作。(计算属性不用添加观察者

初始化器内不会触发 非初始化器赋值触发

@_hasStorage var subName: String { get set } 也具有存储属性,其赋值的时候是由setter方法触发的,sil文件如下:

subName的setter方法的sil文件

如果类存在继承,观察者属性调用又是啥样呢?

class PSYModel{
    var age: Int
    let name: String

    init(_ age: Int, _ name: String, _ subName: String) {
        self.age = age
        self.name = name
        self.subName = subName
    }
    var subName: String {
        willSet{
            print("PSY subName value willSet")
        }
        didSet{
            print("PSY subName value didSet")
        }
    }
}

class LQRModel: PSYModel {
    var sex: Int
    
    override var subName: String{
        willSet{
            print("LQR subName value willSet")
        }
        didSet{
            print("LQR subName value willSet")
        }
    }
    init(_ sex: Int) {
        self.sex = sex
        super.init(18, "P", "SY")
    }
}

var psy = LQRModel.init(1)
psy.subName = "spy"
print("end")

输出结果:****************************
LQR subName value willSet
PSY subName value willSet
PSY subName value didSet
LQR subName value willSet
end
Program ended with exit code: 0

image.png

四、延迟存储属性

  延迟存储属性,其初始值是在第一次使用的时候才计算,使用关键字lazy来标识一个延迟存储属性。延迟存储属性只能用let修饰,不能用let,且必须有值。

class PSYModel{
    lazy var age: Int = 1
    let name: String

    init(_ name: String) {
        self.name = name
    }
}
var psy = PSYModel.init("PSY")
print("psy.age = \(psy.age)")
print("end")

lldb打印内存看一下其实例对象在初始化的时候并没有值,在使用的时候才有值:

lldb内存

那么它具体是怎么做到,使用的时候才有值呢?它实际是被编译成了一个_age: Int?可选类型的存储属性,并且生成了一个_age初始化表达式,返回了一个枚举。
sil文件

初始化表达式返回了一个枚举
age的getter方法
bb9是初始化值为6

可知,延迟存储属性延迟了内存的分配,相当于懒加载一样的效果,但是其并不是线程安全的,因为其并不能保证属性只被访问一次。

五、类型属性

  类型属性其实是一个全局变量,只会被初始化一次,使用static修饰,eg :

class PSYModel{
    static var age: Int = 6
}
PSYModel.age = 18

将上面的代码生成.sil代码看一下,可以发现其仍具有存储属性,比一般存储属性多了个static修饰符,并且生成了一个全局变量age:

sil代码

main函数主要意思:

  1. 引用函数PSYModel.age.unsafeMutableAddressor
  2. 调用函数PSYModel.age.unsafeMutableAddressor并转成RawPointer原生指针
  3. 将原生指针指向Int数据类型
  4. 创建数值为18的数据Int64数据
  5. 将数据存入内存
main函数

PSYModel.age.unsafeMutableAddressor到底做了啥?内存是如何分配的?继续解读sil文件PSYModel.age.unsafeMutableAddressor,可以发现其主要做了:

  1. 申请了一个全局的token0@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
  2. 拿到globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0申请内存函数指针
  3. 调用了一个“once”函数,参数是上面得到的token0globalinit函数指针
PSYModel.age.unsafeMutableAddressor

@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0真正初始化的函数,这个就很简单了,创建全局属性age,拿到内存地址,创建初始值6,将6存储到内存当中并返回。

@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0

但是那个“once”是有什么用吗?为什么还有一个token0?为了得到更接近底层汇编的中间代码,在终端使用命令生成ir文件: swiftc main.swift -emit-ir -o main1.c这里为了方便生成的文件查看我制定后缀为.c文件

找到相同的函数@s4main8PSYModelC3ageSivau--> PSYModel.age.unsafeMutableAddressor,可以发现“once”函数的调用译成了根据标记,如果没有内存分配(once_ont_done)就调用源码swift_once方法,如果已经分配了(once_done)就直接读取内存(load),保证了其只会分配一次内存,只初始化一次。

image.png

结合Swift源码 看一下swift_once做了什么?全局搜索swift_once可以找到其,并根据注释可以知道其调用了GCD(dispatch_once_f)保证函数只被执行一次。

/// 使用给定的上下文参数运行给定函数一次。
///predicate参数必须指向一个全局变量或静态变量,其静态范围类型为swift_once_t。
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
  if (! *predicate) {
    *predicate = true;
    fn(context);
  }
#elif defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}
拓展(单例):

 // 单例
class PSYModel{
  static let shareInstance = PSYModel()
  private init() {}
} 
如果不希望单例类被继承,class前面加上final

六、属性在MachO文件中的位置

  首先我们要有一个共识,根据前两篇文章【Swift语言的类与结构体--2Swift语言的类与结构体--1】我们得到了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
  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
}

以及在typeDescriptor的结构信息中,fieldDescriptor记录了属性的信息,其结构体如下:

struct FieldDescriptor{
   var MangledTypeName: UInt32
  var Superclass: UInt32
  var Kind: Int16
  var FieldRecordSize: Int16
  var NumFields: Int32 // 属性个数
  var FieldRecords // [FieldRecords]
}

FieldRecords中记录每个属性的信息,其结构体如下:

struct FieldRecord{
  Flags :         uint32
  MangledTypeName:   int32
  FieldName:       int32
}

新建代码,编译成.app文件,使用MachO View打开,手动计算验证是否能找到属性名:

class PSYModel{
    var age: Int = 18
    let name: String = "psy"
}

首先找到Section__TEXT,__swift5_types,将pFile + Data - baseAddress(基地址)0xFFFFFF5C + 0x1EE0 - 0x100000000 = 0x1E3C)得到typeDescriptor的偏移

得到typeDescriptor

点击Section64__TEXT,__const,找到0x1E3CpFile,向后偏移4个4字节,得到fieldDescriptor,此时也是得到fieldDescriptoroffset信息,需要pFile + Data0x1E4C+ 0x6C = 0x1EB8)才能得到真正的FieldDescriptor

得到FieldDescriptor

点击Section64__TEXT,__switf5_fieldmd,找到0x1EB8,向后偏移3个4字节,得到连续的内存空间即是FiledRecords,再根据FiledRecords结构,第1个4字节是Flags,第2个4字节是MangledTypeName,接下来的4字节是FiledName的offset(0xffffffdf

得到FieldRecords

pFile + FiledName的offset0x1ED0 + 0xffffffdf = 0x100001EAF),然后在Section64__TEXT,__switf5_reflstr就可找到属性名字如下:同理,0x1EDC + 0xffffffd7 = 0x100001EB3得到另一个属性名name

得到FiledName

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

推荐阅读更多精彩内容

  • 一.存储属性 存储属性是一个作为特定类和结构体实例一部分的常量或变量.存储属性要么是变量存储属性(由var关键字引...
    刘国强阅读 527评论 0 0
  • Swift 属性 在Swift中属性主要分为存储属性、计算属性、延迟存储属性、类型属性这四种,并且Swift还提供...
    just东东阅读 384评论 0 2
  • 类的属性介绍Swift中属性有多种存储属性:存储实例的常量和变量计算属性:通过某种方式计算出来的属性类属性:与整个...
    我为自己dai盐阅读 553评论 0 0
  • 存储属性 - Stored Properties 相当于 OC 的下划线成员变量 适用于:结构体 、 类 类型:常...
    Sunday_David阅读 259评论 0 0
  • 属性分类在Swift中, 严格意义上来讲属性可以分为两大类: 实例属性和类型属性 实例属性(Instance Pr...
    1980_4b74阅读 438评论 0 0