Swift 中的類與結構體
本文結合源碼探究類和結構體的本質。
類和結構體的異同
Swift中,類和結構體有許多相似之處,但也有不同。
我們都知道,內存分配可以分為堆區(qū)(Heap)和棧區(qū)(Stack)。由于棧區(qū)內存是連續(xù)的,內存的分配和銷毀是通過入棧和出棧操作進行的,速度要高于堆區(qū)。堆區(qū)存儲高級數(shù)據(jù)類型,在數(shù)據(jù)初始化時,查找沒有使用的內存,銷毀時再從內存中清除,所以堆區(qū)的數(shù)據(jù)存儲不一定是連續(xù)的。
類(class)和結構體(struct)在內存分配上是不同的,基本數(shù)據(jù)類型和結構體默認分配在棧區(qū),而像類這種高級數(shù)據(jù)類型存儲在堆區(qū),且堆區(qū)數(shù)據(jù)存儲不是線程安全的,在頻繁的數(shù)據(jù)讀寫操作時,要進行加鎖操作。
結構體除了屬性的存儲更安全、效率更高之外,其函數(shù)的派發(fā)也更高效。由于結構體不能被繼承,也就是結構體的類型被final修飾,其內部函數(shù)屬于靜態(tài)派發(fā),在編譯期就確定了函數(shù)的執(zhí)行地址,其函數(shù)的調用通過內聯(lián)(inline)的方式進行優(yōu)化,其內存連續(xù),減少了函數(shù)的尋址及內存地址的偏移計算,其運行相比于動態(tài)派發(fā)更加高效。
另外, 引用技術也會對類的使用效率產生消耗,所以在可選的情況下應該盡可能的使用結構體。
結構體都是值類型, 當它被指定到常量或者變量,或者被傳遞給函數(shù)時會被拷貝的類型。實際上,Swift 中所有的基本類型:整數(shù),浮點數(shù),布爾量,字符串,數(shù)組和字典,還有枚舉,都是值類型,并且都以結構體的形式在后臺實現(xiàn)。這意味著字符串,數(shù)組和字典在它們被賦值到一個新的常量或者變量,亦或者它們本身被傳遞到一個函數(shù)或方法中的時候,其實是傳遞了值的拷貝。這不同于OC的NSString,NSArray和NSDictionary,他們是類,賦值和傳遞都是引用。
retain時不可避免要遍歷堆,而Swift的堆是通過雙向鏈表實現(xiàn)的,理論上可以減少retain時的遍歷,把效率提高一倍,但是還是比不過棧, 所以蘋果把一些放在堆里的類型改成了值類型。
值類型存儲的是值,賦值時都是進行值拷貝,相互之間不會影響。而引用類型存儲的是對象的內存地址,賦值時拷貝指針,都是指向同一個對象(內存空間)。
類和結構體的異同:
相同點:都能定義屬性、方法、初始化器;都能添加extension擴展;都能遵循協(xié)議;
不同點:類是引用類型,存儲在堆區(qū);結構體是值類型,存儲在棧區(qū)。類有繼承特性;結構體沒有。類實例可以被多次引用,有引用計數(shù)。類有反初始化器(析構函數(shù))來釋放資源。類型轉換允許你在運行檢查和解釋一個類實例的類型。
結構體示例
struct Book {
var name: String
var high: Int
func turnToPage(page:Int) {
print("turn to page \(page)")
}
}
var s = Book(name: "易經", high: 8)
var s1 = s
s1.high = 10
print(s.high, s1.high) // 8 10
這段代碼中初始化結構體high為18,賦值給s1時拷貝整個結構體,相當于s1是一個新的結構體,修改s1的high為10后,s的age仍然是8,s和s1互不影響。
通過 lldb 調試, 也能夠看出 s 和 s1 是不同的結構體. 一個在 0x0000000100008080, 一個在 0x0000000100008098.
(lldb) frame variable -L s
0x0000000100008080: (SwiftTest.Book) s = {
0x0000000100008080: name = "易經"
0x0000000100008090: high = 8
}
(lldb) frame variable -L s1
0x0000000100008098: (SwiftTest.Book) s1 = {
0x0000000100008098: name = "易經"
0x00000001000080a8: high = 10
}
類示例
class Person {
var age: Int = 22
var name: String?
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
func eat(food:String) {
print("eat \(food)")
}
func jump() {
print("jump")
}
}
var c = Person(22, "jack")
var c1 = c
c1.age = 30
print(c.age, c1.age) // 30 30
如果是類,c1=c的時候拷貝指針,產生了一個新的引用,但都指向同一個對象,修改c1的age為30后,c的age也會變成30。
(lldb) frame variable -L c
scalar: (SwiftTest.Person) c = 0x0000000100679af0 {
0x0000000100679b00: age = 30
0x0000000100679b08: name = "jack"
}
(lldb) frame variable -L c1
scalar: (SwiftTest.Person) c1 = 0x0000000100679af0 {
0x0000000100679b00: age = 30
0x0000000100679b08: name = "jack"
}
(lldb) cat address 0x0000000100679af0
address:0x0000000100679af0, (String) $R1 = "0x100679af0 heap pointer, (0x30 bytes), zone: 0x7fff8076a000"
通過lldb調試,發(fā)現(xiàn)類的實例 c 和 c1 實際上是同一個對象, 再通過自定義命令 address 可以得出這個對象是在 heap 堆上.
而 c 和 c1 本身是2個不同的指針, 他們里面都存的是 0x0000000100679af0 這個地址.
(lldb) po withUnsafePointer(to: &c, {print($0)})
0x0000000100008298
0 elements
(lldb) po withUnsafePointer(to: &c1, {print($0)})
0x00000001000082a0
0 elements

