Swift 二、属性

属性.png

一、存储属性

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特别要强调的,因为随处可见。

class ZGTeacher {
    let age: Int = 32
    var name: String = "Zhang"
}

比如这里的 agename 就是我们所说的存储属性,这里我们需要加以区分的是 letvar 两者的区别:从定义上: let 用来声明常量,常量的值一旦设置好便不能再被更改;var 用来声明变量,变量的值可以在将来设置为不同的值。

1.1 代码案例

这里我们来看几个案例:

class ZGTeacher {
    let age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

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

let t = ZGTeacher(age: 18, name: "Hello")
t.age = 20
t.name = "Logic"
t = ZGTeacher(age: 30, name: "Kody")

var t1 = ZGTeacher(age: 18, name: "Hello")
t1.age = 20
t1.name = "Logic"
t1 = ZGTeacher(age: 30, name: "Kody")

let s = ZGStudent(age: 18, name: "Hello")
s.age = 25
s.name = "Doman"
s = ZGStudent(age: 18, name: "Hello")

var s1 = ZGStudent(age: 18, name: "Hello")
s1.age = 25
s1.name = "Doman"
s1 = ZGStudent(age: 18, name: "Hello")
属性错误分析.png
  • 第25行代码:t.age因为age属性是被let修饰的,是一个常量存储属性,被赋值为18后,不可以再被更改
  • 第26行代码: t.name因为name属性是被var修饰的,是一个变量存储属性,被赋值为"hello"后,可以被"Logic"这个值修改替换
  • 第27行代码: t被let修饰并赋值为ZGTeacher(age: 18, name: "Hello")后,也是一个常量存储属性,不可以再次更改为ZGTeacher(age: 30, name: "Kody")
  • 第30行代码:t1.age因为age属性是被let修饰的,是一个常量存储属性,被赋值为18后,不可以再被更改为20
  • 第31行代码: t1这个类是被var修饰的,t1中的name属性也是被var修饰的,t1中的name属性可以被再次赋值
  • 第32行代码:t1这个类是被var修饰的,可以被再次赋值
  • 第35行代码:s结构体是被let修饰的,其中的age也是被let修饰的,被赋值为18后,不可以更改为25
  • 第36行代码:s结构体是被let修饰的,其中的name虽然是被var修饰的,但只可以被赋值一次,被赋值为"Hello"后不可以被再次赋值
  • 第37行代码:s结构体是被let修饰的,赋值一次后,不可以再次赋值
  • 第40行代码:s1结构体中的age属性是被let修饰的,赋值18后,不可以再次赋值
  • 第41行代码:s1结构体是被var修饰的,结构体中的name也是被var修饰的,被赋值为"Hello"后,也可以被再次赋值为"doman"
  • 第42行代码:s1结构体是被var修饰的,可以被多次赋值

1.2 let和var的区别

1.2.1 从汇编的角度

1.png

2.png

通过上面的截图可以看到,常量和变量的存储没有明显的区别。

1.2.2 从SIL的角度

///@_hasStorage代表是存储属性
///@_hasInitialValue代表有初始值
///有get和set方法
@_hasStorage @_hasInitialValue var age: Int { get set }

///只有get方法
@_hasStorage @_hasInitialValue let x: Int { get }

代表var修饰的age是变量存储属性,可以调用get方法获取值,也可以调用set方法赋值,而let 修饰的x是常量存储属性,只可以调用get方法获取值,一旦被赋值后不可以再更改。

二、计算属性

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

struct square {
///实例当中占据内存的
    var width: Double
///本质是一个方法,不占据内存
    var area: Double {
        get {
            return width * width
        }
        
        set {
            self.width = newValue
        }
    }
}
汇编打印.png

通过上图可以发现square.area.setter,就是一个方法的静态调用,而不是对属性的存储。

下面我们看一下只读的计算属性和let 有什么区别

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

编译成SIL文件看一下

struct square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get }
  @_hasStorage @_hasInitialValue let height: Double { get }
  init()
  init(width: Double = 30)
}

var area: Double { get }和@_hasStorage @_hasInitialValue let height: Double { get } 相同点是都有get方法,但它们的本质是不一样的。area是方法,height是let修饰的属性。

三、属性观察者

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

