Swift底层进阶--002:类、对象、属性

Swift内存分配过程

对象的内存分配过程,可以使用符号断点进行验证,下面演示如何为__allocating_init添加断点:

选择符号断点
添加__allocating_init
选择汇编模式

运行代码,来到__allocating_init函数执行内部,发现它做了两件事:

  • 调用swift_allocObject函数
  • 执行demo.LGTeacher.init方法,进行初始化变量
demo`LGTeacher.__allocating_init():
    0x1000029c0 <+0>:  pushq  %rbp
    0x1000029c1 <+1>:  movq   %rsp, %rbp
    0x1000029c4 <+4>:  pushq  %r13
    0x1000029c6 <+6>:  pushq  %rax
    0x1000029c7 <+7>:  movl   $0x28, %esi
    0x1000029cc <+12>: movl   $0x7, %edx
->  0x1000029d1 <+17>: movq   %r13, %rdi
//1、调用swift_allocObject函数
    0x1000029d4 <+20>: callq  0x100003be2               ; symbol stub for: swift_allocObject
    0x1000029d9 <+25>: movq   %rax, %r13
//2、执行demo.LGTeacher.init方法,进行初始化变量
    0x1000029dc <+28>: callq  0x100002a20               ; demo.LGTeacher.init() -> demo.LGTeacher at main.swift:10
    0x1000029e1 <+33>: addq   $0x8, %rsp
    0x1000029e5 <+37>: popq   %r13
    0x1000029e7 <+39>: popq   %rbp
    0x1000029e8 <+40>: retq

添加swift_allocObject断点,发现其内部依次调用_swift_allocObject_swift_slowAlloc两个函数

libswiftCore.dylib`swift_allocObject:
->  0x7fff6cd73d00 <+0>:  pushq  %rbp
    0x7fff6cd73d01 <+1>:  movq   %rsp, %rbp
    0x7fff6cd73d04 <+4>:  pushq  %rbx
    0x7fff6cd73d05 <+5>:  pushq  %rax
    0x7fff6cd73d06 <+6>:  movq   %rdi, %rbx
    0x7fff6cd73d09 <+9>:  movq   0x26d55b98(%rip), %rax    ; _swift_allocObject
//1、调用_swift_allocObject_t函数
    0x7fff6cd73d10 <+16>: leaq   0x1d69(%rip), %rcx        ; _swift_allocObject_
    0x7fff6cd73d17 <+23>: cmpq   %rcx, %rax
    0x7fff6cd73d1a <+26>: jne    0x7fff6cd73d39            ; <+57>
    0x7fff6cd73d1c <+28>: movq   %rsi, %rdi
    0x7fff6cd73d1f <+31>: movq   %rdx, %rsi
//2、调用swift_slowAlloc函数
    0x7fff6cd73d22 <+34>: callq  0x7fff6cd73c90            ; swift_slowAlloc
    0x7fff6cd73d27 <+39>: movq   %rbx, (%rax)
    0x7fff6cd73d2a <+42>: movq   $0x2, 0x8(%rax)
    0x7fff6cd73d32 <+50>: addq   $0x8, %rsp
    0x7fff6cd73d36 <+54>: popq   %rbx
    0x7fff6cd73d37 <+55>: popq   %rbp
    0x7fff6cd73d38 <+56>: retq   
    0x7fff6cd73d39 <+57>: movq   %rbx, %rdi
    0x7fff6cd73d3c <+60>: addq   $0x8, %rsp
    0x7fff6cd73d40 <+64>: popq   %rbx
    0x7fff6cd73d41 <+65>: popq   %rbp
    0x7fff6cd73d42 <+66>: jmpq   *%rax
    0x7fff6cd73d44 <+68>: nopw   %cs:(%rax,%rax)
    0x7fff6cd73d4e <+78>: nop

添加swift_slowAlloc断点,其内部最终调用了malloc函数