編譯過程
clang編譯器
OC和C這類語言,會使用 clang 作為編譯器前端, 編譯成中間語言 IR, 再交給后端 LLVM 生成可執(zhí)行文件.

Clang編譯過程有以下幾個缺點:
源代碼與LLVM IR之間有巨大的抽象鴻溝 IR不適合源碼級別的分析 CFG(Control Flow Graph)缺少精準度 CFG偏離主道 在CFG和IR降級中會出現(xiàn)重復分析
Swift編譯器
為了解決這些缺點, Swift開發(fā)了專屬的Swift前端編譯器, 其中最關鍵的就是引入 SIL。
SIL
Swift Intermediate Language,Swift高級中間語言,Swift 編譯過程引入SIL有以下優(yōu)點:
完全保留程序的語義 既能進行代碼的生成,又能進行代碼分析 處在編譯管線的主通道 (hot path) 架起橋梁連接源碼與LLVM,減少源碼與LLVM之間的抽象鴻溝
SIL會對Swift進行高級別的語意分析和優(yōu)化。像LLVM IR一樣,也具有諸如Module,F(xiàn)unction和BasicBlock之類的結構。與LLVM IR不同,它具有更豐富的類型系統(tǒng),有關循環(huán)和錯誤處理的信息仍然保留,并且虛函數(shù)表和類型信息以結構化形式保留。它旨在保留Swift的含義,以實現(xiàn)強大的錯誤檢測,內存管理等高級優(yōu)化。
swift編譯步驟
Swift前端編譯器先把Swift代碼轉成SIL, 再轉成IR.