class Subject {
    var subjectName: String = "" {
        ///系统默认生成的newValue
        willSet {
            print("subject will set value \(newValue)")
        }
        didSet {
            print("subject will set value \(oldValue)")
        }
        
//        ///这里newBody=newValue,如果你不想用系统默认创建的值,可以自己定义一个别名
//        willSet (newBody) {
//            print("subject will set value \(newBody)")
//        }
//        ///这里oldBody=oldBody
//        didSet(oldBody) {
//            print("subject will set value \(oldBody)")
//        }
    }
}

let s = Subject()
s.subjectName = "Swift"

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

class Subject {
    var subjectName: String = "" {
        ///系统默认生成的newValue
        willSet {
            print("subject will set value \(newValue)")
        }
        didSet {
            print("subject will set value \(oldValue)")
        }
        
    }
    
    init(subjectName: String) {
        ///初始化的操作
        self.subjectName = subjectName
    }
}

let s = Subject(subjectName: "Swift")

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

运行报错.png

对于计算属性的观察者:分为基本计算属性带有继承的计算属性
因为没有初始化方法,不需要添加willSet,didSet观察者,如果想被观察者访问到,只需要将观察者需要实现的代码添加到自己本身自带的setter方法里。

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

class ZGPartTimeTeacher: ZGTeacher {
    override var age: Int {
        willSet {
            print("override age will set value \(newValue)")
        }
    
        didSet {
            print("override age has been changed \(oldValue)")
        }
    }
    var subjectName: String
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(18, 30.0)
        self.age = 20
    }
}

let t = ZGPartTimeTeacher("Swift")

打印结果如下:

override age will set value 20
age will set value 20
age has been changed 18
override age has been changed 18

可以得出以下结论:继承属性调用setter --> 调用自身willset --> 调用父类setter --> 父类调用自身willset --> 赋值 --> 父类调用自身didset --> 继承属性调用自身didset

四、延迟存储属性

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

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

我们来看下lldb的对应打印

po s
<Subject: 0x101b32fc0>

(lldb) x/8g 0x101b32fc0
0x101b32fc0: 0x0000000100008160 0x0000000200000003
0x101b32fd0: 0x0000000000000000 0x0000000101b33101
0x101b32fe0: 0x0000000000000000 0x0000000000000000
0x101b32ff0: 0x0000000101b30006 0x0000000100000001
18
(lldb) po s
<Subject: 0x101b32fc0>

(lldb) x/8g 0x101b32fc0
0x101b32fc0: 0x0000000100008160 0x0000000400000003
0x101b32fd0: 0x0000000000000012 0x0000000101b33100
0x101b32fe0: 0x0000000000000000 0x0000000000000000
0x101b32ff0: 0x0000000101b30006 0x0000000100000001

这是一个存储属性,前16个字节存储我们的Metadata(0x0000000100008160)refro(0x0000000400000003)
16个字节后开始存储属性对应的值0x0000000000000000
过掉s.age断点值存储进去变为0x0000000000000012,所以age被lazy修饰后,所以它是在第一次访问之后才会对它进行初始化操作。
那么添加lazy和不添加lazy对我们的内存大小有没有影响哪?我们把代码编译成sil文件看一下。

class Subject {
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

可以看到Int?,意味着添加了lazy以后,属性被修饰成了一个可选类型。

// variable initialization expression of Subject.$__lazy_storage_$_age
sil hidden [transparent] @$s4main7SubjectC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
bb0:
  %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
  return %0 : $Optional<Int>                      // id: %1
}

lazy_storage初始化表达式中 给了一个枚举值Optional.none!enumelt ,可以看作OC中的nil

%18 = class_method %15 : $Subject, #Subject.age!getter : (Subject) -> () -> Int, $@convention(method) (@guaranteed Subject) -> Int // user: %19

Subject.age!getter调用age的getter方法, class_method是一个vtable函数表的调用。

// Subject.age.getter
sil hidden [lazy_getter] [noinline] @$s4main7SubjectC3ageSivg : $@convention(method) (@guaranteed Subject) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Subject):
  debug_value %0 : $Subject, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $Subject, #Subject.$__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

Subject.age.setter方法首先访问我们的Subject.$__lazy_storage_$_age地址ref_element_addr,然后将这个地址的值给到我们的常量寄存器%4, switch_enum %4进行一个枚举模式的匹配,如果有值,走bb1代码块,没值,就走bb2代码块。

