在 Swift 的標準庫中,絕大多數的公開類型都是結構體,而枚舉和類只佔很小一部分。比如 Bool、Int、Double、 String、Array、Dictionary 等常見類型都是結構體。
我們現在定義一個結構體。
struct SHPerson {var age: Intvar name: String}
let p = SHPerson(age: 18, name: "Coder_張三")(滑動顯示更多)
所有的結構體都有一個編譯器自動生成的初始化器(initializer,初始化方法、構造器、構造方法)。如上代碼,可以傳入所有成員值,用以初始化所有成員(存儲屬性,Stored Property)。
編譯器根據情況,可能會為結構體生成多個初始化器。前提是保證 所有成員都有初始值。
一但在定義結構體時自定義初始化器,編譯器就不會幫自動生成其他初始化器。
struct SHPerson {var age: Intvar name: String
init(age: Int) {self.age = ageself.name = "Coder_張三" }}
let p = SHPerson(age: 0)(滑動顯示更多)
在對結構體進行初始化的時候,必須保證結構體的成員都值,所以當我們對結構體的某個成員變量設置初始值時,生成的初始化器可以不用傳該成員變量的參數賦值。
我們來看下面這個結構體,SHPerson 有 age,weight,sex 三個成員。
struct SHPerson {var age: Intvar weight: Intvar sex: Bool}
print(MemoryLayout<SHPerson>.size) // 17print(MemoryLayout<SHPerson>.stride) // 24print(MemoryLayout<SHPerson>.alignment) // 8(滑動顯示更多)
列印出其內存對齊字節數和佔用的內存,在 64 位系統下,結構體中 Int 佔 8 字節,Bool 佔一個字節 ,所以 SHPerson 一共佔 17 個字節,但是因為要遵守內存對齊原則(8個字節),所以系統會分配 24 個字節來存儲 SHPerson。
類的定義和結構體類似,但編譯器並沒有為類自動生成可以傳入成員值的初始化器。
當類的成員沒有初始值時,必須自定義初始化器,初始化成員值。
class SHPerson {var age: Intvar name: String
init(age: Int, name: String) {self.age = age;self.name = name; }}
let p = SHPerson(age: 18, name: "Coder_張三")(滑動顯示更多)
如果類的所有成員都在定義的時候指定了初始值,編譯器會為類生成無參的初始化器,成員的初始化是在這個初始化器中完成的。
class SHPerson {var age: Int = 18var name: String = "Coder_張三"}
let p = SHPerson()(滑動顯示更多)
當初始化的值不滿足某個條件時我們需要給初始化方法返回一個nil,那麼可以在 init 後面加上一個可選項來修飾。
class SHPerson {var age: Intvar name: String
init?(age: Int, name: String) {if age < 18 { return nil}self.age = ageself.name = name }}
let p1 = SHPerson(age: 16, name: "Coder_ 張三")let p2 = SHPerson(age: 18, name: "Coder_李四")print("p1 - \(String(describing: p1))")print("p2 - \(String(describing: p2))")(滑動顯示更多)
列印結果:p1 - nilp2 - Optional(_1_結構體與類.SHPerson)(滑動顯示更多)
例如,當 SHPerson 不滿 18 歲時返回 nil,屬於未成年人。
必要初始化器需要在 init 前用 required 修飾。
class SHPerson {var age: Intvar name: String// 父類定義必要實現初始化器required init(age: Int, name: String) {self.age = ageself.name = name }}
class SHStudent: SHPerson {var height: Int
init(height: Int) {self.height = heightsuper.init(age: 18, name: "Coder_ 張三") }
// 子類必須實現父類的必要初始化器required init(age: Int, name: String) {fatalError("init(age:name:) has not been implemented") }}(滑動顯示更多)
如代碼所示,當在 init 前修飾 required,該類的子類都必須實現該初始化器。
我們可以為類提供一個便捷初始化器,便捷初始化器需要在 init 前用 convenience 修飾。
class SHPerson {var age: Intvar name: String
init(age: Int, name: String) {self.age = ageself.name = name }
convenience init() {self.init(age: 18, name: "Coder_ 張三") }}(滑動顯示更多)
如代碼所示,便捷初始化器必須從相同的類裡調用另一個初始化器,並且最終必須調用一個指定初始化器。
結構體與類的本質區別為結構體是值類型,類是引用類型(其實也可以理解為指針類型)。那麼它們還有一個最直觀的區別就是存儲的位置不同:一般情況,值類型存儲的在棧上,引用類型存儲在堆上。
class SHPerson {var age = 18var height = 180}
struct SHPoint {var x = 0;var y = 0;}
func test() {let point = SHPoint()let person = SHPerson()}(滑動顯示更多)
SHPoint 在初始化完成賦值給 point 後,SHPoint 的內存數據直接就是放在棧空間。而 SHPerson 在初始化完成賦值給 person 後,person 只是一個引用地址,這個地址存的內存數據為 SHPerson 的內存地址,該內存地址放在堆空間。
值類型賦值給 var、let 或者給函數傳參,是直接將所有內容拷貝一份。類似於對文件進行 copy、paste操作,產生了全新的文件副本。屬於深拷貝(deep copy)。
struct SHPoint {var x = 4;var y = 8;}
var p1 = SHPoint()var p2 = p1;
p2.x = 6
print("p1 - \(p1)")print("p2 - \(p2)")列印結果:p1 - SHPoint(x: 4, y: 8)p2 - SHPoint(x: 6, y: 8)我們可以看到在修改 p2 的 x 後,對 p1 並沒有影響,這屬於深拷貝。我們來看數組的列印結果。
var a1 = [1, 2, 3]var a2 = a1a2.append(4)a1[0] = 2print(a1)print(a2)列印結果:[2, 2, 3][1, 2, 3, 4]在 Swift 標準庫中,為了提升性能,String、Array、Dictionary、Set 採取了Copy On Write 的技術, 比如僅當有「寫」操作時,才會真正執行拷貝操作。
對於標準庫值類型的賦值操作,Swift 能確保最佳性能,所有沒必要為了保證最佳性能來避免賦值。
建議:不需要修改的,儘量定義成 let。
引用賦值給var、let或者給函數傳參,是將內存地址拷貝一份。類似於製作一個文件的替身(快捷方式、連結),指向的是同一個文件。屬於淺拷貝(shallow copy)。
class SHPerson {var age: Int = 18var name: String = "Coder_張三"}
let p1 = SHPerson()let p2 = p1
print("p1-age: \(p1.age)")p2.age = 20print("p1-age: \(p1.age)")print("p2-age: \(p2.age)")(滑動顯示更多)
列印結果:p1-age: 18p1-age: 20p2-age: 20在Swift中,創建類的實例對象,要向堆空間申請內存,大概流程如下:
Class.__allocating_init()
libswiftCore.dylib: swift_allocObject
libswiftCore.dylib: swift_slowAlloc
libsystem_malloc.dylib: malloc
在Mac、iOS中的 malloc 函數分配的內存大小總是16 的倍數。
class CGPoint {var x = 11var y = 22var test = true}var p = CGPoint()print(class_getInstanceSize(CGPoint.self))print(malloc_size(unsafeBitCast(p, to: UnsafeRawPointer.self)))(滑動顯示更多)
通過列印得知,CGPoint 的大小為 40 個字節,系統分配 CGPoint 的內存大小為 48 個字節。
在 CGPoint 中,x 佔 8 個字節,y 佔 8 個字節,test 佔 1 個字節,所以目前我們看到的有 17 個字節。但是因為類存儲在堆空間中,它前面會有 8 個字節存放類型信息,8個字節存引用計數,再加上面的,加起來一共是 33 個字節,根據內存對齊原則(8 個字節),所以 CGPoint 的大小為 40 個字節。
因為在Mac、iOS中的 malloc 函數分配的內存大小總是16 的倍數,所以最終系統會分配 CGPoint 的內存大小為 48 字節。
結構體與類的使用方式很相似,那麼在平時開發中使用結構體比較好還是類比較好呢?這種時候分情況,如果定義的數據結構比較簡單的情況下,建議用結構體,比如 Model。如果定義的數據結構比較複雜的話,建議用類,比如需要用到多態的時候。
StructVsClassPerformance demo 測試
我們可以通過 github 上的 上 StructVsClassPerformance 這個 demo 來直觀的測試當前結構體和類的時間分配。
具體的代碼就不去貼出來了,我們來看一下調用方式以及列印結果:
列印結果:Running tests
class (1 field)9.566281178005738
struct (1 field)6.391943185008131
class (10 fields)10.430800677015213
struct (10 fields)6.610909776005428通過列印結果可以直觀的看到,結構體對比類的時間分配時要快將近一倍的速度。
iOS 開發的語言不管是 OC 還是 Swift,後端都是通過 LLVM 進行編譯的,如下圖所示:
OC 通過 clang 編譯器,編譯成 IR,然後再生成可執行文件 .o(這裡也就是我們的機器碼)。Swift 則是通過 Swift 編譯器編譯成 IR,然後在生成可執行文件。
首先 Swift Code 經過 -dump-parse 進行語義分析解析成 Parse(抽象語法樹)。
Parse 經過 -dump-ast 進行語義分析分析語法是否正確,是否安全。
Seam 之後會把 Swift Code 會降級變成 SILGen(Swift 中間代碼),對於 SILGen 又分為原生的(Raw SIL)和經過優化的(SIL Opt Canonical SIL)。
優化完成的 SIL 會由 LLVM 降級成為 IR,降級成 IR 之後由後端代碼編譯成機器碼。
以上就是 Swift 的編譯流程,下面為編譯流程的命令。
分析輸出 AST:
// 分析輸出ASTswiftc main.swift -dump-parse
// 分析並且檢查類型輸出ASTswiftc main.swift -dump-ast
// 生成中間體語言(SIL),未優化swiftc main.swift -emit-silgen
// 生成中間體語言(SIL),優化後的swiftc main.swift -emit-sil
// 生成LLVM中間體語言 (.ll文件)swiftc main.swift -emit-ir
// 生成LLVM中間體語言 (.bc文件)swiftc main.swift -emit-bc
// 生成彙編swiftc main.swift -emit-assembly
// 編譯生成可執行.out文件swiftc -o main.o main.swift(滑動顯示更多)
將以下代碼編譯成 sil 代碼:
import Foundation
class SHPerson {var age = 18var name = "Coder_張三"}
let p = SHPerson()終端 cd 到項目的 main.swift 目錄,輸入 swiftc main.swift -emit-sil 並按回車鍵,會在生成一個 main.sil 文件,並且會在終端輸出 SIL 代碼。SIL 代碼如下:
關於 SIL 的語法說明,其實也有相應的文檔。這裡貼上文檔說明的地址:SIL參考文檔
接下來我們通過彙編來查看類的初始化流程,我們打個斷點如下:
接下來打開彙編調試
通過彙編查看,SHPerson 在進行初始化的時候,在底層會調用 __allocating_init 的函數,那麼 __allocating_init 做了什麼事情呢,跟進去看一下。
讓斷點走到 __allocating_init 這一行代碼,按住 control 鍵,點擊這個向下的按鈕。
可以看到,進入到 __allocating_init 的內部實現後,發現它會調用一個 swift_allocObject 函數,那麼在繼續跟彙編的時候跟丟了。
接下來我們來看一下源碼。源碼可以去蘋果官網下-swift源碼下載地址。用 VSCode 打開下載好的 swift 源碼,全局搜索 swift_allocObject 這個函數。
在 HeapObject.cpp 文件中找到 swift_allocObject 函數的實現,並且在 swift_allocObject 函數的實現上方,有一個 _swift_allocObject_ 函數的實現。
// 第一個參數,元數據。// 第二個參數,分配內存的大小// 第三個參數,內存對齊,值一般為 7,因為遵守8位元組對齊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));
// 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;}(滑動顯示更多)
在函數的內部會調用一個 swift_slowAlloc 函數,我們來看下 swift_slowAlloc 函數的內部實現:
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);#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;}(滑動顯示更多)
swift_slowAlloc 函數的內部是去進行一些分配內存的操作,比如 malloc。所以就印證了第四點引用類型->對象申請堆空間的過程。
在調用 _swift_allocObject_ 函數的時候有一個參數,名為 metadata 的 HeapMetadata。以下是 HeapMetadata 跟進的代碼過程:
// HeapMetadata 為 TargetHeapMetadata 的別名,InProcess 為泛型。using HeapMetadata = TargetHeapMetadata<InProcess>;(滑動顯示更多)
template <typename Runtime>struct TargetHeapMetadata : TargetMetadata<Runtime> {using HeaderType = TargetHeapMetadataHeader<Runtime>;
TargetHeapMetadata() = default;constexpr TargetHeapMetadata(MetadataKind kind) : TargetMetadata<Runtime>(kind) {}#if SWIFT_OBJC_INTEROPconstexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa) : TargetMetadata<Runtime>(isa) {}#endif};(滑動顯示更多)
在這裡有對 OC 和 Swift 做兼容。調用的 TargetHeapMetadata 函數的時候,如果是 OC 的類,那麼參數為 isa 指針,否則就是一個 MetadataKind 類型。MetadataKind 是一個 uint32_t 的類型。
enum class MetadataKind : uint32_t {#define METADATAKIND(name, value) name = value,#define ABSTRACTMETADATAKIND(name, start, end) \ name##_Start = start, name##_End = end,#include "MetadataKind.def"
/// The largest possible non-isa-pointer metadata kind value.////// This is included in the enumeration to prevent against attempts to/// exhaustively match metadata kinds. Future Swift runtimes or compilers/// may introduce new metadata kinds, so for forward compatibility, the/// runtime must tolerate metadata with unknown kinds./// This specific value is not mapped to a valid metadata kind at this time,/// however. LastEnumerated = 0x7FF,};(滑動顯示更多)
那麼 MetadataKind 的種類如下:
name Value
Class 0x0Struct 0x200Enum 0x201Optional 0x202ForeignClass 0x203ForeignClass 0x203Opaque 0x300Tuple 0x301Function 0x302Existential 0x303Metatype 0x304ObjCClassWrapper 0x305ExistentialMetatype 0x306HeapLocalVariable 0x400HeapGenericLocalVariable 0x500ErrorObject 0x501LastEnumerated 0x7FF(滑動顯示更多)
接下來我們找到 TargetHeapMetadata 的繼承 TargetMetadata(在 C++ 中結構體是允許繼承的)。在 TargetMetadata 結構體中找到了 getTypeContextDescriptor 函數,代碼如下:
ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor> getTypeContextDescriptor() const {switch (getKind()) {case MetadataKind::Class: {const auto cls = static_cast<const TargetClassMetadata<Runtime> *>(this);if (!cls->isTypeMetadata())return nullptr;if (cls->isArtificialSubclass())return nullptr;return cls->getDescription(); }case MetadataKind::Struct:case MetadataKind::Enum:case MetadataKind::Optional:return static_cast<const TargetValueMetadata<Runtime> *>(this) ->Description;case MetadataKind::ForeignClass:return static_cast<const TargetForeignClassMetadata<Runtime> *>(this) ->Description;default:return nullptr; } }(滑動顯示更多)
可以看到,當 kind 是一個 Class 的時候,會拿到一個名為 TargetClassMetadata 的指針,我們看看 TargetClassMetadata 的實現:
終於看到熟悉的東西了,我們在看看它的繼承 TargetAnyClassMetadata 結構體,可以看到有 superclass,isa 等。
通過以上的分析,我們可以得出,Swift 類中的 metadata 數據結構大致如下:
struct Metadata {var kind: Intvar superClass: Any.Typevar cacheData: (Int, Int)var data: Intvar classFlags: Int32var instanceAddressPoint: UInt32var instanceSize: UInt32var instanceAlignmentMask: UInt16var reserved: UInt16var classSize: UInt32var classAddressPoint: UInt32var typeDescriptor: UnsafeMutableRawPointervar iVarDestroyer: UnsafeRawPointer}(滑動顯示更多)
接下來我們做一個測試,通過 lldb 查看 Swift 類的內存結構,那麼既然在 Swift 的底層,_swift_allocObject_ 函數返回的是 HeapObject 的指針類型,我們來看一下 HeapObject 的結構:
struct HeapObject {/// This is always a valid pointer to a metadata object. HeapMetadata const *__ptrauth_objc_isa_pointer 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) { }
// Initialize a HeapObject header for an immortal objectconstexpr HeapObject(HeapMetadata const *newMetadata, InlineRefCounts::Immortal_t immortal) : metadata(newMetadata) , refCounts(InlineRefCounts::Immortal) { }
#ifndef NDEBUGvoid dump() const SWIFT_USED;#endif
#endif // __swift__ };(滑動顯示更多)
知道了 HeapObject 的源碼結構之後,我們也假裡假氣的模仿源碼,自己定義一個 HeapObject,一下的 refcounted1 和 refcounted2 可以先忽略,不管,主要看 metadata。
struct HeapObject {var metadata: UnsafeRawPointervar refcounted1: UInt32var refcounted2: UInt32}接下來我們將 SHPerson 類轉成 HeapObject 結構體,通過 lldb 列印,查看其內存結構。
class SHPerson {var age = 18var name = "Coder_張三"}
let p = SHPerson()
// 將 SHPerson 轉成 HeapObject 指針let p_raw_ptr = Unmanaged.passUnretained(p as AnyObject).toOpaque()let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)// 將 p_ptr 指針轉成 HeapObject 的指針類型並列印出 HeapObject 的內存結構print(p_ptr.pointee)(滑動顯示更多)
列印結果:HeapObject(metadata: 0x00000001000081a0, refcounted1: 3, refcounted2: 0)
(lldb) x/8g 0x00000001000081a00x1000081a0: 0x0000000100008168 0x00007fff806208f80x1000081b0: 0x00007fff20208aa0 0x00008030000000000x1000081c0: 0x00000001085040f2 0x00000000000000020x1000081d0: 0x0000000700000028 0x00000010000000a8
(lldb) x/8g 0x00000001000081680x100008168: 0x00007fff80620920 0x00007fff806209200x100008178: 0x00007fff20208aa0 0x0000a031000000000x100008188: 0x0000000108504090 0x00000001000032b00x100008198: 0x00007fff8152f3e0 0x0000000100008168(lldb)(滑動顯示更多)
通過列印,得知,Swift 類的本質就是 HeapObject 的結構體指針,並且,我們將其內存布局以 x/8g 的形式列印出來。
接下來我需要列印出 HeapObject 中 metadata 的內存結構,來試一下:
struct Metadata{var kind: Intvar superClass: Any.Typevar cacheData: (Int, Int)var data: Intvar classFlags: Int32var instanceAddressPoint: UInt32var instanceSize: UInt32var instanceAlignmentMask: UInt16var reserved: UInt16var classSize: UInt32var classAddressPoint: UInt32var typeDescriptor: UnsafeMutableRawPointervar iVarDestroyer: UnsafeRawPointer}
struct HeapObject {var metadata: UnsafeRawPointervar refcounted1: UInt32var refcounted2: UInt32}
class SHPerson {var age = 18var name = "Coder_張三"}
let p = SHPerson()
let p_raw_ptr = Unmanaged.passUnretained(p as AnyObject).toOpaque()let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)// 我們將 HeapObject 中的 metadata 綁定成 Metadata 類型,並轉成 Metadata 的指針類型,那麼數據類型的大小可以用 MemoryLayout 測量出來。let metadata = p_ptr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointeeprint(metadata)(滑動顯示更多)
列印結果:Metadata(kind: 4295000432, superClass: _TtCs12_SwiftObject, cacheData: (140733732391584, 140943646785536), classFlags: 2, instanceAddressPoint: 0, instanceSize: 40, instanceAlignmentMask: 7, reserved: 0, classSize: 168, classAddressPoint: 16, typeDescriptor: 0x0000000100003c6c, iVarDestroyer: 0x0000000000000000)(lldb)(滑動顯示更多)
我們成功的列印出 kind、superClass、cacheData 等成員變量的值。
原文連結:https://juejin.cn/post/7046043638781968421
粉絲福利:最近給小夥伴們整理了一些最新的面試題資料,匯總整理出來的資源,可謂是程式設計師面試必備!所有資料都整理到網盤了,歡迎下載!