libswiftCore.dylib`swift_slowAlloc:
->  0x7fff6cd73c90 <+0>:  pushq  %rbp
    0x7fff6cd73c91 <+1>:  movq   %rsp, %rbp
    0x7fff6cd73c94 <+4>:  subq   $0x10, %rsp
    0x7fff6cd73c98 <+8>:  movq   %rdi, %rdx
    0x7fff6cd73c9b <+11>: cmpq   $0xf, %rsi
    0x7fff6cd73c9f <+15>: ja     0x7fff6cd73cb0            ; <+32>
    0x7fff6cd73ca1 <+17>: movq   %rdx, %rdi
//调用了malloc函数
    0x7fff6cd73ca4 <+20>: callq  0x7fff6cdf028c            ; symbol stub for: malloc
    0x7fff6cd73ca9 <+25>: testq  %rax, %rax
    0x7fff6cd73cac <+28>: jne    0x7fff6cd73cdb            ; <+75>
    0x7fff6cd73cae <+30>: jmp    0x7fff6cd73ce1            ; <+81>
    0x7fff6cd73cb0 <+32>: incq   %rsi
    0x7fff6cd73cb3 <+35>: movl   $0x10, %eax
    0x7fff6cd73cb8 <+40>: cmovneq %rsi, %rax
    0x7fff6cd73cbc <+44>: cmpq   $0x8, %rax
    0x7fff6cd73cc0 <+48>: movl   $0x8, %esi
    0x7fff6cd73cc5 <+53>: cmovaq %rax, %rsi
    0x7fff6cd73cc9 <+57>: leaq   -0x8(%rbp), %rdi
    0x7fff6cd73ccd <+61>: callq  0x7fff6cdf038e            ; symbol stub for: posix_memalign
    0x7fff6cd73cd2 <+66>: movq   -0x8(%rbp), %rax
    0x7fff6cd73cd6 <+70>: testq  %rax, %rax
    0x7fff6cd73cd9 <+73>: je     0x7fff6cd73ce1            ; <+81>
    0x7fff6cd73cdb <+75>: addq   $0x10, %rsp
    0x7fff6cd73cdf <+79>: popq   %rbp
    0x7fff6cd73ce0 <+80>: retq   
    0x7fff6cd73ce1 <+81>: callq  0x7fff6cdefb00            ; swift_slowAlloc.cold.1
    0x7fff6cd73ce6 <+86>: nopw   %cs:(%rax,%rax)

以上可以得出⼀个简单的结论:
swift内存分配过程:__allocating_init->swift_allocObject->_swift_allocObject_->swift_slowAlloc->malloc

实例对象的本质

使用VSCode打开Swift源码,找到_swift_allocObject_函数,添加断点:

_swift_allocObject_
_swift_allocObject_函数,负责创建当前实例对象,有下列3个参数。在其内部先后调用swift_slowAllocnew HeapObject两个函数

  • metadata:元数据
  • requiredSize:创建实例对象分配的实际内存大小,这里看到占用40字节
  • requiredAlignmentMask:字节对齐方式,必须为8的倍速。不足会自动补齐,以空间换取时间,提高访问效率。可以看到占用7字节

找到swift_slowAlloc函数:

swift_slowAlloc
swift_slowAlloc函数内部,又调用malloc,负责在堆中创建size大小的内存空间,并进行内存的字节对齐

回到_swift_allocObject_函数,当swift_slowAlloc完成创建内存的工作后,继续执行new HeapObject来进行初始化对象的工作,最终返回HeapObject结构体

了解一下HeapObject结构体:

HeapObject
实例对象初始化需要metadatarefCounts两个参数:

  • metadata:元数据,类型为HeapMetadata,是指针类型,占8字节
  • refCounts:引用计数,因为swift也是采用ARC进行内存管理。类型为InlineRefCounts,而InlineRefCountsClass类型,占8字节

实例对象的本质:

  • ocobjc_object结构体,默认有class类型的isa指针,占8字节
  • swiftHeapObject结构体,默认有元数据metadata、引用计数refCounts,占16字节
类结构的探索

找到HeapMetadata定义:

HeapMetadata
HeapMetadata针对TargetHeapMetadata类型取别名,而TargetHeapMetadata是模板类型,接收一个Inprocess参数,也是下文中的kind属性

找到TargetHeapMetaData定义:

TargetHeapMetaData
TargetHeapMetadata继承自TargetMetaDataTargetMetaData内部有kind属性,对于kind属性其实就是unsigned long类型,主要用于区分是哪种类型的元数据。

找到MetadataKind.def文件,里面记录了所有元数据类型,包含ClassStructEnum

Name Value
Class 0x0
Struct 0x200
Enum 0x201
Optional 0x2027
ForeignClass 0x203
Opaque 0x300
Tuple 0x301
Function 0x302
Existential 0x303
Metatype 0x304
ObjCClassWrapper 0x305
ExistentialMetatype 0x306
HeapLocalVariable 0x400
HeapGenericLocalVariable 0x500
ErrorObject 0x501
LastEnumerated 0x7FF

找到TargetMetaData定义:

TargetMetaData -getClassObject
除了包含kind属性外,还有一个getClassObject方法。该方法有个返回类型TargetClassMetadata,其内部通过kind去匹配上述枚举值。一旦匹配成功,将this,也就是当前Metadata的指针,强转相应类型。当前枚举为Class,所以this会被强转为ClassMetadata并返回

下面我们通过lldb进行验证

通过lldb验证

  • po metadata->getKind(),打印kind类型为Class
  • po metadata->getClassObject(),打印出元数据内存地址为0x000000010f4dfc88
  • x/8g 0x000000010f4dfc88,可以看到元数据里记录的信息

由此得出结论,其实当前的TargetMetadata就是TargetClassMetadata,因为在内存结构中,它们可以直接进行指针转换

找到TargetClassMetadata定义:

TargetClassMetadata
TargetClassMetadata继承自TargetAnyClassMetadata,所以拥有了父类的kindsuperclasscacheData等属性

找到TargetAnyClassMetadata定义:

TargetAnyClassMetadata

TargetAnyClassMetadata继承自TargetHeapMetadata,而TargetHeapMetadata又继承自TargetMetadata,所以拥有了父类的kind属性。

经过源码阅读,我们得出当前metadata的数据结构体如下:

struct swift_class_t : NSObject {
    void *kind; //8字节
    void *superClass; 
    void *cacheData 
    void *data 
    uint32_t flags; //4字节
    uint32_t instanceAddressOffset; //4字节
    uint32_t instanceSize;//4字节
    uint16_t instanceAlignMask; //2字节
    uint16_t reserved; //2字节
    uint32_t classSize; //4字节
    uint32_t classAddressOffset; //4字节
    void *description; 
    // ... 
}; 

类结构的探索:

  • metadatakind类型为Class,类结构继承关系如下:
    TargetClassMetadata->TargetAnyClassMetadata->TargetHeapMetadata->TargetMetaData
  • TargetMetaData类似oc中的objc_object,内含kind属性
  • HeapMetadata针对TargetHeapMetadata取别名,类似oc中的objc_class
  • TargetHeapMetadata为模板类型,接收一个Inprocess参数,也就是kind属性
  • kind属性为unsigned long类型,类似oc中的isa
Swift属性
存储属性:
  • 占用存储空间
  • 要么是let修饰的常量
  • 要么是var修饰的变量
class LGTeacher{
    let age: Int = 18
    var name: String = "Zang"
}

let t = LGTeacher()

上述代码中的agename,都是变量存储属性

通过SIL进行验证:

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

为什么说存储属性占用存储空间?

class LGTeacher{
    let age: Int = 18
    var name: String = "Zang"
}

let t = LGTeacher()

print("size of t.age: \(MemoryLayout.size(ofValue: t.age))")
print("size of t.name: \(MemoryLayout.size(ofValue: t.name))")
print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")

//输出以下内容:
//size of t.age: 8
//size of t.name: 16
//size of LGTeacher Class: 40

通过上面代码可以看出LGTeacher Class共占40字节

  • metadata:元数据,占8字节
  • refCounts:引用计数,占8字节
  • ageInt类型存储属性,占8字节
  • nameString类型存储属性,占16字节

再来使用pox/8g,查看HeapObject存储地址:

HeapObject存储地址
可以看出HeapObject存放了该类的元数据和引用计数,还存放了该类下的存储属性

计算属性
  • 不占⽤存储空间
  • 本质是get/set⽅法
class Square{
    var width: Double = 8.0
    var area: Double{
        get{
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

print("size of Square Class: \(class_getInstanceSize(Square.self))")

//输出以下内容:
//size of Square Class: 24

通过上面代码可以看出Square Class共占24字节

  • metadata:元数据,占8字节
  • refCounts:引用计数,占8字节
  • widthDouble类型存储属性,占8字节
  • area:计算属性,本质是get/set⽅法,不占⽤存储空间

通过SIL进行验证:

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

上述代码中,只有width属性被标记了_hasStorage,所以只有它是存储属性。而area属性有的只是get/set⽅法,所以它是计算属性

SIL中的getter⽅法

// Square.width.getter
sil hidden [transparent] @$s4main6SquareC5widthSdvg : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Square):
  debug_value %0 : $Square, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $Square, #Square.width // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*Double // users: %4, %5
  %4 = load %3 : $*Double                         // user: %6
  end_access %3 : $*Double                        // id: %5
  return %4 : $Double                             // id: %6
} // end sil function '$s4main6SquareC5widthSdvg'

SIL中的setter⽅法

// Square.width.setter
sil hidden [transparent] @$s4main6SquareC5widthSdvs : $@convention(method) (Double, @guaranteed Square) -> () {
// %0 "value"                                     // users: %6, %2
// %1 "self"                                      // users: %4, %3
bb0(%0 : $Double, %1 : $Square):
  debug_value %0 : $Double, let, name "value", argno 1 // id: %2
  debug_value %1 : $Square, let, name "self", argno 2 // id: %3
  %4 = ref_element_addr %1 : $Square, #Square.width // user: %5
  %5 = begin_access [modify] [dynamic] %4 : $*Double // users: %6, %7
  store %0 to %5 : $*Double                       // id: %6
  end_access %5 : $*Double                        // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function '$s4main6SquareC5widthSdvs'
属性观察者
  • willSet:新值存储前调用,可获取即将被更新的新值newValue
  • didSet:新值存储后调用,可获取被更新前的原始值oldValue
class LGTeacher{
    
    var name: String = "无"{
        willSet{
            print("willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
        }
        didSet{
            print("didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
        }
    }
}

var t = LGTeacher()
t.name = "Zang"

print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")

//输出以下内容:
//willSet-新值存储前调用,当前值:无,即将被更新为:Zang
//didSet-新值存储后调用,当前值:Zang,被更新前的原始值:无
//size of LGTeacher Class: 32

init方法中修改属性,能否触发属性观察者的willSetdidSet

//父类LGTeacher
class LGTeacher{
    
    var name: String = "无" {
        willSet{
            print("willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
        }
        didSet{
            print("didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
        }
    }
    
    init() {
        self.name = "Teacher"
    }
}
//子类LGChild
class LGChild : LGTeacher{
    
    override init() {
        super.init()
        self.name = "Child"
    }
}

var t = LGChild()

//输出以下内容:
//willSet-新值存储前调用,当前值:Teacher,即将被更新为:Child
//didSet-新值存储后调用,当前值:Child,被更新前的原始值:Teacher

对上述结果的打印,有些神奇的地方:

  • 父类init方法将name赋值为Teacher,但并没有触发willSetdidSet
    因为此时父类的初始化还未完成
  • 子类init方法将name赋值为Child,此时却触发了willSetdidSet
    因为此时super.init,也就是父类的初始化已完成
  • init方法会调用memset清理其他属性的内存空间(不包括metadatarefCounts),因为有可能是脏数据,被别人使用过,之后才会赋值。

能否在当前类的计算属性上,再添加属性观察者?


计算属性,能否添加属性观察者?

很明显,在当前类的计算属性上,无法再添加属性观察者。编译报错:“willSet cannot be provided together with a getter”

但我们可以通过类的继承,对父类的计算属性,通过子类添加属性观察者

class Square{
    var width: Double = 8.0
    
    var area: Double{
        get{
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

class LGChild : Square{

    override var area: Double{
        willSet{
        
            print("willSet-新值存储前调用,当前值:\(area),即将被更新为:\(newValue)")
        }
        didSet{
        
            print("didSet-新值存储后调用,当前值:\(area),被更新前的原始值:\(oldValue)")
        }
    }
}

var t = LGChild()
t.area=16;

//输出以下内容:
//willSet-新值存储前调用,当前值:64.0,即将被更新为:16.0
//didSet-新值存储后调用,当前值:16.0,被更新前的原始值:64.0

子类和父类能否同时存在willSetdidSet

class LGTeacher{

    var name: String = "无" {
        willSet{
            print("LGTeacher-willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
        }
        didSet{
            print("LGTeacher-didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
        }
    }
}

class LGChild : LGTeacher{

    override var name: String {
        willSet{
            print("LGChild-willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
        }
        didSet{
            print("LGChild-didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
        }
    }
}

var t = LGChild()
t.name="Zang";

//输出以下内容:
//LGChild-willSet-新值存储前调用,当前值:无,即将被更新为:Zang
//LGTeacher-willSet-新值存储前调用,当前值:无,即将被更新为:Zang
//LGTeacher-didSet-新值存储后调用,当前值:Zang,被更新前的原始值:无
//LGChild-didSet-新值存储后调用,当前值:Zang,被更新前的原始值:无

上述代码证明子类和父类,可以同时存在willSetdidSet
调用顺序:子类willSet->父类willSet->父类didSet->子类didSet

  • t是子类的实例对象,当name属性被修改,首先触发子类的willSet方法
  • 之后会调用父类的setter方法
  • 然后触发父类的willSetdidSet方法
  • 最后触发子类的didSet方法
延迟存储属性
  • 使⽤lazy修饰的存储属性
  • 延迟存储属性必须有⼀个默认初始值
  • 延迟存储属性在第⼀次访问的时候才会被赋值
  • 延迟存储属性并不能保证线程安全
  • 延迟存储属性会影响实例对象的⼤⼩
class LGTeacher{
    lazy var age: Int = 18
}

var t = LGTeacher()

print("age: \(t.age)")
print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")

//输出以下内容:
//age: 18
//size of LGTeacher Class: 32

对上述结果的打印,有些神奇的地方:
正常来说LGTeacher应该由metadatarefCountsInt属性age组成,共占24字节。但打印结果中的LGTeacher,为什么输出32字节?

继续通过SIL查看代码:

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

通过SIL可以看到age属性,由于设置lazy关键字的原因,被加上了final修饰符,类型变为Optional可选类型

  • OptionalOptional本质是enum占1字节,Int占8字节,故此Optional占9字节。加上metadatarefCounts共计25字节,再经过内存对齐,需要8的倍数,最终LGTeacher输出32字节。

再来使用po、x/8g,查看lazy属性首次访问的情况:

lazy属性第一次访问
很明显首次访问之前,内存地址是0x0,没有值。当使用t.age触发getter方法后,地址变成0x12,也就是18。

通过SIL的getter方法验证:

// LGTeacher.age.getter
sil hidden [lazy_getter] [noinline] @main.LGTeacher.age.getter : Swift.Int : $@convention(method) (@guaranteed LGTeacher) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $LGTeacher):
  debug_value %0 : $LGTeacher, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $LGTeacher, #LGTeacher.$__lazy_storage_$_age // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*Optional // users: %4, %5
  %4 = load %3 : $*Optional                  // user: %6
  end_access %3 : $*Optional                 // id: %5
//这里在验证age是否有值,有值进入bb1流程,没值进入bb2流程
  switch_enum %4 : $Optional, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  br bb3(%7 : $Int)                               // id: %9

bb2:                                              // Preds: bb0
//将初始化的默认值18赋值给age属性
  %10 = integer_literal $Builtin.Int64, 18        // user: %11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  %13 = enum $Optional, #Optional.some!enumelt, %11 : $Int // user: %16
  %14 = ref_element_addr %0 : $LGTeacher, #LGTeacher.$__lazy_storage_$_age // user: %15
  %15 = begin_access [modify] [dynamic] %14 : $*Optional // users: %16, %17
  store %13 to %15 : $*Optional              // id: %16
  end_access %15 : $*Optional                // id: %17
  br bb3(%11 : $Int)                              // id: %18

// %19                                            // user: %20
bb3(%19 : $Int):                                  // Preds: bb2 bb1
  return %19 : $Int                               // id: %20
} // end sil function 'main.LGTeacher.age.getter : Swift.Int'
  • getter方法中,读取age属性的值。再通过case #Optional.some!enumelt判断age属性是否有值,有值进入bb1流程,没值进入bb2流程。首次使用age属性没有值,进入bb2将初始化的默认值18赋值给age属性。
  • 这段代码也可以看出lazy并不能保证线程安全。当多线程同时对age属性赋值时,getter方法中case #Optional.some!enumelt判断,很可能多次进入bb2流程,造成多次初始化的情况。
类型属性
  • 使⽤static来声明⼀个类型属性
  • 类型属性属于这个类的本身,不管有多少个实例,类型属性只有⼀份
  • 类型属性必须有⼀个默认初始值
  • 类型属性只会被初始化一次
  • 类型属性是线程安全的
class LGTeacher{
    static var age: Int = 18
}

var t = LGTeacher()
//print("age: \(t.age)")
print("age: \(LGTeacher.age)")
print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")

//输出以下内容:
//age: 18
//size of LGTeacher Class: 16
  • 类型属性必须有初始值,否则编译报错:“static var declaration requires an initializer expression or getter/setter specifier”
  • 类型属性必须通过LGTeacher.age访问,不能通过t.age访问,后者编译报错:“Static member age cannot be used on instance of type LGTeacher”
  • LGTeacher输出16字节,说明LGTeacher里不包含类型属性的存储空间

通过SIL进行验证:

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

// globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
sil_global private @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $Builtin.Word

// static LGTeacher.age
sil_global hidden @static main.LGTeacher.age : Swift.Int : $Int

通过最后一行代码就能看出,age属性变成全局变量,所以说类型属性其实就是一个全局变量

添加符号断点查看汇编代码,可以看到调用了一个swift_once方法,它就是GCD的单例方法dispatch_once_f。所以说类型属性只会初始化一次,并且它是线程安全的

swift_once

正确声明⼀个单利:

class LGTeacher{

    static let shareInstance = LGTeacher.init()
    private init(){ }
}

var t=LGTeacher.shareInstance
  • 使用static+let初始化实例对象
  • 设置init方法为private私有访问权限

你可能感兴趣的:(Swift底层进阶--002:类、对象、属性)