swift探索2: swift属性

在swift中,属性主要分为以下几种

  • 存储属性

  • 计算属性

  • 延迟存储属性

  • 类型属性

存储属性

存储属性,又分两种:

  • 要么是常量存储属性,即let修饰

  • 要么是变量存储属性,即var修饰

定义如下代码

class CJLTeacher{
    var age: Int = 18
    var name: String = "CJL"
}

let t = CJLTeacher()

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

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

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

下面我们同断点调试来验证

  • po t

  • x/8g 内存地址,即HeapObject存储的地址

    图片

    属性-1

    图片

    属性-2

计算属性

计算属性:是指不占用内存空间,本质是set/get方法的属性

我们通过一个demo来说明,以下写法正确吗?

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

在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set

图片

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

图片

属性-4

验证:不占内存
对于其不占用内存空间这一特征,我们可以通过以下案例来验证,打印以下类的内存大小

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方法

    图片

    属性-5

属性观察者(didSet、willSet)

  • willSet:新值存储之前调用 newValue

  • didSet:新值存储之后调用 oldValue

验证

  • 可以通过demo来验证

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

//**********打印结果*********
willSet newValue CJL
didSet oldValue 测试
  • 也可以通过编译来验证,将main.swift编译成mail.sil,在sil文件中找nameset方法

    图片

    属性-6

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

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

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

  • init方法中,如果调用属性,是不会触发属性观察者的

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

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

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

主要有以下三个地方可以添加:

  • 1、中定义的存储属性

  • 2、通过类继承的存储属性

class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
  • 3、通过类继承的计算属性

class CJLTeacher{
    var age: Int = 18
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}
var t = CJLTeacher()


class CJLMediumTeacher: CJLTeacher{
    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)")
        }
    }
}

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

有以下代码,其调用顺序是什么?

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

var t = CJLMediumTeacher()
t.age = 20

运行结果如下:

图片

属性-7

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

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

在问题3的基础,修改CJLMediumTeacher

class CJLMediumTeacher: CJLTeacher{
    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确保变量初始化完成了),所以可以观察属性了

延迟属性

延迟属性主要有以下几点说明:

  • 1、使用lazy修饰的存储属性

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

  • 3、延迟存储在第一次访问的时候才被赋值

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

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

下面来一一进行分析

1、使用lazy修饰的存储属性

class CJLTeacher{
    lazy var age: Int = 18
}

2、延迟属性必须有一个默认的初始值

如果定义为可选类型,则会报错,如下所示

图片

属性-8

3、延迟存储在第一次访问的时候才被赋值
可以通过调试,来查看实例变量的内存变化

  • age第一次访问前的内存情况:此时的age是没值的,为0x0

    图片

    属性-9

  • age第一次访问后的内存情况:此时age是有值的,为30

    图片

    属性-10


    从而可以验证,懒加载存储属性只有在第一次访问时才会被赋值

我们也可以通过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类型

    图片

    属性-11

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

    图片

    属性-12

通过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

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

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

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

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

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

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

5、延迟存储属性对实例对象大小的影响
下面来继续看下不使用lazy的内存与使用lazy的内存是否有变化?

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

    图片

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

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

类型属性

类型属性,主要有以下几点说明:

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

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

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

1、使用关键字static修饰

class CJLTeacher{
    static var age: Int = 18
}

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

生成SIL文件

  • 查看定义,发现多了一个全局变量,说以,类型属性是一个全局变量

    图片

    属性-15

  • 查看入口函数中age的获取

    图片

    属性-16

  • 查看age的getter方法

    图片

    • 其中 globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0是全局变量初始化函数

    • builtin "once" ,通过断点调试,发现调用的是swift_once,表示属性只初始化一次

  • 源码中搜索swift_once,其内部是通过GCDdispatch_once_f 单例实现。从这里可以验证上面的第3点

void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if 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
}

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

如下图所示,如果没有给默认的初始值,会报错

图片

属性-20

所以对于类型属性来说,一是全局变量,只初始化一次,二是线程安全的

单例的创建

//****** Swift单例 ******
class CJLTeacher{
    //1、使用 static + let 创建声明一个实例对象
    static let shareInstance = CJLTeacher.init()
    //2、给当前init添加private访问权限
    private init(){ }
}
//使用(只能通过单例,不能通过init)
var t = CJLTeacher.shareInstance

//****** OC单例 ******
@implementation CJLTeacher
+ (instancetype)shareInstance{
    static CJLTeacher *shareInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[CJLTeacher alloc] init];
    });
    return shareInstance;
}
@end

总结

  • 存储属性会占用实例变量的内存空间,且

  • 计算属性不会占用内存空间,其本质是set/get方法

  • 属性观察者

    • willset:新值存储之前调用,先通知子类,再通知父类(因为父类中可能需要做一些额外的操作),即子父

    • didSet:新值存储完成后,先告诉父类,再通知子类(父类的操作优先于子类),即父子

    • 类中的init方法赋值不会触发属性观察

    • 属性可以添加在 类定义的存储属性、继承的存储属性、继承的计算属性

    • 子类调用父类的init方法,会触发观察属性

  • 延迟存储属性

    • 使用lazy修饰存储属性,且必须有一个默认值

    • 只有在第一次被访问时才会被赋值,且是线程不安全

    • 使用lazy和不使用lazy,会对实例对象的内存大小有影响,主要是因为lazy在底层是optional类型,optional的本质是enum,除了存储属性本身的内存大小,还需要一个字节用于存储case

  • 类型属性

    • 使用static + let创建实例变量

    • init方法的访问权限为private

    • 使用static 修饰,且必须有一个默认初始值

    • 是一个全局变量,只会被初始化一次,是线程安全

    • 用于创建单例对象:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值