swift 类、对象、属性

与OC对比:

  • 实例对象 & 类

    • OC中的实例对象本质是结构体,是通过底层的objc_object模板创建,类是继承自objc_class
    • Swift中的实例对象本质也是结构体,类型是HeapObject,比OC多了一个refCounts
  • 方法列表

    • OC中的方法存储在objc_class结构体class_rw_tmethodList
    • swift中的方法存储在metadata元数据中
  • 引用计数

    • OC中的ARC维护的是散列表
    • swift中的ARC是对象内部有一个refCount属性

Swift属性

在swift中, 属性主要分为存储属性、计算属性、延迟存储属性、类型属性

存储属性

存储属性:常量存储属性(let修饰)、变量存储属性(var修饰)
代码如下

class Teacher {
    let age: Int = 18
    var name: String = "Hello"
}

var t = Teacher()

其中代码中的agename都是变量存储属性,在SIL中可以看出

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

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

断点调试.png
  • po t
  • x/8g 内存地址,即HeapObject的内存地址

计算属性

计算属性:不占用内存空间,本质是set/get方法的属性
下面先看一个demo,这demo有问题吗?

class Teacher {
    var age: Int {
        get {
            return 18
        }
        set {
            age = newValue
        }
    }
}

然后运行发现崩溃了,原因是ageset方法中调用age.set导致了循环引用,即递归

验证不占用内存

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属性没有占有内存空间。

验证:本质是set/get方法

  • 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中只有setter、getter方法

属性观察者(didSet、willSet)

  • willSet:新值存储之前调用 newValue
  • didSet:新值存储之后调用 oldValue
class Teacher{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
var t = CJLTeacher()
t.name = "swift"

//**********打印结果*********
willSet newValue swift
didSet oldValue 测试

通过以上代码可以验证观察者属性
那么以下几个问题思考一下
问题一:init方法中是否会触发属性观察者?

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

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

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

问题二:哪里可以添加属性观察者?

  • 中定义的存储属性
  • 通过继承的存储属性
class MediumTeacher: Teacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
  • 通过继承的计算属性
class Teacher{
    var age: Int = 18
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}
var t = Teacher()


class MediumTeacher: Teacher{
    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)")
        }
    }
}

问题三:子类和父类的存储属性同时存在didset、willset时,其调用顺序是什么?

class Teacher{
    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 MediumTeacher: Teacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
}

var t = MediumTeacher()
t.age = 20

打印结果:

子类 willSet newValue 20
父类 willSet newValue 20
父类 didSet oldValue 18
子类 didSet oldValue 18

结论:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,在父类didset, 子类的didset,即:子父 父子

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

class MediumTeacher: Teacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 willSet 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确保变量初始化完成了),所以可以观察属性了

延迟属性

  • ⽤关键字 lazy 来标识⼀个延迟存储属性。
  • 延迟存储属性的初始值在其第⼀次使⽤时才进⾏计算。
  • 延迟存储属性并不能保证线程安全
  • 延迟存储属性对实例对象大小的影响
class Teacher{
    lazy var age: Int = 18
}

延迟存储在第一次访问的时候才被赋值
通过代码调试来查看一下内存变化情况

  • age第一次访问前的内存情况:此时的age是没值的,为0x0
    image.png
  • age第一次访问后的内存情况:此时age是有值的,为20
    image.png

    因此可以验证懒加载存储属性只有在第一次访问时才会被赋值。
    lazy修饰的属性有以下两点说明
  • 1、lazy修饰的属性,在底层默认是optional,在没有被访问时,默认是nil,在内存中的表现就是0x0。在第一次访问过程中,调用的是属性的getter方法,其内部实现是通过当前enum的分支,来进行一个赋值操作。
  • 2、可选类型是16字节吗?可以通过MemoryLayout打印
print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)

打印结果:
16
9

为什么实际大小是9?Optional其本质是一个enum,其中Int占8字节,另一个字节主要用于存储case

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

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

  • 不使用lazy修饰,类的内存大小是24

    image.png

  • 使用lazy修饰,类的内存大小是32

    image.png

从而可以证明,使用lazy和不使用lazy,其实例对象的内存大小是不一样的

类型属性

  • 使用static修饰,且是一个全局变量
  • 类型属性必须有一个默认初始值
  • 类型属性只被初始化一次
class Teacher{
    static var age: Int = 18
}

// **** 使用 ****
var age = Teacher.age

单例的创建

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

推荐阅读更多精彩内容