延迟存储属性的初始值在其第一次使用时才进行计算,相当于节省了我们的内存空间。我们的延迟存储属性并不是线程安全的,这点大家要注意。

五、类型属性

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

类型属性的调用方式是 类名+属性,如上面代码调用ZGTeacher.age
我们转译成sil文件看一下上面的代码

class ZGTeacher {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}

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

// static ZGTeacher.age
sil_global hidden @$s4main9ZGTeacherC3ageSivpZ : $Int

我们看sil文件代码,发现生成了一个token和一个static修饰的全局变量。
所以看到这里,我们应该可以理解static本质上就是一个全局变量。

  • 类型属性只会被初始化一次
// ZGTeacher.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main9ZGTeacherC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  %0 = global_addr @$s4main9ZGTeacherC3age_Wz : $*Builtin.Word // user: %1
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref one-time initialization function for age
  %2 = function_ref @$s4main9ZGTeacherC3age_WZ : $@convention(c) () -> () // user: %3
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  %4 = global_addr @$s4main9ZGTeacherC3ageSivpZ : $*Int // user: %5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  return %5 : $Builtin.RawPointer                 // id: %6
}
// one-time initialization function for age
sil private [global_init_once_fn] @$s4main9ZGTeacherC3age_WZ : $@convention(c) () -> () {
bb0:
  alloc_global @$s4main9ZGTeacherC3ageSivpZ       // id: %0
  %1 = global_addr @$s4main9ZGTeacherC3ageSivpZ : $*Int // user: %4
  %2 = integer_literal $Builtin.Int64, 18         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  store %3 to %1 : $*Int                          // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
}

builtin "once"表示执行一次
我们降级成IR文件再来看一下

once_not_done:                                    ; preds = %entry
  call void @swift_once(i64* @"$s4main9ZGTeacherC3age_Wz", i8* bitcast (void ()* @"$s4main9ZGTeacherC3age_WZ" to i8*), i8* undef)
  br label %once_done
}

这里调用了@swift_once,那么@swift_once是做什么的哪,我们到Swift源码搜一下。我们在once.cpp文件中看到如下代码:

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

可以看出,是调用了GCD,确保只被初始化一次。只初始化一次,就和我们的OC中的单例很像。
我们先来回顾一下OC单例:

+ (instancetype)sharedInstance {
    static Test1 * t = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        t = [[Test1 alloc]init];
    });
    return t;
}

那么Swift中的单例又是怎么样的哪?

class ZGTest {
    static let sharedInstance = ZGTest()
    private init(){}
}

六、属性在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 ,这里面记录了 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
}

上节课我们提到,我们的typeDescriptor存储在__TEXT,__Swift_types里,将自己项目的mach-o文件放到machoView应用里,可以看到:

typeDescriptor地址.png

fieldDescriptor 记录了当前的属性信息,其中fieldDescriptor在源码中的结构如下:

struct FieldDescriptor {
    MangledTypeName int32
    Superclass int32
    Kind uint16
    FieldRecordSize uint16
    NumFields uint32
    FieldRecords [FieldRecord]
}

我们用 0xFFFFFF2C+0x00003F4C得到0x100003E78,这个就是typeDescriptor在当前Mach-o文件中的地址,减去我们的虚拟内存地址0x100000000,得到一个0x3E78,我们可以直接定位到该地址

image.png

看一下这个typeDescriptor结构体,如果我们想要找到fieldDescriptor地址,需要从当前地址偏移4个4字节。
image.png

这个9C存储的是偏移信息,所以我们用0x00003E88加上偏移信息
0x9C得到0x3F24
image.png

这个地址0x3F24就就代表了我们的fieldDescriptor,而它后面的地址就是这个结构体存储的内容。

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

struct FieldRecord {
    Flags uint32
    MangledTypeName int32
    FieldName int32
}

我们想要拿到FieldRecords的地址,只需要0x3F24偏移4个4字节。

image.png

我们想要拿到FieldRecords结构体中的FieldName来验证一下拿到的对不对,那么我们只需要偏移2个4字节。用0x00003F34 + 0x8得到0x00003F3C再加上0xFFFFFFDD,得到0x100003F19,减去我们的虚拟内存地址0x100000000,得到0x3F19
image.png

是的,这里我们拿到并验证了age和age1两个属性存放的地址。

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

推荐阅读更多精彩内容