下面是每個步驟對應的命令和解釋
// 1 Parse: 語法分析組件, 從Swift源碼分析輸出抽象語法樹AST
swiftc main.swift -dump-parse
// 2 語義分析組件: 對AST進行類型檢查,并對其進行類型信息注釋
swiftc main.swift -dump-ast
// 3 SILGen組件: 生成中間體語言,未優(yōu)化的 raw SIL (生SIL)
// 一系列在 生 SIL上運行的,用于確定優(yōu)化和診斷合格,對不合格的代碼嵌入特定的語言診斷。
// 這些操作一定會執(zhí)行,即使在`-Onone`選項下也不例外
swiftc main.swift -emit-silgen
// 4 生成中間體語言(SIL),優(yōu)化后的
// 一般情況下,是否在正式SIL上運行SIL優(yōu)化是可選的,這個檢測可以提升結果可執(zhí)行文件的性能.
// 可以通過優(yōu)化級別來控制,在-Onone模式下不會執(zhí)行.
swiftc main.swift -emit-sil
// 5 IRGen會將正式SIL降級為 LLVM IR(.ll文件)
swiftc main.swift -emit-ir
// 6 LLVM后端優(yōu)化, 生成LLVM中間體語言 (.bc文件)
swiftc main.swift -emit-bc
// 7 生成匯編
swiftc main.swift -emit-assembly
// 8 生成二進制機器碼, 編譯成可執(zhí)行.out文件
swiftc -o main.o main.swift
一般我們在分析 sil 文件的時候,通過下面這條命令把 swift 文件直接轉成 sil 文件:
swiftc -emit-sil main.swift > main.sil
類的生命周期
下面分析一下類的創(chuàng)建過程, 如下代碼
class Human {
var name: String
init(_ name: String) {
self.name = name
}
func eat(food:String) {
print("eat \(food)")
}
}
var h = Human("hali")
轉成sil, swiftc -emit-sil main.swift > human.sil
分析sil文件, 可以看到如下代碼, 是 __allocating_init 初始化方法
// Human.__allocating_init(_:)
sil hidden [exact_self_class] @$s4main5HumanCyACSScfC : $@convention(method) (@owned String, @thick Human.Type) -> @owned Human {
// %0 "name" // user: %4
// %1 "$metatype"
bb0(%0 : $String, %1 : $@thick Human.Type):
%2 = alloc_ref $Human // user: %4
// function_ref Human.init(_:)
%3 = function_ref @$s4main5HumanCyACSScfc : $@convention(method) (@owned String, @owned Human) -> @owned Human // user: %4
%4 = apply %3(%0, %2) : $@convention(method) (@owned String, @owned Human) -> @owned Human // user: %5
return %4 : $Human // id: %5
} // end sil function '$s4main5HumanCyACSScfC'
接下來在Xcode打上符號斷點 __allocating_init,

調用的是 swift_allocObject 這個方法, 而如果 Human繼承自NSObject, 會調用objc的 objc_allocWithZone 方法, 走OC的初始化流程.

分析Swift源碼[1], 搜索 swift_allocObject, 定位到 HeapObject.cpp 文件,

