swift 进阶: 属性

swift 进阶之路:学习大纲

前言

  • swift里面的属性和oc的大不一样,属性将值与类,结构体,枚举进行关联。

内容

  • 存储型属性和计算型属性
  • 属性观察者(willSet、didSet)
  • 类型属性:单例
  • 延迟存储属性:lazy

一、存储型属性和计算型属性

存储属性 计算属性
存储常量或变量作为实例的一部分 计算(而不是存储)一个值
用于类和结构体 用于类、结构体和枚举
存储属性

定义:简单来说,一个存储属性就是存储在特定类或结构体的实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字var定义),也可以是常量存储属性(用关键字let定义)。

  • 可以在定义存储属性的时候指定默认值
  • 也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值
class HJPerson {
    var age : Int = 20
    var name : String = "HJ"
}

let t = CJLTeacher()

其中代码中的age、name来说,都是变量存储属性,这一点可以在SIL中体现

class HJPerson {
    //_hasStorage 表示是存储属性
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

存储属性特征:会占用分配实例对象的内存空间

验证:
计算属性

除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。

【计算属性特征】 :不占用内存空间

验证:

class Square{
    var width: Double = 8.0 //存储属性 占内存
    var area: Double{ //计算属性 不占内存
        get{
            //这里的return可以省略,编译器会自动推导
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

print(class_getInstanceSize(Square.self))

//********* 打印结果 *********
24

从结果可以看出类Square的内存大小是24,等于 (metadata +refCounts)类自带16字节 + width(8字节) = 24,是没有加上area的。从这里可以证明 area属性没有占有内存空间。

【防止循环引用】:

class HJPerson{
    var age: Int{
        get{
            return 18
        }
        set{
            age = newValue
        }
    }
}
  • 在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set;
  • 然后运行发现崩溃了,原因是age的set方法中调用age.set导致了循环引用,即递归。

【本质是set/get方法】:

cd到main.swift的文件夹,然后将main.swift转换为SIL文件:swiftc -emit-sil main.swift >> ./main.sil
查看SIL文件,对于存储属性,有_hasStorage的标识符

class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}

对于计算属性,SIL中只有settergetter方法

二、属性观察者(willSet、didSet)

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,甚至新的值和现在的值相同的时候也不例外。
可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重载属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。

  • willSet:新值存储之前调用 newValue
  • didSet:新值存储之后调用 oldValue
  • willSetdidSet观察器在属性初始化过程中不会被调用
    举例:
class HJPerson {
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
var t = HJPerson()
t.name = "kc"

//**********打印结果*********
willSet newValue kc
didSet oldValue 测试
使用方式:
  • 1、类中定义的存储属性
  • 2、通过类继承的存储属性
class HJBoy : HJPerson{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
  • 3、通过类继承的计算属性
class HJPerson{
    var age: Int = 18
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}
var t = HJPerson()

class HJBoy : HJPerson{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    override var age2: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
属性观察者的触发时机:

以下代码中,init方法中设置name,是否会触发属性观察者?

class HJPerson{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    init() {
        self.name = "KC"
    }
}

运行结果发现,并没有走willSet、didSet中的打印方法,所以有以下结论:

  • 在init方法中,如果调用属性,是不会触发属性观察者的
  • init中主要是初始化当前变量,除了默认的前16个字节,其他属性会调用memset清理内存空间(因为有可能是脏数据,即被别人用过),然后才会赋值

【总结】:初始化器(即init方法设置)和定义时设置默认值(即在didSet中调用其他属性值)都不会触发

【问题1】:子类和父类的计算属性同时存在didset、willset时,其调用顺序是什么?
import Foundation

class HJPerson {
    var age: Int = 18{
        //新值存储之前调用
        willSet{
            print("父类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("父类 didSet oldValue \(oldValue)")
        }
    }
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}


class HJSon: HJPerson{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
}

var t = HJSon()
t.age = 20

//  ------打印:
子类 newValue 20
父类 willSet newValue 20
父类 didSet oldValue 18
子类 didSet oldValue 18

【结论】:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,再父类didset, 子类的didset,即:子父 父子,记忆:父总是让着子

【问题2】:子类调用了父类的init,是否会触发观察属性?

import Foundation

class HJPerson {
    var age: Int = 18{
        //新值存储之前调用
        willSet{
            print("父类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("父类 didSet oldValue \(oldValue)")
        }
    }
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}


class HJSon: HJPerson{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
    override init() {
     super.init()
     self.age = 20
    }
}
//****** 打印结果 ******
子类 willSet newValue 20
父类 willSet newValue 20
父类 didSet oldValue 18
子类 didSet oldValue 18
  • 从打印结果发现,会触发属性观察者,主要是因为子类调用了父类的init,已经初始化过了,而初始化流程保证了所有属性都有值(即super.init确保变量初始化完成了),所以可以观察属性了

三、 类型属性

  • 1、使用关键字static修饰,且是一个全局变量

  • 2、类型属性必须有一个默认的初始值

  • 3、类型属性只会被初始化一次

举例:

class CJLTeacher{
    static var age: Int = 18
}
// **** 使用 ****
var age = CJLTeacher.age

swift单例:

直接static创建,将init方法藏起来(private私有重写)。
支持懒加载, 线程安全

class HJPerson {
    // 创建单例对象
    static let sharedInstance = HJPerson()
    // 重写init方法,设为私有方法
    private init(){}
}

回忆标准OC单例:

@implementation HJPerson

static HJPerson rson *sharedInstance = nil;

+ (instancetype)sharedInstance{
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 不使用alloc方法,而是调用[[super allocWithZone:NULL] init]
        // 重载allocWithZone基本的对象分配方法,所以要借用父类(NSObject)的功能处理底层内存分配
        // 此时不管外部使用设么方式创建对象,最终返回的都是单例对象
        sharedInstance = [[super allocWithZone:NULL] init] ;
    });
    return sharedInstance;
}

+(id)allocWithZone:(struct _NSZone *)zone {
    return [HJPerson sharedInstance] ;
}
 
-(id)copyWithZone:(NSZone *)zone {
    return [HJPerson sharedInstance] ;
}
 
-(id)mutablecopyWithZone:(NSZone *)zone {
    return [HJPerson sharedInstance] ;
}

@end

四、 延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。
在属性声明前使用 lazy 来标示一个延迟存储属性。

注意:

  • 必须将延迟存储属性声明成变量(使用var关键字),因为属性的值在实例构造完成之前可能无法得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。
  • 延迟存储属性并不能保证线程安全
  • 延迟存储属性对实例对象大小的影响

延迟存储属性一般用于:

  • 延迟对象的创建。
  • 当属性的值依赖于其他未知类
import Cocoa

class sample {
    lazy var no = number() // `var` 关键字是必须的
}

class number {
    var name = "延迟存储属性"
}

var firstsample = sample()
print(firstsample.no.name)

//****** 打印结果 ******
延迟存储属性

验证:
  • 懒加载存储属性只有在第一次访问时才会被赋值

我们也可以通过sil文件来查看,这里可以在生成sil文件时,加上还原swift中混淆名称的命令(即xcrun swift-demangle):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil,demo代码如下

class CJLTeacher{
    lazy var age: Int = 18
}

var t = CJLTeacher()
t.age = 30

【类+main】:lazy修饰的存储属性在底层是一个optional类型

【 setter+getter】:从getter方法中可以验证,在第一次访问时,就从没值变成了有值的操作

通过sil,有以下两点说明:

  • 1、lazy修饰的属性,在底层默认是optional,在没有被访问时,默认是nil,在内存中的表现就是0x0。在第一次访问过程中,调用的是属性的getter方法,其内部实现是通过当前enum的分支,来进行一个赋值操作

  • 2、可选类型是16字节吗?可以通过MemoryLayout打印

size:实际大小
stride:分配大小(主要是由于内存对齐)

print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)

//*********** 打印结果 ***********
16
9

为什么实际大小是9?Optional其本质是一个enum,其中Int占8字节,另一个字节主要用于存储case值(这个后续会详细讲解)

延迟存储属性并不能保证线程安全

继续分析3中sil文件,主要是查看age的getter方法,如果此时有两个线程:

  • 线程1此时访问age,其age是没有值的,进入bb2流程

  • 然后时间片将CPU分配给了线程2,对于optional来说,依然是none,同样可以走到bb2流程

  • 所以,在此时,线程1会走一遍赋值,线程2也会走一遍赋值,并不能保证属性只初始化了一次

懒加载属性,创建时,是可选值。但是在首次访问(getter)时,进行初始赋值,返回非可选类型的值。

延迟存储属性对实例对象大小的影响

下面来继续看下不使用lazy的内存与使用lazy的内存是否有变化?

从而可以证明,使用lazy和不使用lazy,其实例对象的内存大小是不一样的,Int变成16字节了。

Q: 为何lazy修饰的Int属性是16字节:

  • 因为lazy修饰的属性,会变成可选类型
    option: 可选类型。本质是枚举值类型
    包含some<Int>none两个枚举类型。其中none0x0。打印
image
  • 其中:none1字节some<Int>8字节。所以实际大小(size)为9字节
  • 对外遵循align8(8字节对齐)原则,系统会开辟16字节空间(8的倍数)来存储真实大小9字节数据
    align8原则:为了避免五花八门空间大小,增加系统读取数据困难性>。所以统一8字节为一个单位,进行一段一段截取,提高读取效率。)

【 延迟存储属性 lazy 总结】

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

推荐阅读更多精彩内容