本文主要介紹以下幾點(diǎn)
通過SIL來理解對象的創(chuàng)建
Swift類結(jié)構(gòu)分析
存儲屬性 & 計算屬性
延遲存儲屬性 & 單例創(chuàng)建方式
SIL
在底層流程中娶吞,OC代碼和SWift代碼時通過不同的編譯器
進(jìn)行編譯,然后通過LLVM
械姻,生成.o
可執(zhí)行文件妒蛇,如下所示
OC
中通過clang
編譯器(clang可以參考這篇文章iOS-底層原理 31:LLVM編譯流程 & Clang插件開發(fā)),編譯成IR楷拳,然后再生成可執(zhí)行文件.o(即機(jī)器碼)swift
中通過swiftc
編譯器绣夺,編譯成IR,然后再生成可執(zhí)行文件
下面是Swift中的編譯流程欢揖,其中SIL
(Swift Intermediate Language)陶耍,是Swift編譯過程中的中間代碼
,主要用于進(jìn)一步分析和優(yōu)化Swift代碼她混。如下圖所示烈钞,SIL
位于在AST
和LLVM
IR之間
注意:這里需要說明一下泊碑,Swift與OC的區(qū)別
在于 Swift生成了高級的SIL
我們可以通過swiftc -h
終端命令,查看swiftc的所有命令
例如:在main.swift文件定義如下代碼
class CJLTeacher{
var age: Int = 18
var name: String = "CJL"
}
var t = CJLTeacher()
- 查看抽象語法樹:
swiftc -dump-ast main.swift
- 生成SIL文件:
swiftc -emit-sil main.swift >> ./main.sil && code 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 @$s4main1tAA10CJLTeacherCvp // id: %2
//`global_addr`:獲取全局變量地址,并賦值給寄存器%3
%3 = global_addr @$s4main1tAA10CJLTeacherCvp : $*CJLTeacher // user: %7
//`metatype`獲取`CJLTeacher`的`MetaData`賦值給%4
%4 = metatype $@thick CJLTeacher.Type // user: %6
//將`__allocating_init`的函數(shù)地址賦值給 %5
// function_ref CJLTeacher.__allocating_init()
%5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %6
//`apply`調(diào)用 `__allocating_init` 初始化一個變量压昼,賦值給%6
%6 = apply %5(%4) : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %7
//將%6的值存儲到%3求冷,即全局變量的地址(這里與前面的%3形成一個閉環(huán))
store %6 to %3 : $*CJLTeacher // 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'
注意:code
命令是在.zshrc
中做了如下配置窍霞,可以在終端中指定軟件打開相應(yīng)文件
$ open .zshrc
//****** 添加以下別名
alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'
//****** 使用
$ code main.sil
//如果想SIL文件高亮匠题,需要安裝插件:VSCode SIL
-
從SIL文件中,可以看出但金,代碼是經(jīng)過混淆的韭山,可以通過以下命令還原,以
s4main1tAA10CJLTeacherCvp
為例:xcrun swift-demangle s4main1tAA10CJLTeacherCvp
-
在SIL文件中搜索
s4main10CJLTeacherCACycfC
冷溃,其內(nèi)部實現(xiàn)主要是分配內(nèi)存+初始化變量
-
allocing_ref
:創(chuàng)建一個CJLTeacher
的實例對象钱磅,當(dāng)前實例對象的引用計數(shù)為1
- 調(diào)用
init
方法
-
//********* main入口函數(shù)中代碼 *********
%5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher
// s4main10CJLTeacherCACycfC 實際就是__allocating_init()
// CJLTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher {
// %0 "$metatype"
bb0(%0 : $@thick CJLTeacher.Type):
// 堆上分配內(nèi)存空間
%1 = alloc_ref $CJLTeacher // user: %3
// function_ref CJLTeacher.init() 初始化當(dāng)前變量
%2 = function_ref @$s4main10CJLTeacherCACycfc : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %3
// 返回
%3 = apply %2(%1) : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %4
return %3 : $CJLTeacher // id: %4
} // end sil function '$s4main10CJLTeacherCACycfC'
SIL語言對于Swift源碼的分析是非常重要的,關(guān)于其更多的語法信息似枕,可以在這個網(wǎng)站進(jìn)行查詢
符號斷點(diǎn)調(diào)試
- 在demo中設(shè)置
_allocing_init
符號斷點(diǎn)
發(fā)現(xiàn)其內(nèi)部調(diào)用的是swift_allocObject
源碼調(diào)試
下面我們就通過swift_allocObject
來探索swift中對象的創(chuàng)建過程
-
在
REPL
(命令交互行盖淡,類似于python的,可以在這里編寫代碼)中編寫如下代碼(也可以拷貝)凿歼,并搜索swift_allocObject
函數(shù)加一個斷點(diǎn)褪迟,然后定義一個實例對象t
-
斷點(diǎn)斷住,查看左邊local有詳細(xì)的信息
其中
requiredSize
是分配的實際內(nèi)存大小,為40requiredAlignmentMask
是swift中的字節(jié)對齊方式答憔,這個和OC中是一樣的味赃,必須是8
的倍數(shù),不足的會自動補(bǔ)齊虐拓,目的是以空間換時間
心俗,來提高內(nèi)存操作效率
swift_allocObject 源碼分析
swift_allocObject
的源碼如下,主要有以下幾部分
- 通過
swift_slowAlloc
分配內(nèi)存,并進(jìn)行內(nèi)存字節(jié)對齊 - 通過
new + 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;
}
- 進(jìn)入
swift_slowAlloc
函數(shù)揪利,其內(nèi)部主要是通過malloc
在堆
中分配size大小的內(nèi)存空間
,并返回內(nèi)存地址
吠谢,主要是用于存儲實例變量
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;
}
- 進(jìn)入
HeapObject
初始化方法,需要兩個參數(shù):metadata工坊、refCounts
- 其中
metadata
類型是HeapMetadata
献汗,是一個指針類型,占8
字節(jié) -
refCounts
(引用計數(shù)王污,類型是InlineRefCounts
罢吃,而InlineRefCounts
是一個類RefCounts
的別名,占8
個字節(jié))昭齐,swift采用arc引用計數(shù)
- 其中
總結(jié)
-
對于實例對象
t
來說尿招,其本質(zhì)是一個HeapObject
結(jié)構(gòu)體,默認(rèn)16
字節(jié)內(nèi)存大汹寮荨(metadata
8字節(jié) +refCounts
8字節(jié))就谜,與OC的對比如下OC中實例對象的本質(zhì)是
結(jié)構(gòu)體
,是以objc_object
為模板繼承的里覆,其中有一個isa指針丧荐,占8
字節(jié)Swift中實例對象,默認(rèn)的比OC中多了一個
refCounted
引用計數(shù)大小喧枷,默認(rèn)屬性占16
字節(jié)
Swift中對象的內(nèi)存分配流程是:
__allocating_init --> swift_allocObject_ --> _swift_allocObject --> swift_slowAlloc --> malloc
init在其中的職責(zé)就是初始化變量虹统,這點(diǎn)與OC中是一致的
針對上面的分析,我們還遺留了兩個問題:metadata
是什么隧甚,40
是怎么計算的车荔?下面來繼續(xù)探索
在demo中,我們可以通過Runtime
方法獲取類的內(nèi)存大小
這點(diǎn)與在源碼調(diào)試時左邊local的requiredSize
值是相等的戚扳,從HeapObject
的分析中我們知道了忧便,一個類在沒有任何屬性的情況下,默認(rèn)占用16
字節(jié)大小帽借,
對于Int
珠增、String
類型,進(jìn)入其底層定義,兩個都是結(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é)(后面文章會進(jìn)行詳細(xì)講解),這點(diǎn)與OC中是有所區(qū)別的
所以這也解釋了為什么CJLTeacher
的內(nèi)存大小等于40
禀综,即40 = metadata(8字節(jié)) +refCount(8字節(jié))+ Int(8字節(jié))+ String(16字節(jié))
這里驗證了40
的來源简烘,但是metadata
是什么還不知道苔严,繼續(xù)往下分析
探索Swift中類的結(jié)構(gòu)
在OC中類是從objc_class
模板繼承過來的,具體的參考這篇文章iOS-底層原理 08:類 & 類結(jié)構(gòu)分析
而在Swift中孤澎,類的結(jié)構(gòu)在底層是HeapObject
届氢,其中有 metadata + refCounts
HeapMetadata類型分析
下面就來分析metadata,看看它到底是什么覆旭?
- 進(jìn)入
HeapMetadata
定義退子,是TargetHeapMetaData
類型的別名,接收了一個參數(shù)Inprocess
using HeapMetadata = TargetHeapMetaData<Inprocess>;
- 進(jìn)入
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
};
- 進(jìn)入
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
- 進(jìn)入
MetadataKind
定義,里面有一個#include "MetadataKind.def"
碗暗,點(diǎn)擊進(jìn)入颈将,其中記錄了所有類型的元數(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)強(qiáng)轉(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)前指針強(qiá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;
}
}
這一點(diǎn)死姚,我們可以通過lldb
來驗證
-
po metadata->getKind()
,得到其kind是Class -
po metadata->getClassObject()
勤篮、x/8g 0x0000000110efdc70都毒,這個地址中存儲的是元數(shù)據(jù)信息!
所以,TargetMetadata
和 TargetClassMetadata
本質(zhì)上是一樣的碰缔,因為在內(nèi)存結(jié)構(gòu)中账劲,可以直接進(jìn)行指針的轉(zhuǎn)換
,所以可以說,我們認(rèn)為的結(jié)構(gòu)體
瀑焦,其實就是TargetClassMetadata
- 進(jìn)入
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;
//運(yùn)行時保留字段
uint16_t Reserved;
//類的內(nèi)存大小
uint32_t ClassSize;
//類的內(nèi)存首地址
uint32_t ClassAddressPoint;
...
}
- 進(jìn)入
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對比
- 實例對象 & 類
OC中的
實例對象本質(zhì)
是結(jié)構(gòu)體
,是通過底層的objc_object
模板創(chuàng)建拣凹,類是繼承自objc_class
Swift中的
實例對象本質(zhì)
也是結(jié)構(gòu)體
森爽,類型是HeapObject
,比OC多了一個refCounts
- 方法列表
OC
中的方法存儲在objc_class
結(jié)構(gòu)體class_rw_t
的methodList
中swift
中的方法存儲在metadata
元數(shù)據(jù)中
- 引用計數(shù)
OC中的ARC維護(hù)的是
散列表
Swift中的ARC是對象內(nèi)部有一個
refCounts
屬性
Swift屬性
在swift中嚣镜,屬性主要分為以下幾種
存儲屬性
計算屬性
延遲存儲屬性
類型屬性
存儲屬性
存儲屬性爬迟,又分兩種:
要么是
常量存儲屬性
,即let
修飾要么是
變量存儲屬性
菊匿,即var
修飾
定義如下代碼
class CJLTeacher{
var age: Int = 18
var name: String = "CJL"
}
let t = CJLTeacher()
其中代碼中的age付呕、name
來說,都是變量存儲屬性
跌捆,這一點(diǎn)可以在SIL
中體現(xiàn)
class CJLTeacher {
//_hasStorage 表示是存儲屬性
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
存儲屬性特征:會占用占用分配實例對象的內(nèi)存空間
下面我們同斷點(diǎn)調(diào)試來驗證
po t
-
x/8g 內(nèi)存地址徽职,即HeapObject存儲的地址
計算屬性
計算屬性:是指不占用內(nèi)存空間,本質(zhì)是set/get方法的屬性
我們通過一個demo來說明佩厚,以下寫法正確嗎姆钉?
class CJLTeacher{
var age: Int{
get{
return 18
}
set{
age = newValue
}
}
}
在實際編程中,編譯器會報以下警告抄瓦,其意思是在age的set方法中又調(diào)用了age.set
然后運(yùn)行發(fā)現(xiàn)崩潰了潮瓶,原因是
age的set方法中調(diào)用age.set
導(dǎo)致了循環(huán)引用,即遞歸
驗證:不占內(nèi)存
對于其不占用內(nèi)存空間
這一特征钙姊,我們可以通過以下案例來驗證
毯辅,打印以下類的內(nèi)存大小
class Square{
var width: Double = 8.0
var area: Double{
get{
//這里的return可以省略,編譯器會自動推導(dǎo)
return width * width
}
set{
width = sqrt(newValue)
}
}
}
print(class_getInstanceSize(Square.self))
//********* 打印結(jié)果 *********
24
從結(jié)果可以看出類Square
的內(nèi)存大小是24
煞额,等于 (metadata + refCounts)類自帶16
字節(jié) + width(8字節(jié)) = 24思恐,是沒有加上area的赤屋。從這里可以證明 area屬性沒有占有內(nèi)存空間
。
驗證:本質(zhì)是set/get方法
- 將main.swift轉(zhuǎn)換為SIL文件:
swiftc -emit-sil main.swift >> ./main.sil
- 查看SIL文件壁袄,對于
存儲屬性
,有_hasStorage
的標(biāo)識符
class Square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get set }
@objc deinit
init()
}
- 對于計算屬性媚媒,
SIL
中只有setter嗜逻、getter
方法
屬性觀察者(didSet、willSet)
willSet
:新值存儲之前調(diào)用newValue
didSet
:新值存儲之后調(diào)用oldValue
驗證
- 可以通過demo來驗證
class CJLTeacher{
var name: String = "測試"{
//新值存儲之前調(diào)用
willSet{
print("willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("didSet oldValue \(oldValue)")
}
}
}
var t = CJLTeacher()
t.name = "CJL"
//**********打印結(jié)果*********
willSet newValue CJL
didSet oldValue 測試
- 也可以通過編譯來驗證缭召,將main.swift編譯成mail.sil栈顷,在sil文件中找
name
的set
方法
問題1:init方法中是否會觸發(fā)屬性觀察者?
以下代碼中嵌巷,init方法中設(shè)置name萄凤,是否會觸發(fā)屬性觀察者?
class CJLTeacher{
var name: String = "測試"{
//新值存儲之前調(diào)用
willSet{
print("willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("didSet oldValue \(oldValue)")
}
}
init() {
self.name = "CJL"
}
}
運(yùn)行結(jié)果發(fā)現(xiàn)搪哪,并沒有走willSet靡努、didSet中的打印方法,所以有以下結(jié)論:
- 在
init
方法中晓折,如果調(diào)用屬性惑朦,是不會觸發(fā)
屬性觀察者的 - init中主要是
初始化當(dāng)前變量
,除了默認(rèn)的前16個字節(jié)漓概,其他屬性會調(diào)用memset
清理內(nèi)存空間(因為有可能是臟數(shù)據(jù)漾月,即被別人用過),然后才會賦值
【總結(jié)】:初始化器(即init
方法設(shè)置)和定義時設(shè)置默認(rèn)值(即在didSet
中調(diào)用其他屬性值)都不會觸發(fā)
問題2:哪里可以添加屬性觀察者胃珍?
主要有以下三個地方可以添加:
- 1梁肿、
類
中定義的存儲屬性
- 2、通過類
繼承的存儲屬性
class CJLMediumTeacher: CJLTeacher{
override var age: Int{
//新值存儲之前調(diào)用
willSet{
print("willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
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{
//新值存儲之前調(diào)用
willSet{
print("willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("didSet oldValue \(oldValue)")
}
}
override var age2: Int{
//新值存儲之前調(diào)用
willSet{
print("willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("didSet oldValue \(oldValue)")
}
}
}
問題3:子類和父類的計算屬性同時存在didset吩蔑、willset時,其調(diào)用順序是什么填抬?
有以下代碼哥纫,其調(diào)用順序是什么?
class CJLTeacher{
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 CJLMediumTeacher: CJLTeacher{
override var age: Int{
//新值存儲之前調(diào)用
willSet{
print("子類 newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("子類 didSet oldValue \(oldValue)")
}
}
}
var t = CJLMediumTeacher()
t.age = 20
運(yùn)行結(jié)果如下:
結(jié)論:對于同一個屬性痴奏,子類和父類都有屬性觀察者蛀骇,其順序是:先子類willset,后父類willset读拆,在父類didset擅憔, 子類的didset,即:子父 父子
問題4:子類調(diào)用了父類的init檐晕,是否會觸發(fā)觀察屬性暑诸?
在問題3的基礎(chǔ)蚌讼,修改CJLMediumTeacher
類
class CJLMediumTeacher: CJLTeacher{
override var age: Int{
//新值存儲之前調(diào)用
willSet{
print("子類 willSet newValue \(newValue)")
}
//新值存儲之后調(diào)用
didSet{
print("子類 didSet oldValue \(oldValue)")
}
}
override init() {
super.init()
self.age = 20
}
}
//****** 打印結(jié)果 ******
子類 willSet newValue 20
父類 willSet newValue 20
父類 didSet oldValue 18
子類 didSet oldValue 18
從打印結(jié)果發(fā)現(xiàn),會觸發(fā)屬性觀察者个榕,主要是因為子類
調(diào)用了父類
的init
篡石,已經(jīng)初始化過了,而初始化流程保證了所有屬性都有值(即super.init
確保變量初始化完成了)西采,所以可以觀察屬性了
延遲屬性
延遲屬性主要有以下幾點(diǎn)說明:
1凰萨、使用
lazy
修飾的存儲屬性2、延遲屬性必須有一個默認(rèn)的初始值
3械馆、延遲存儲在第一次訪問的時候才被賦值
4胖眷、延遲存儲屬性并不能保證線程安全
5、延遲存儲屬性對實例對象大小的影響
下面來一一進(jìn)行分析
1霹崎、使用lazy修飾的存儲屬性
class CJLTeacher{
lazy var age: Int = 18
}
2珊搀、延遲屬性必須有一個默認(rèn)的初始值
如果定義為可選類型,則會報錯尾菇,如下所示
3境析、延遲存儲在第一次訪問的時候才被賦值
可以通過調(diào)試,來查看實例變量的內(nèi)存變化
- age
第一次訪問前
的內(nèi)存情況:此時的age是沒值
的派诬,為0x0
- age
第一次訪問后
的內(nèi)存情況:此時age是有值
的簿晓,為30
從而可以驗證,懶加載存儲屬性只有在第一次訪問時才會被賦值
我們也可以通過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
類型
-
setter+getter
:從getter方法中可以驗證放可,在第一次訪問時谒臼,就從沒值變成了有值的操作
通過sil,有以下兩點(diǎn)說明:
1耀里、
lazy
修飾的屬性蜈缤,在底層默認(rèn)是optional
,在沒有被訪問時冯挎,默認(rèn)是nil
底哥,在內(nèi)存中的表現(xiàn)就是0x0
。在第一次訪問過程中房官,調(diào)用
的是屬性的getter
方法趾徽,其內(nèi)部實現(xiàn)是通過當(dāng)前enum的分支,來進(jìn)行一個賦值操作
-
2翰守、可選類型是16字節(jié)嗎孵奶?可以通過
MemoryLayout
打印- size:實際大小
- stride:分配大小(主要是由于內(nèi)存對齊)
print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)
//*********** 打印結(jié)果 ***********
16
9
為什么實際大小是9
蜡峰?Optional
其本質(zhì)是一個enum
了袁,其中Int
占8
字節(jié)朗恳,另一個字節(jié)主要用于存儲case
值(這個后續(xù)會詳細(xì)講解)
4、延遲存儲屬性并不能保證線程安全
繼續(xù)分析3中sil文件载绿,主要是查看age的getter
方法粥诫,如果此時有兩個線程:
線程1
此時訪問age,其age是沒有值的崭庸,進(jìn)入bb2
流程然后時間片將CPU分配給了
線程2
怀浆,對于optional來說,依然是none
冀自,同樣可以走到bb2
流程所以,在此時秒啦,線程1會走一遍賦值熬粗,線程2也會走一遍賦值,并
不能保證屬性只初始化了一次
5余境、延遲存儲屬性對實例對象大小的影響
下面來繼續(xù)看下不使用lazy
的內(nèi)存與使用lazy
的內(nèi)存是否有變化驻呐?
-
不使用lazy
修飾的情況,類
的內(nèi)存大小是24
-
使用lazy
修飾的情況下芳来,類的內(nèi)存大小是32
從而可以證明含末,使用lazy和不使用lazy,其實例對象的內(nèi)存大小是不一樣的
類型屬性
類型屬性即舌,主要有以下幾點(diǎn)說明:
1佣盒、使用關(guān)鍵字
static
修飾,且是一個全局變量2顽聂、類型屬性必須有一個
默認(rèn)的初始值
3肥惭、類型屬性只會被
初始化一次
1、使用關(guān)鍵字static修飾
class CJLTeacher{
static var age: Int = 18
}
// **** 使用 ****
var age = CJLTeacher.age
生成SIL文件
- 查看定義紊搪,發(fā)現(xiàn)多了一個
全局變量
蜜葱,說以,類型屬性是一個全局變量
-
查看入口函數(shù)中age的獲取
-
查看age的getter方法
- 其中
globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
是全局變量初始化函數(shù)
-
builtin "once"
耀石,通過斷點(diǎn)調(diào)試牵囤,發(fā)現(xiàn)調(diào)用的是swift_once
,表示屬性只初始化一次
- 其中
- 源碼中搜索
swift_once
滞伟,其內(nèi)部是通過GCD
的dispatch_once_f 單例
實現(xiàn)揭鳞。從這里可以驗證上面的第3點(diǎn)
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、類型屬性必須有一個默認(rèn)的初始值
如下圖所示梆奈,如果沒有給默認(rèn)的初始值汹桦,會報錯
所以對于類型屬性
來說,一是全局變量
鉴裹,只初始化一次舞骆,二是線程安全的
單例的創(chuàng)建
//****** Swift單例 ******
class CJLTeacher{
//1钥弯、使用 static + let 創(chuàng)建聲明一個實例對象
static let shareInstance = CJLTeacher.init()
//2、給當(dāng)前init添加private訪問權(quán)限
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
總結(jié)
存儲屬性
會占用實例變量的內(nèi)存空間脆霎,且計算屬性
不會占用內(nèi)存空間,其本質(zhì)是set/get
方法-
屬性觀察者
willset
:新值存儲之前調(diào)用狈惫,先通知子類睛蛛,再通知父類(因為父類中可能需要做一些額外的操作),即子父
didSet
:新值存儲完成后胧谈,先告訴父類忆肾,再通知子類(父類的操作優(yōu)先于子類),即父子
類中的
init
方法賦值不會觸發(fā)
屬性觀察屬性可以添加在
類定義的存儲屬性菱肖、繼承的存儲屬性客冈、繼承的計算屬性
中子類調(diào)用父類的
init
方法,會觸發(fā)
觀察屬性
-
延遲存儲屬性
使用
lazy
修飾存儲屬性稳强,且必須有一個默認(rèn)值
只有在
第一次被訪問時才會被賦值
场仲,且是線程不安全
的使用lazy和不使用lazy,會
對實例對象的內(nèi)存大小有影響
退疫,主要是因為lazy在底層是optional
類型渠缕,optional的本質(zhì)是enum
,除了存儲屬性本身的內(nèi)存大小褒繁,還需要一個字節(jié)用于存儲case
-
類型屬性
使用
static
修飾亦鳞,且必須有一個默認(rèn)初始值
是一個全局變量,只會被
初始化一次
棒坏,是線程安全
的-
用于創(chuàng)建
單例
對象:使用
static + let
創(chuàng)建實例變量init
方法的訪問權(quán)限為private