Swift編譯過程
編譯過程(OC垦搬、Swift的區(qū)別)
-
OC
中通過clang
編譯器呼寸,編譯成IR,然后再生成可執(zhí)行文件.o(即機器碼) -
swift
中通過swiftc
編譯器猴贰,編譯成IR对雪,然后再生成可執(zhí)行文件
iOS開發(fā)語言,不管是OC還是Swift米绕,后端都是通過LLVM
進行編譯的瑟捣,如下圖所示
Swift編譯過程
下面是Swift中的編譯流程,其中SIL
(Swift Intermediate Language)栅干,是Swift編譯過程中的中間代碼
迈套,主要用于進一步分析和優(yōu)化Swift代碼。如下圖所示碱鳞,SIL
位于在AST
和LLVM
IR之間
我們可以通過swiftc -h
終端命令桑李,查看swiftc的所有命令
例如:在main.swift文件定義如下代碼
class YCTeacher {
var age: Int = 18
var name: String = "teacher"
}
var t = YCTeacher()
- 查看抽象語法樹:
swiftc -dump-ast main.swift
swift抽象語法樹@2x - 生成
SIL
文件(Swift Intermediate Language):swiftc -emit-sil main.swift >> ./main.sil
,其中main的入口函數(shù)如下:
/ main
// `@main`:標(biāo)識當(dāng)前main.swift的`入口函數(shù)`窿给,SIL中的標(biāo)識符名稱以`@`作為前綴
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
// `%0贵白、%1` 在SIL中叫做寄存器,可以理解為開發(fā)中的常量崩泡,一旦賦值就不可修改禁荒,如果還想繼續(xù)使用,就需要不斷的累加數(shù)字(注意:這里的寄存器角撞,與`register read`中的寄存器是有所區(qū)別的呛伴,這里是指`虛擬寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
// `alloc_global`:創(chuàng)建一個`全局變量`谒所,即代碼中的`t`
alloc_global @$s4main1tAA9YCTeacherCvp // id: %2
// `global_addr`:獲取全局變量地址磷蜀,并賦值給寄存器%3
%3 = global_addr @$s4main1tAA9YCTeacherCvp : $*YCTeacher // user: %7
// `metatype`獲取`YCTeacher`的`MetaData`賦值給%4
%4 = metatype $@thick YCTeacher.Type // user: %6
// 將`__allocating_init`的函數(shù)地址賦值給 %5
// function_ref YCTeacher.__allocating_init()
%5 = function_ref @$s4main9YCTeacherCACycfC : $@convention(method) (@thick YCTeacher.Type) -> @owned YCTeacher // user: %6
// `apply`調(diào)用 `__allocating_init` 初始化一個變量,賦值給%6
%6 = apply %5(%4) : $@convention(method) (@thick YCTeacher.Type) -> @owned YCTeacher // user: %7
// 將%6的值存儲到%3百炬,即全局變量的地址(這里與前面的%3形成一個閉環(huán))
store %6 to %3 : $*YCTeacher // id: %7
// 構(gòu)建`Int`,并`return`
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
return %9 : $Int32 // id: %10
} // end sil function 'main'
- 從SIL文件中污它,可以看出剖踊,代碼是經(jīng)過混淆的,可以通過
xcrun swift-demangle
還原
xcrun swift-demangle s4main1tAA9YCTeacherCvp
$s4main1tAA9YCTeacherCvp ---> main.t : main.YCTeacher
-
SIL
更多語法信息衫贬,可參考github地址 - 在
SIL
文件中搜索s4main9YCTeacherC3age4nameACSi_SStcfC
德澈,其內(nèi)部主要是分配內(nèi)存+初始化變量
// ************* main入口函數(shù)中的代碼 ****************
// function_ref YCTeacher.__allocating_init(age:name:)
%13 = function_ref @$s4main9YCTeacherC3age4nameACSi_SStcfC : $@convention(method) (Int, @owned String, @thick YCTeacher.Type) -> @owned YCTeacher // user: %14
// s4main9YCTeacherC3age4nameACSi_SStcfC 實際上就是__allocating_init(age:name:)
// YCTeacher.__allocating_init(age:name:)
sil hidden [exact_self_class] @$s4main9YCTeacherC3age4nameACSi_SStcfC : $@convention(method) (Int, @owned String, @thick YCTeacher.Type) -> @owned YCTeacher {
// %0 "age" // user: %5
// %1 "name" // user: %5
// %2 "$metatype"
bb0(%0 : $Int, %1 : $String, %2 : $@thick YCTeacher.Type):
// 堆上分配內(nèi)存空間
%3 = alloc_ref $YCTeacher // user: %5
// function_ref YCTeacher.init(age:name:) 初始化當(dāng)前變量
%4 = function_ref @$s4main9YCTeacherC3age4nameACSi_SStcfc : $@convention(method) (Int, @owned String, @owned YCTeacher) -> @owned YCTeacher // user: %5
%5 = apply %4(%0, %1, %3) : $@convention(method) (Int, @owned String, @owned YCTeacher) -> @owned YCTeacher // user: %6
return %5 : $YCTeacher // id: %6
} // end sil function '$s4main9YCTeacherC3age4nameACSi_SStcfC'
對象的創(chuàng)建過程
符號斷點調(diào)試
- 在工程中添加
__allocating_init
符號斷點
- 發(fā)現(xiàn)其內(nèi)部調(diào)用的是
swift_allocObject
源碼調(diào)試
下面我們通過swift_allocObject
來探索swift中對象的創(chuàng)建過程
- 在
REPL
(Read Eval PrintLoop)swift交互式解釋器中編寫代碼,也可以拷貝固惯,并在HeapObject.cpp
文件中搜索swift_allocObject
函數(shù)加一個斷點梆造,然后定義一個實例對象t
- 其中
requiredSize
是分配的實際內(nèi)存大小,40
-
requiredAlignmentMask
是swift中的字節(jié)對齊方式,這個和OC是一樣的镇辉,必須是8
的倍數(shù)屡穗,不足的會自動補齊,目的是以空間換時間
忽肛,來提高內(nèi)存操作效率
swift_allocObject源碼分析
swift_allocObject
源碼如下村砂,主要有以下幾個部分
- 通過
swift_slowAlloc
分配內(nèi)存,并進行內(nèi)存字節(jié)對齊 - 通過
new (object) HeapObject(metadata);
初始化一個實例對象 - 函數(shù)的返回值是
HeapObject
類型屹逛,所以當(dāng)前對象的內(nèi)存結(jié)構(gòu)
就是HeapObject
的內(nèi)存結(jié)構(gòu)
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
size_t requiredSize,
size_t requiredAlignmentMask) {
assert(isAlignmentMask(requiredAlignmentMask));
auto object = reinterpret_cast<HeapObject *>(
swift_slowAlloc(requiredSize, requiredAlignmentMask)); // 分配內(nèi)存础废、字節(jié)對齊
// NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
// check on the placement new allocator which we have observed on Windows,
// Linux, and macOS.
new (object) HeapObject(metadata); // 初始化一個實例對象
// If leak tracking is enabled, start tracking this object.
SWIFT_LEAKS_START_TRACKING_OBJECT(object);
SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);
return object;
}
- 在
Heap.cpp
文件中,進入swift_slowAlloc
函數(shù)罕模,其內(nèi)部主要是通過malloc
在堆
中分配size大小的內(nèi)存空間
评腺,并返回內(nèi)存地址p
,主要是用來存儲實例變量
void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
void *p;
// This check also forces "default" alignment to use AlignedAlloc.
if (alignMask <= MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
p = malloc(size); // 堆中創(chuàng)建size大小的內(nèi)存空間淑掌,用于存儲實例變量
#endif
} else {
size_t alignment = (alignMask == ~(size_t(0)))
? _swift_MinAllocationAlignment
: alignMask + 1;
p = AlignedAlloc(size, alignment);
}
if (!p) swift::crash("Could not allocate memory.");
return p;
}
- 進入
HeapObject
初始化方法蒿讥,需要兩個參數(shù)metadata
、refCounts
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata;
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
#ifndef __swift__
HeapObject() = default;
// Initialize a HeapObject header as appropriate for a newly-allocated object.
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{ }
- 其中
metadata
類型是HeapMetadata
锋拖,是一個指針類型诈悍,占8
字節(jié)大小 -
refCounts
(引用計數(shù),類型是InlineRefCounts
兽埃,而InlineRefCounts
是一個類RefCounts
的別名侥钳,占8個字節(jié)),swift采用arc引用計數(shù)
RefCounts
總結(jié)
- 對于實例對象
t
來說柄错,其本質(zhì)是一個HeapObject
結(jié)構(gòu)體舷夺,默認16
字節(jié)內(nèi)存大小(metadata
8字節(jié) +refCounts
8字節(jié))售貌,與OC的對比如下- OC中實例對象的本質(zhì)是
結(jié)構(gòu)體
给猾,是以objc_object
為模板繼承的,其中有一個isa
指針颂跨,占8字節(jié) - Swift中實例對象敢伸,默認的比OC中多了一個
refCounted
引用計數(shù)大小,默認屬性占16
字節(jié)
- OC中實例對象的本質(zhì)是
- Swift中對象的內(nèi)存分配流程是:
__allocating_init --> swift_allocObject_ --> _swift_allocObject --> swift_slowAlloc --> malloc
-
init
在其中的職責(zé)就是初始化變量恒削,這點與OC中是一致的
針對上面的分析池颈,我們還遺留了兩個問題:metadata
是什么,40
是怎么計算的钓丰?下面來繼續(xù)探索
在demo中躯砰,我們可以通過Runtime
方法獲取類的內(nèi)存大小
這點與在源碼調(diào)試時左邊local的requiredSize
值是相等的,從HeapObject
的分析中我們知道了携丁,一個類在沒有任何屬性的情況下琢歇,默認占用16
字節(jié)大小
對于Int
、String
類型,進入其底層定義,兩個都是結(jié)構(gòu)體類型李茫,那么是否都是8字節(jié)呢揭保?可以通過打印其內(nèi)存大小來驗證
//********* Int底層定義 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}
//********* String底層定義 *********
@frozen public struct String {...}
//********* 驗證 *********
print(MemoryLayout<Int>.stride)
print(MemoryLayout<String>.stride)
//********* 打印結(jié)果 *********
8
16
- 從打印的結(jié)果中可以看出,
Int
類型占8
字節(jié)涌矢,String
類型占16
字節(jié)(后面文章會進行詳細講解)掖举,這點與OC中是有所區(qū)別的
所以這也解釋了為什么YCTeacher
的內(nèi)存大小等于40
,即40 = metadata(8字節(jié)) +refCount(8字節(jié))+ Int(8字節(jié))+ String(16字節(jié))
探索Swift中類的結(jié)構(gòu)
- 在OC中類是從
objc_class
模板繼承過來的 - 在Swift中娜庇,類的結(jié)構(gòu)在底層是
HeapObject
塔次,其中有 metadata + refCounts
HeapMetadata類型分析
- 進入
HeapMetadata
定義,是TargetHeapMetaData
類型的別名名秀,接收了一個參數(shù)Inprocess
using HeapMetadata = TargetHeapMetaData<Inprocess>;
- 進入
TargetHeapMetaData
定義励负,其本質(zhì)是一個模板類型
,其中定義了一些所需的數(shù)據(jù)結(jié)構(gòu)匕得。這個結(jié)構(gòu)體中沒有屬性继榆,只有初始化
方法,傳入了一個MetadataKind
類型的參數(shù)(該結(jié)構(gòu)體沒有汁掠,那么只有在父類中了)這里的kind
就是傳入的Inprocess
//模板類型
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
using HeaderType = TargetHeapMetadataHeader<Runtime>;
TargetHeapMetadata() = default;
//初始化方法
constexpr TargetHeapMetadata(MetadataKind kind)
: TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
: TargetMetadata<Runtime>(isa) {}
#endif
};
- 進入
TargetMetaData
定義略吨,有一個kind
屬性,kind
的類型就是之前傳入的Inprocess
考阱。從這里可以得出翠忠,對于kind
,其類型就是unsigned long
乞榨,主要用于區(qū)分是哪種類型的元數(shù)據(jù)
//******** TargetMetaData 定義 ********
struct TargetMetaData{
using StoredPointer = typename Runtime: StoredPointer;
...
StoredPointer kind;
}
//******** Inprocess 定義 ********
struct Inprocess{
...
using StoredPointer = uintptr_t;
...
}
//******** uintptr_t 定義 ********
typedef unsigned long uintptr_t;
從TargetHeapMetadata
秽之、TargetMetaData
定義中,均可以看出初始化方法中參數(shù)kind
的類型是MetadataKind
- 進入
MetadataKind
定義吃既,里面有一個#include "MetadataKind.def"
考榨,點擊進入,其中記錄了所有類型的元數(shù)據(jù)
鹦倚,所以kind
種類總結(jié)如下
name | value |
---|---|
Class | 0x0 |
Struct | 0x200 |
Enum | 0x201 |
Optional | 0x202 |
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
結(jié)構(gòu)體定義中河质,找方法getClassObject
,在該方法中去匹配kind
返回值是TargetClassMetadata
類型, 如果是Class
震叙,則直接對this
(當(dāng)前指針掀鹅,即metadata)強轉(zhuǎn)為ClassMetadata
const TargetClassMetadata<Runtime> *getClassObject() const;
//******** 具體實現(xiàn) ********
template<> inline const ClassMetadata *
Metadata::getClassObject() const {
//匹配kind
switch (getKind()) {
//如果kind是class
case MetadataKind::Class: {
// Native Swift class metadata is also the class object.
//將當(dāng)前指針強轉(zhuǎn)為ClassMetadata類型
return static_cast<const ClassMetadata *>(this);
}
case MetadataKind::ObjCClassWrapper: {
// Objective-C class objects are referenced by their Swift metadata wrapper.
auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
return wrapper->Class;
}
// Other kinds of types don't have class objects.
default:
return nullptr;
}
}
這一點,我們可以通過lldb
來驗證
-
po metadata->getKind()
捐友,得到其kind是Class -
po metadata->getClassObject()
、x/8g 0x0000000110efdc70溃槐,這個地址中存儲的是元數(shù)據(jù)信息!
所以匣砖,TargetMetadata
和 TargetClassMetadata
本質(zhì)上是一樣的,因為在內(nèi)存結(jié)構(gòu)中,可以直接進行指針的轉(zhuǎn)換
猴鲫,所以可以說对人,我們認為的結(jié)構(gòu)體
,其實就是TargetClassMetadata
- 進入
TargetClassMetadata
定義拂共,繼承自TargetAnyClassMetadata
牺弄,有以下這些屬性,這也是類結(jié)構(gòu)的部分
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
...
//swift特有的標(biāo)志
ClassFlags Flags;
//實例對象內(nèi)存大小
uint32_t InstanceSize;
//實例對象內(nèi)存對齊方式
uint16_t InstanceAlignMask;
//運行時保留字段
uint16_t Reserved;
//類的內(nèi)存大小
uint32_t ClassSize;
//類的內(nèi)存首地址
uint32_t ClassAddressPoint;
...
}
- 進入
TargetAnyClassMetadata
定義宜狐,繼承自TargetHeapMetadata
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
...
ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
TargetPointer<Runtime, void> CacheData[2];
StoredSize Data;
...
}
總結(jié)
綜上所述势告,當(dāng)metadata
的kind
為Class時,有如下繼承鏈:
- 當(dāng)前類返回的實際類型是
TargetClassMetadata
,而TargetMetaData
中只有一個屬性kind
抚恒,TargetAnyClassMetaData
中有4個屬性咱台,分別是kind
,superclass
俭驮,cacheData
回溺、data
(圖中未標(biāo)出) - 當(dāng)前
Class在內(nèi)存中所存放的屬性
由TargetClassMetadata
屬性 +TargetAnyClassMetaData
屬性 +TargetMetaData
屬性 構(gòu)成,所以得出的metadata的數(shù)據(jù)結(jié)構(gòu)體如下所示
struct swift_class_t: NSObject{
void *kind;//相當(dāng)于OC中的isa混萝,kind的實際類型是unsigned long
void *superClass;
void *cacheData;
void *data;
uint32_t flags; //4字節(jié)
uint32_t instanceAddressOffset;//4字節(jié)
uint32_t instanceSize;//4字節(jié)
uint16_t instanceAlignMask;//2字節(jié)
uint16_t reserved;//2字節(jié)
uint32_t classSize;//4字節(jié)
uint32_t classAddressOffset;//4字節(jié)
void *description;
...
}
與OC的區(qū)別
-
實例對象 & 類
- OC中的
實例對象本質(zhì)
是結(jié)構(gòu)體
遗遵,是通過底層的objc_object
模板創(chuàng)建,類是繼承自objc_class
- Swift中的
實例對象本質(zhì)
也是結(jié)構(gòu)體
逸嘀,類型是HeapObject
车要,比OC多了一個refCounts
- OC中的
-
方法列表
- OC中的方法存儲在
objc_class
結(jié)構(gòu)體class_rw_t
的methodList
中 - swift中的方法存儲在
metadata
元數(shù)據(jù)中
- OC中的方法存儲在
-
引用計數(shù)
- OC中的ARC維護的是
散列表
- Swift中的ARC是對象內(nèi)部有一個
refCounts
屬性
- OC中的ARC維護的是
Swift屬性
在swift中,屬性主要分為以下幾種
- 存儲屬性
- 計算屬性
- 延遲存儲屬性
- 類型屬性
存儲屬性
存儲屬性分為兩種:
- 常量存儲屬性厘熟,用
let
修飾 - 變量存儲屬性屯蹦,用
var
修飾
存儲屬性特征:會占用分配實例對象的內(nèi)存空間
計算屬性
計算屬性是不占用內(nèi)存空間
的,本質(zhì)是set/get
方法
屬性觀察者(didSet绳姨、willSet)
-
willSet
:新值存儲之前調(diào)用newValue
-
didSet
:新值存儲之后調(diào)用oldValue
問題1:init方法中是否會觸發(fā)屬性觀察者登澜?
以下代碼中,init方法中設(shè)置name飘庄,是否會觸發(fā)屬性觀察者脑蠕?
class YCTeacher{
var name: String = "測試"{
//新值存儲之前調(diào)用
willSet{
print("willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("didSet oldValue \(oldValue)")
}
}
init() {
self.name = "teacher"
}
}
運行結(jié)果發(fā)現(xiàn),并沒有走willSet跪削、didSet中的打印方法谴仙,所以有以下結(jié)論:
- 在
init
方法中,如果調(diào)用屬性碾盐,是不會觸發(fā)
屬性觀察者的 - init中主要是
初始化當(dāng)前變量
晃跺,除了默認的前16個字節(jié),其他屬性會調(diào)用memset
清理內(nèi)存空間(因為有可能是臟數(shù)據(jù)毫玖,即被別人用過)掀虎,然后才會賦值
問題2:哪里可以添加屬性觀察者凌盯?
主要有以下三個地方可以添加:
- 1、類中定義的存儲屬性
- 2烹玉、通過類繼承的存儲屬性
- 3驰怎、通過類繼承的計算屬性
問題3:子類和父類的屬性同時存在didset、willset時二打,其調(diào)用順序是什么县忌?
class YCTeacher{
var age: Int = 18{
//新值存儲之前調(diào)用
willSet{
print("父類 willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("父類 didSet oldValue \(oldValue)")
}
}
var age2: Int {
get{
return age
}
set{
self.age = newValue
}
}
}
class YCMediumTeacher: YCTeacher{
override var age: Int{
//新值存儲之前調(diào)用
willSet{
print("子類 newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("子類 didSet oldValue \(oldValue)")
}
}
}
var t = YCMediumTeacher()
t.age = 20
運行結(jié)果如下:
結(jié)論:對于同一個屬性,子類和父類都有屬性觀察者继效,其順序是:先子類willset症杏,后父類willset,再父類didset莲趣, 子類的didset鸳慈,即:子父 父子
延遲存儲屬性
- 使用
lazy
修飾的存儲屬性 - 延遲屬性必須有一個默認的初始值
- 延遲存儲在第一次訪問的時候才被賦值
- 延遲存儲屬性并不能保證線程安全
- 延遲存儲屬性對實例對象大小的影響
類型屬性
- 使用關(guān)鍵字
static
修飾,且是一個全局變量
- 類型屬性必須有一個
默認的初始值
- 類型屬性只會被
初始化一次
喧伞,線程安全
單例的寫法
//****** Swift單例 ******
class YCTeacher{
//1走芋、使用 static + let 創(chuàng)建聲明一個實例對象
static let shareInstance = YCTeacher.init()
//2、給當(dāng)前init添加private訪問權(quán)限
private init(){ }
}
//使用(只能通過單例潘鲫,不能通過init)
var t = YCTeacher.shareInstance
//****** OC單例 ******
@implementation YCTeacher
+ (instancetype)shareInstance{
static YCTeacher *shareInstance = nil;
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[YCTeacher alloc] init];
});
return shareInstance;
}
@end
總結(jié)
-
存儲屬性
會占用實例變量的內(nèi)存空間 -
計算屬性
不會占用內(nèi)存空間翁逞,其本質(zhì)是set/get
方法 - 屬性觀察者
-
willset
:新值存儲之前調(diào)用,先通知子類溉仑,再通知父類(因為父類中可能需要做一些額外的操作)挖函,即子父
-
didSet
:新值存儲完成后,先告訴父類浊竟,再通知子類(父類的操作優(yōu)先于子類)怨喘,即父子
- 類中的
init
方法賦值不會觸發(fā)
屬性觀察 - 屬性可以添加在
類定義的存儲屬性、繼承的存儲屬性振定、繼承的計算屬性
中 - 子類調(diào)用父類的
init
方法必怜,會觸發(fā)
觀察屬性
-
- 延遲存儲屬性
- 使用
lazy
修飾存儲屬性,且必須有一個默認值
- 只有在
第一次被訪問時才會被賦值
后频,且是線程不安全
的 - 使用lazy和不使用lazy梳庆,會
對實例對象的內(nèi)存大小有影響
,主要是因為lazy在底層是optional
類型卑惜,optional的本質(zhì)是enum
膏执,除了存儲屬性本身的內(nèi)存大小,還需要一個字節(jié)用于存儲case
- 使用
- 類型屬性
- 使用
static
修飾露久,且必須有一個默認初始值
- 是一個全局變量更米,只會被
初始化一次
,是線程安全
的 - 用于創(chuàng)建
單例
對象:- 使用
static + let
創(chuàng)建實例變量 -
init
方法的訪問權(quán)限為private
- 使用
- 使用