內部調用 swift_slowAlloc,
至此, 通過分析 sil, 匯編, 源代碼,我們可以得出swift對象的初始化過程如下:
__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> Malloc
類的內存結構
通過上面的源碼, 發(fā)現(xiàn)初始化返回的是一個 HeapObject, 它的定義如下:
// The members of the HeapObject header that are not shared by a
// standard Objective-C instance
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \
InlineRefCounts refCounts //
/// The Swift heap-object header.
/// This must match RefCountedStructTy in IRGen.
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata; // 8字節(jié)
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; //64位的位域信息, 8字節(jié); metadata 和 refCounts 一起構成默認16字節(jié)實例對象的內存大小
#ifndef __swift__
// ......
#endif // __swift__
};
HeapObject的metadata是一個HeapMetadata類型, 本質上是 TargetHeapMetadata, 我們可以在源碼中找到這個定義
using HeapMetadata = TargetHeapMetadata<InProcess>;
再點擊跳轉到 TargetHeapMetadata,
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> { //繼承自TargetMetadata
using HeaderType = TargetHeapMetadataHeader<Runtime>;
// 下面是初始化
TargetHeapMetadata() = default;
constexpr TargetHeapMetadata(MetadataKind kind) // 純swift
: TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP //和objc交互
constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa) //isa
: TargetMetadata<Runtime>(isa) {}
#endif
};
這里可以看到, 如果是純swift,就會給入 kind, 如果是OC就給入 isa.
再繼續(xù)點擊跳轉分析 TargetHeapMetadata 的父類 TargetMetadata,
/// The common structure of all type metadata.
template <typename Runtime>
struct TargetMetadata { // 所有元類類型的最終基類
using StoredPointer = typename Runtime::StoredPointer;
/// The basic header type.
typedef TargetTypeMetadataHeader<Runtime> HeaderType;
constexpr TargetMetadata()
: Kind(static_cast<StoredPointer>(MetadataKind::Class)) {}
constexpr TargetMetadata(MetadataKind Kind)
: Kind(static_cast<StoredPointer>(Kind)) {}
#if SWIFT_OBJC_INTEROP
protected:
constexpr TargetMetadata(TargetAnyClassMetadata<Runtime> *isa)
: Kind(reinterpret_cast<StoredPointer>(isa)) {}
#endif
private:
/// The kind. Only valid for non-class metadata; getKind() must be used to get
/// the kind value.
StoredPointer Kind;//Kind成員變量
public:
// ......
/// Get the nominal type descriptor if this metadata describes a nominal type,
/// or return null if it does not.
ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor>
getTypeContextDescriptor() const {
switch (getKind()) { // 根據(jù) kind 區(qū)分不同的類
case MetadataKind::Class: {
const auto cls = static_cast<const TargetClassMetadata<Runtime> *>(this);//把this強轉成TargetClassMetadata類型
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;
}
}
// ......
};
TargetMetadata就是最終的基類, 其中有個 Kind 的成員變量, 它是一個固定值 0x7FF.
TargetMetadata 中根據(jù) kind 種類強轉成其它類型, 所以 這個 TargetMetadata 就是所有元類型的基類.
在強轉成類的時候, 強轉類型是 TargetClassMetadata, 點擊跳轉然后分析它的繼承連如下
TargetClassMetadata : TargetAnyClassMetadata : TargetHeapMetadata : TargetMetadata
通過分析源碼, 可以得出關系圖

所以綜合繼承鏈上的成員變量, 可以得出類的內存結構:
struct Metadata {
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
PS: 補充kind種類, 這個是固定值

通過SIL分析異變方法
Class 和 struct 都可以定義方法,但是默認情況下,值類型不能被自身修改,也就意味著 struct方法不能修改自身的屬性。所以如下的代碼就會報錯 Left side of mutating operator isn't mutable: 'self' is immutable
struct Point {
var x = 0.0, y = 0.0
func moveBy(x deltaX: Double, y deltaY: Double) {
self.x += deltaX //Left side of mutating operator isn't mutable: 'self' is immutable
self.y += deltaY //Left side of mutating operator isn't mutable: 'self' is immutable
}
}
此時在方法前面添加 mutating 關鍵字即可。
struct Point {
var x = 0.0, y = 0.0
func test() {
print("test")
}
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
self.x += deltaX
self.y += deltaY
}
}
什么是 mutating ?我們把代碼轉成 sil 來分析 swiftc -emit-sil main.swift > main.sil
// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
// %0 "self" // user: %1
bb0(%0 : $Point):
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
與OC不同,Swift只有1個默認參數(shù)self,且作為最后一個參數(shù)傳入, 默認放在 x0 寄存器。debug_value 直接取值,不能被修改。
// Point.moveBy(x:y:)
sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {
// %0 "deltaX" // users: %10, %3
// %1 "deltaY" // users: %20, %4
// %2 "self" // users: %16, %6, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*Point):
debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
比較上面2斷sil代碼,發(fā)現(xiàn) mutating 的方法 moveBy 的默認參數(shù)self 多了一個 @inout修飾,它表示當前參數(shù)類型是間接的,傳遞的是已經初始化過的地址通過下面的 debug_value_addr 也可以看出, 取的是 *Point這個內容的地址,通過指針對self進行修改。
函數(shù)定義形參的時候,函數(shù)內參數(shù)的改變并不會影響外部, 但是在前面加上 inout 關鍵字就變成一個輸入輸出形式參數(shù),在函數(shù)外部這些參數(shù)的改變將被保留.
方法調度
Swift函數(shù)的3種派發(fā)機制
Swift有3種函數(shù)派發(fā)機制:
靜態(tài)派發(fā) (static dispatch)
是在編譯期就能確定調用方法的派發(fā)方式, Swift中的靜態(tài)派發(fā)直接使用函數(shù)地址.
動態(tài)派發(fā) (dynamic dispatch) / 虛函數(shù)表派發(fā)
動態(tài)派發(fā)是指編譯期無法確定應該調用哪個方法,需要在運行時才能確定方法的調用, 通過虛函數(shù)表查找函數(shù)地址再調用.
消息派發(fā) (message dispatch)
使用objc的消息派發(fā)機制, objc采用了運行時
objc_msgSend進行消息派發(fā),所以Objc的一些動態(tài)特性在Swift里面也可以被限制的使用。
靜態(tài)派發(fā)相比于動態(tài)派發(fā)更快,而且靜態(tài)派發(fā)還會進行內聯(lián)等一些優(yōu)化,減少函數(shù)的尋址過程, 減少內存地址的偏移計算等一系列操作,使函數(shù)的執(zhí)行速度更快,性能更高。
一般情況下, 不同類型的函數(shù)調度方式如下
| 類型 | 調度方式 | extension |
|---|---|---|
| 值類型 | 靜態(tài)派發(fā) | 靜態(tài)派發(fā) |
| 類 | 函數(shù)表派發(fā) | 靜態(tài)派發(fā) |
| NSObject 子類 | 函數(shù)表派發(fā) | 靜態(tài)派發(fā) |
類函數(shù)的動態(tài)派發(fā)
通過一個案例探究 動態(tài)派發(fā)/虛函數(shù)表派發(fā) 表這種方式中, 程序是如何找到函數(shù)地址的
class LGTeacher {
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
var t = LGTeacher()
t.teach()
在程序中, 斷點在函數(shù)處, 進入?yún)R編代碼讀取寄存器匯中的值,

這個 0x10004bab4 就是 teach() 函數(shù)的地址, 下面我們具體探究下中個地址是怎么來的.
源碼的解讀
一般來講, Swift會把所有的方法都被存在類的虛表中, 我們可以在 sil 文件中發(fā)現(xiàn)這個 vtable.

根據(jù)之前的分析, 類的結構 TargetClassMetadata 有個屬性 Description, 這個是Swift類的描述TargetClassDescriptor.
// Description is by far the most likely field for a client to try
// to access directly, so we force access to go through accessors.
private:
/// An out-of-line Swift-specific description of the type, or null
/// if this is an artificial subclass. We currently provide no
/// supported mechanism for making a non-artificial subclass
/// dynamically.
ConstTargetMetadataPointer<Runtime, TargetClassDescriptor> Description;
TargetClassDescriptor 它的內存結構如下
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
//V-Table
}
在這個描述的開始到vtable之間的屬性有 13 ?? 4 = 52 字節(jié),后面就是存儲方法描述TargetMethodDescriptor的 vtable 了。
struct TargetMethodDescriptor {
/// Flags describing the method.
MethodDescriptorFlags Flags; // 4字節(jié), 標識方法的種類, 初始化/getter/setter等等
/// The method implementation.
TargetRelativeDirectPointer<Runtime, void> Impl; // 相對地址, Offset
// TODO: add method types or anything else needed for reflection.
};
TargetMethodDescriptor 是對方法的描述, Flags表示方法的種類,占據(jù)4個字節(jié), Impl里面并不是真正的方法imp, 而是一個相對偏移量,所以需要找到這個 TargetMethodDescriptor + 4字節(jié) + 相對偏移量 才能得到方法的真正地址。
可執(zhí)行文件的解讀
在可執(zhí)行文件中, Class、Struct、Enum 的 Discripter 地址信息一般存在 _TEXT,_swift5_types 段.

iOS上一般小端模式, 所以我們讀到地址信息+偏移量 0xFFFFFBF4 + 0xBC68 = 0x10000B85C 得到 LGTeacher Description<TargetClassDescriptor> 在 MachO 中的地址. 虛擬內存的基地址是 0x100000000, 所以 B85C 就是 Description 的偏移量.
找到 B85C,

根據(jù) TargetClassDescriptor 的內存結構,從 B85C 往后讀 52個字節(jié)就是 vtable,對應的偏移量 B890.
vtable是個數(shù)組,所以第一個元素 10 00 00 00 20 C2 FF FF 是 TargetMethodDescriptor, 再根據(jù) TargetMethodDescriptor 的內存結構, 前面4字節(jié)是Flags, 后面4字節(jié)就是 Impl 的偏移量 Offset FFFFC220.
回到程序中,

通過 image list 輸出可執(zhí)行文件加載的地址,其中第一個就是程序運行首地址,0x100044000 加上 v-table偏移量,就得到v-table在程序運行中的地址,也就是第一個函數(shù) teach() 的 TargetMethodDescriptor的地址 0x100044000 + 0xB890 = 0x10004F890
然后加上 Flags 的4字節(jié),0x10004F890 + 0x4 = 0x10004F894 得到 Impl,
加上Offset再減去虛擬內存基地址 0x10004F894 + 0xFFFFC220 - 0x100000000 = 0x10004BAB4
才得到函數(shù)地址 0x10004BAB4 .
Struct函數(shù)靜態(tài)派發(fā)
struct LGTeacher {
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
var t = LGTeacher()
t.teach()
上述案例中改為 Struct, 那么就是直接調用的函數(shù)地址, 屬于靜態(tài)派發(fā).

extension
不論是 Class 或者 Struct, extension里的函數(shù)都是靜態(tài)派發(fā), 無法在運行時做任何替換和改變, 因為其里面的方法都是在編譯期確定好的, 程序中以硬編碼的方式存在, 不會放在vtable中.
extension LGTeacher{
func teach3(){
print("teach3")
}
}
var t = LGTeacher()
t.teach3()
都是直接調用函數(shù)地址
所以, 無法通過 extension 支持多態(tài).
那么為什么 Swift 會把 extension 設計成靜態(tài)的呢?
OC中子類繼承后不重寫方法的話是去父類中找方法實現(xiàn), 但是 Swift類在繼承的時候, 是把父類的方法形成一張vtable存在自己身上,這樣做也是為了節(jié)省方法的查找時間, 如果想讓 extension 加到 vtable 中, 并不是直接在子類vtable的最后直接追加就可以的, 需要在子類中記錄下父類方法的index,把父類的extension方法插入到子類vtable中父類方法index后相鄰的位置,再把子類自己的方法往后移動,這樣的一番操作消耗是很大的.
關鍵字最派發(fā)方式的影響
不同的函數(shù)修飾關鍵字對派發(fā)方式也有這不同的影響
final
final:添加了 final 關鍵字的函數(shù)無法被重寫/繼承,使用靜態(tài)派發(fā),不會在 vtable 中出現(xiàn),且對 objc 運行時不可見。
dynamic
dynamic: 函數(shù)均可添加 dynamic 關鍵字,為非objc類和值類型的函數(shù)賦予動態(tài)性,但派發(fā)方式還是函數(shù)表派發(fā)。
class LGTeacher {
dynamic func teach(){
print("teach")
}
}
extension LGTeacher {
@_dynamicReplacement(for: teach())
func teach3() {
print("teach3")
}
}
var t = LGTeacher()
t.teach3() // teach3
t.teach() // teach3
如上代碼中, teach() 函數(shù)是函數(shù)表派發(fā), 存在 vtable, 并且 dynamic 賦予動態(tài)性, 與 @_dynamicReplacement(for: teach()) 關鍵字配合使用, 把 teach()函數(shù)的實現(xiàn)改為 teach3()的實現(xiàn), 相當于OC中把 teach()的SEL對應為teach3()的imp, 實現(xiàn)方法的替換.
這個具體的實現(xiàn)是 llvm 編譯器處理的, 在中間語言 IR 中, teach() 函數(shù)中有2個分支, 一個 original, 一個 forward, 如果我們有替換的函數(shù), 就走 forward 分支.
# 轉成 IR 中間語言 .ll 文件
swiftc -emit-ir main.swift > dynamic.ll

@objc
@objc:該關鍵字可以將Swift函數(shù)暴露給Objc運行時,依舊是函數(shù)表派發(fā)。
@objc dynamic
@objc dynamic:消息派發(fā)的方式, 和 OC 一樣. 實際開發(fā)中, Swift 和 OC 交互大多會使用這種方式.
對于純Swift類, @objc dynamic 可以讓方法和OC一樣使用 Runtime API.
如果需要和OC進行交互, 需要把類繼承自 NSObjec.
參考資料
《Swift高級進階班》[2]
GitHub: apple[3] - swift源碼[4]
《跟戴銘學iOS編程: 理順核心知識點》
《程序員的自我修養(yǎng)》
Swift編程語言 - 類和結構體[5]
Swift Intermediate Language 初探[6]
Swift性能高效的原因深入分析[7]
Swift編譯器中間碼SIL[8]
Swift的高級中間語言:SIL[9]
參考資料
Swift源碼: https://github.com/apple/swift
[2]《Swift高級進階班》: https://ke.qq.com/course/3202559
[3]apple: https://github.com/apple
[4]swift源碼: https://github.com/apple/swift
[5]類和結構體: https://www.cnswift.org/classes-and-structures#spl
[6]Swift Intermediate Language 初探: https://zhuanlan.zhihu.com/p/101898915
[7]Swift性能高效的原因深入分析: http://www.codebaoku.com/it-swift/it-swift-198322.html
[8]Swift編譯器中間碼SIL: https://woshiccm.github.io/posts/Swift編譯器中間碼SIL/#sil簡介
[9]Swift的高級中間語言:SIL: https://www.jianshu.com/p/c2880460c6cd
轉自:掘金-Jerod
鏈接:https://juejin.cn/post/7051492093889347615
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網盤了,歡迎下載!

面試題】即可獲取