一.存储属性&计算属性
- swift属性分为两大类 stored property(存储属性) computed property(计算属性)
- stored property:属性放在该对象内部,占用该对象内存,所以只有struct和class才有存储属性,枚举就不可以有存储属性,因为关联值和原始值都放在枚举类里面,而不是枚举实例化对象
- computed property: 只用作计算,没有真正存储到实例化对象的内存中
- 所以,一个对象占用内存的大小,只计算 stored property(存储属性) ,而不计算computed property(计算属性)
//下面的storedP就是存储属性, 而computedP为计算属性(本质是方法)
class Property{
var storedP : Int = 0 //存储属性要初始化值才可以编译
var computedP: Int {
get{
storedP * 2
}
set{
storedP = 9
}
}
}
let p = Property()
p.storedP = 11
print(p.computedP) //22
print(MemoryLayout<Property>.size) //8
print(MemoryLayout<Property>.stride) //8
print(class_getInstanceSize(Property.self)) //24 类(HeapObject)16+Int8
可以看到计算属性并不占用内存空间
二. 属性观察器
2.1 什么是属性观察器
class PropertyWillDidSet {
var name: String = "water" {
willSet{
print("willSet \(newValue)")
}
didSet{
print("didSet \(oldValue)")
}
}
}
let p = PropertyWillDidSet()
p.name = "fire"
//willSet fire
//didSet water
- 顾名思义,属性观察器就是用来观察属性变化的,属性观察器可以将新值和老值都获取到
- 使用
swiftc -emit-sil sil.swift > sil.sil
拿到这段代码的sil
发现在PropertyWillDidSet的name的set方法中,调用了name的willset
和didset
方法
图片.png
2.2 init方法里面调用set
下面这段代码,自定义初始化器内部调用set方法,会调用属性观察器的willset
和didset
方法吗?
class PropertyWillDidSet {
var name: String = "water" {
willSet{
print("willSet \(newValue)")
}
didSet{
print("didSet \(oldValue)")
}
}
init() {
self.name = "trump"
}
}
let p = PropertyWillDidSet()
答案是Flase
在init方法里面调用set方法,是不会触发属性观察器的。
为什么呢?因为你现在还在init方法中,这时的init还没有真正的初始化完成,swift不允许这样不安全的操作,所以不会触发属性观察器。
那么下面的代码呢?
class Father {
var name: String = "water" {
willSet{
print("Father willSet \(newValue)")
}
didSet{
print("Father didSet \(oldValue)")
}
}
}
class Son: Father {
override init() {
super.init()
self.name = "SonName"
}
}
let son = Son()
//Father willSet SonName
//Father didSet water
可以看到在init方法里面调用了父类的属性观察器。这是因为super.init
后,该类已经初始化完成,现在的赋值操作是安全的,所以可以触发。
2.3 override父类的属性观察器
class Father {
var name: String = "water" {
willSet{
print("Father willSet \(newValue)")
}
didSet{
print("Father didSet \(oldValue)")
}
}
}
class Son: Father {
override var name: String {
willSet{
print("Son willSet \(newValue)")
}
didSet{
print("Son didSet \(oldValue)")
}
}
}
let son = Son()
son.name = "老王"
//打印如下:
Son willSet 老王
Father willSet 老王
Father didSet water
Son didSet water
- 正常来说,覆写父类的方法应该只会调用子类自己的方法,为什么还会调用Father的willSet和didSet呢?
2.4 什么样的属性可以有属性观察器呢
- 存储属性
- 继承的存储属性
- 继承的计算属性
- 总之就是:属性观察器不能和get&set方法同时存在,也就是说willSet&didSet不能和set方法同时存在,编译器会报错的。
三. 延迟存储属性 lazy stored property
- 使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会初始化
class LazyClass {
init() {
print("LazyClass初始化啦")
}
func lazyRun() {
print("Lazy运行")
}
}
class Person {
lazy var l = LazyClass()
init() {
print("Person初始化啦")
}
func useLazy () {
l.lazyRun()
}
}
let person = Person()
person.useLazy()
打印:
//Person初始化啦
//LazyClass初始化啦
//Lazy运行
可以看到,虽然LazyClass先创建的,但是没有调用它的init方法。而是在第一次使用的时候才调用它的init方法。
3.1 lazy特点
import Foundation
class PersonTest {
var age: Int = 11
}
let p = PersonTest()
print(class_getInstanceSize(PersonTest.self)) //24
import Foundation
class PersonTest {
lazy var age: Int = 11
}
let p = PersonTest()
print(class_getInstanceSize(PersonTest.self)) //32
我们给PersonTest加上lazy以后,发现该类占用的内存增大了。
我们可以通过sil来看下是怎么回事
- 不加lazy的实现
swiftc -emit-sil sil.swift | xcrun swift-demangle > sil.sil
class PersonTest {
@_hasStorage @_hasInitialValue var age: Int { get set }
@objc deinit
init()
}
// PersonTest.age.getter
sil hidden [transparent] @sil.PersonTest.age.getter : Swift.Int : $@convention(method) (@guaranteed PersonTest) -> Int {
// %0 "self" // users: %2, %1
bb0(%0 : $PersonTest):
debug_value %0 : $PersonTest, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $PersonTest, #PersonTest.age // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Int // users: %4, %5
%4 = load %3 : $*Int // user: %6
end_access %3 : $*Int // id: %5
return %4 : $Int // id: %6
} // end sil function 'sil.PersonTest.age.getter : Swift.Int'
// PersonTest.age.setter
sil hidden [transparent] @sil.PersonTest.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed PersonTest) -> () {
// %0 "value" // users: %6, %2
// %1 "self" // users: %4, %3
bb0(%0 : $Int, %1 : $PersonTest):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $PersonTest, let, name "self", argno 2 // id: %3
%4 = ref_element_addr %1 : $PersonTest, #PersonTest.age // user: %5
%5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
store %0 to %5 : $*Int // id: %6
end_access %5 : $*Int // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function 'sil.PersonTest.age.setter : Swift.Int'
- 加了lazy的实现
class PersonTest {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
// PersonTest.age.getter
sil hidden [lazy_getter] [noinline] @sil.PersonTest.age.getter : Swift.Int : $@convention(method) (@guaranteed PersonTest) -> Int {
// %0 "self" // users: %14, %2, %1
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
}
可以看到,加了lazy的属性,系统会将其变成可选型。在你未访问它之前,他默认值为nil;访问它以后,它才会赋值
我们看下可选型的大小,发现Optional<Int>为16, int为8 ,这也就是为什么加了lazy以后多了8个字节。
print(MemoryLayout<Optional<Int>>.size)//9
print(MemoryLayout<Optional<Int>>.stride)//16
四. Type property 类型属性
struct TypeProperty {
var width : Int = 0
static var height : Int = 0
}
let typeP = TypeProperty()
typeP.width
TypeProperty.height;
- 使用static修饰的属性为类型属性。类型属性,只能通过类型去访问
- 使用static修饰的属性为全局变量,存放在全局区,只有一份内存
// static TypeProperty.height
sil_global hidden @static sil.TypeProperty.height : Swift.Int : $Int
- 类型属性是线程安全的
// one-time initialization token for height
sil_global private @one-time initialization token for height : $Builtin.Word
初始化height的时候会调用swift_once,swift_once ->底层调用了gcd的dispatch_once_f
- 所以,swift单例可以这样写
class ShareInstance {
static let share: ShareInstance = ShareInstance() //static保证了share只被创建了一次
private init(){} //禁止使用init方法
}
FinalQuestion
- 要lazy到底有啥用?
节约资源 虽然你代码写了初始化一个类,并且也调用了该类的init方法,但是我暂时不会执行;等你真正用的时候我才去初始化,延迟加载了就节省资源了。 - swift的init方法里面做了什么,为什么在init方法里面赋值不会触发属性观察器
- 覆写父类的方法为什么还能调用父类的方法,可不可以不调用父类的方法
- lazy属性,如果把age变成class,你会发现大小没有变,那是因为
print(MemoryLayout<Optional<LazyClass>>.size)//8 print(MemoryLayout<Optional<LazyClass>>.stride)//8
,那么为什么类的可选型是8,而int的可选型为16呢。
size的作用:
并不能看出该类型在内存中实际使用的大小,因为size并不包含任何动态分配
而且 如果是class类型,那么不管用多少存储属性,他们的size都是一样的,占用8个字节
stride的作用:
对象实际在内存中占用的大小,经过了内存对齐,它的大小永远是份分配积极的,只多不少
// 结构体没有继承,它的内存大小是由它内部的存储变量决定的
// 类有继承,它的size永远是8
- 为什么延迟存储属性必须要初始化呢?
是因为本质是可选型吗? - 为什么延迟存储属性不是线程安全的呢?
多个线程同时访问延迟属性时,由于lazy_property内部存在可选判断会执行多个分支,不加锁当然就不是线程安全的喽。。。你看lazy var cls = LazyClass()
,其中LazyClass
就可能被创建多次
网友评论