日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

Swift 中的类与结构体

發(fā)布時(shí)間:2023/12/8 72 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Swift 中的类与结构体 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

👇👇關(guān)注后回復(fù)?“進(jìn)群”?,拉你進(jìn)程序員交流群👇👇

本文結(jié)合源碼探究類和結(jié)構(gòu)體的本質(zhì)。

類和結(jié)構(gòu)體的異同

Swift中,類和結(jié)構(gòu)體有許多相似之處,但也有不同。

我們都知道,內(nèi)存分配可以分為堆區(qū)(Heap)和棧區(qū)(Stack)。由于棧區(qū)內(nèi)存是連續(xù)的,內(nèi)存的分配和銷(xiāo)毀是通過(guò)入棧和出棧操作進(jìn)行的,速度要高于堆區(qū)。堆區(qū)存儲(chǔ)高級(jí)數(shù)據(jù)類型,在數(shù)據(jù)初始化時(shí),查找沒(méi)有使用的內(nèi)存,銷(xiāo)毀時(shí)再?gòu)膬?nèi)存中清除,所以堆區(qū)的數(shù)據(jù)存儲(chǔ)不一定是連續(xù)的。

類(class)和結(jié)構(gòu)體(struct)在內(nèi)存分配上是不同的,基本數(shù)據(jù)類型和結(jié)構(gòu)體默認(rèn)分配在棧區(qū),而像類這種高級(jí)數(shù)據(jù)類型存儲(chǔ)在堆區(qū),且堆區(qū)數(shù)據(jù)存儲(chǔ)不是線程安全的,在頻繁的數(shù)據(jù)讀寫(xiě)操作時(shí),要進(jìn)行加鎖操作。

結(jié)構(gòu)體除了屬性的存儲(chǔ)更安全、效率更高之外,其函數(shù)的派發(fā)也更高效。由于結(jié)構(gòu)體不能被繼承,也就是結(jié)構(gòu)體的類型被final修飾,其內(nèi)部函數(shù)屬于靜態(tài)派發(fā),在編譯期就確定了函數(shù)的執(zhí)行地址,其函數(shù)的調(diào)用通過(guò)內(nèi)聯(lián)(inline)的方式進(jìn)行優(yōu)化,其內(nèi)存連續(xù),減少了函數(shù)的尋址及內(nèi)存地址的偏移計(jì)算,其運(yùn)行相比于動(dòng)態(tài)派發(fā)更加高效。

另外, 引用技術(shù)也會(huì)對(duì)類的使用效率產(chǎn)生消耗,所以在可選的情況下應(yīng)該盡可能的使用結(jié)構(gòu)體。

結(jié)構(gòu)體都是值類型, 當(dāng)它被指定到常量或者變量,或者被傳遞給函數(shù)時(shí)會(huì)被拷貝的類型。實(shí)際上,Swift 中所有的基本類型:整數(shù),浮點(diǎn)數(shù),布爾量,字符串,數(shù)組和字典,還有枚舉,都是值類型,并且都以結(jié)構(gòu)體的形式在后臺(tái)實(shí)現(xiàn)。這意味著字符串,數(shù)組和字典在它們被賦值到一個(gè)新的常量或者變量,亦或者它們本身被傳遞到一個(gè)函數(shù)或方法中的時(shí)候,其實(shí)是傳遞了值的拷貝。這不同于OC的NSString,NSArray和NSDictionary,他們是類,賦值和傳遞都是引用。

retain時(shí)不可避免要遍歷堆,而Swift的堆是通過(guò)雙向鏈表實(shí)現(xiàn)的,理論上可以減少retain時(shí)的遍歷,把效率提高一倍,但是還是比不過(guò)棧, 所以蘋(píng)果把一些放在堆里的類型改成了值類型。

值類型存儲(chǔ)的是值,賦值時(shí)都是進(jìn)行值拷貝,相互之間不會(huì)影響。而引用類型存儲(chǔ)的是對(duì)象的內(nèi)存地址,賦值時(shí)拷貝指針,都是指向同一個(gè)對(duì)象(內(nèi)存空間)。

類和結(jié)構(gòu)體的異同:

相同點(diǎn):都能定義屬性、方法、初始化器;都能添加extension擴(kuò)展;都能遵循協(xié)議;

不同點(diǎn):類是引用類型,存儲(chǔ)在堆區(qū);結(jié)構(gòu)體是值類型,存儲(chǔ)在棧區(qū)。類有繼承特性;結(jié)構(gòu)體沒(méi)有。類實(shí)例可以被多次引用,有引用計(jì)數(shù)。類有反初始化器(析構(gòu)函數(shù))來(lái)釋放資源。類型轉(zhuǎn)換允許你在運(yùn)行檢查和解釋一個(gè)類實(shí)例的類型。

結(jié)構(gòu)體示例

struct?Book?{var?name:?Stringvar?high:?Intfunc?turnToPage(page:Int)?{print("turn?to?page?\(page)")} }var?s?=?Book(name:?"易經(jīng)",?high:?8) var?s1?=?s s1.high?=?10 print(s.high,?s1.high)?//?8?10

這段代碼中初始化結(jié)構(gòu)體high為18,賦值給s1時(shí)拷貝整個(gè)結(jié)構(gòu)體,相當(dāng)于s1是一個(gè)新的結(jié)構(gòu)體,修改s1的high為10后,s的age仍然是8,s和s1互不影響。

通過(guò) lldb 調(diào)試, 也能夠看出 s 和 s1 是不同的結(jié)構(gòu)體. 一個(gè)在 0x0000000100008080, 一個(gè)在 0x0000000100008098.

(lldb)?frame?variable?-L?s 0x0000000100008080:?(SwiftTest.Book)?s?=?{ 0x0000000100008080:???name?=?"易經(jīng)" 0x0000000100008090:???high?=?8 } (lldb)?frame?variable?-L?s1 0x0000000100008098:?(SwiftTest.Book)?s1?=?{ 0x0000000100008098:???name?=?"易經(jīng)" 0x00000001000080a8:???high?=?10 }

類示例

class?Person?{var?age:?Int?=?22var?name:?String?init(_?age:?Int,?_?name:?String)?{self.age?=?ageself.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的時(shí)候拷貝指針,產(chǎn)生了一個(gè)新的引用,但都指向同一個(gè)對(duì)象,修改c1的age為30后,c的age也會(huì)變成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"

通過(guò)lldb調(diào)試,發(fā)現(xiàn)類的實(shí)例 c 和 c1 實(shí)際上是同一個(gè)對(duì)象, 再通過(guò)自定義命令 address 可以得出這個(gè)對(duì)象是在 heap 堆上.

而 c 和 c1 本身是2個(gè)不同的指針, 他們里面都存的是 0x0000000100679af0 這個(gè)地址.

(lldb)?po?withUnsafePointer(to:?&c,?{print($0)}) 0x0000000100008298 0?elements (lldb)?po?withUnsafePointer(to:?&c1,?{print($0)}) 0x00000001000082a0 0?elements

編譯過(guò)程

clang編譯器

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

Clang編譯過(guò)程有以下幾個(gè)缺點(diǎn):

  • 源代碼與LLVM IR之間有巨大的抽象鴻溝

  • IR不適合源碼級(jí)別的分析

  • CFG(Control Flow Graph)缺少精準(zhǔn)度

  • CFG偏離主道

  • 在CFG和IR降級(jí)中會(huì)出現(xiàn)重復(fù)分析

Swift編譯器

為了解決這些缺點(diǎn), Swift開(kāi)發(fā)了專屬的Swift前端編譯器, 其中最關(guān)鍵的就是引入 SIL。

SIL

Swift Intermediate Language,Swift高級(jí)中間語(yǔ)言,Swift 編譯過(guò)程引入SIL有以下優(yōu)點(diǎn):

  • 完全保留程序的語(yǔ)義

  • 既能進(jìn)行代碼的生成,又能進(jìn)行代碼分析

  • 處在編譯管線的主通道 (hot path)

  • 架起橋梁連接源碼與LLVM,減少源碼與LLVM之間的抽象鴻溝

SIL會(huì)對(duì)Swift進(jìn)行高級(jí)別的語(yǔ)意分析和優(yōu)化。像LLVM IR一樣,也具有諸如Module,Function和BasicBlock之類的結(jié)構(gòu)。與LLVM IR不同,它具有更豐富的類型系統(tǒng),有關(guān)循環(huán)和錯(cuò)誤處理的信息仍然保留,并且虛函數(shù)表和類型信息以結(jié)構(gòu)化形式保留。它旨在保留Swift的含義,以實(shí)現(xiàn)強(qiáng)大的錯(cuò)誤檢測(cè),內(nèi)存管理等高級(jí)優(yōu)化。

swift編譯步驟

Swift前端編譯器先把Swift代碼轉(zhuǎn)成SIL, 再轉(zhuǎn)成IR.

下面是每個(gè)步驟對(duì)應(yīng)的命令和解釋

//?1?Parse:?語(yǔ)法分析組件,?從Swift源碼分析輸出抽象語(yǔ)法樹(shù)AST swiftc?main.swift?-dump-parse???//?2?語(yǔ)義分析組件:?對(duì)AST進(jìn)行類型檢查,并對(duì)其進(jìn)行類型信息注釋 swiftc?main.swift?-dump-ast???//?3?SILGen組件:?生成中間體語(yǔ)言,未優(yōu)化的?raw?SIL?(生SIL) //?一系列在?生 SIL上運(yùn)行的,用于確定優(yōu)化和診斷合格,對(duì)不合格的代碼嵌入特定的語(yǔ)言診斷。 //?這些操作一定會(huì)執(zhí)行,即使在`-Onone`選項(xiàng)下也不例外 swiftc?main.swift?-emit-silgen???//?4?生成中間體語(yǔ)言(SIL),優(yōu)化后的 //?一般情況下,是否在正式SIL上運(yùn)行SIL優(yōu)化是可選的,這個(gè)檢測(cè)可以提升結(jié)果可執(zhí)行文件的性能. //?可以通過(guò)優(yōu)化級(jí)別來(lái)控制,在-Onone模式下不會(huì)執(zhí)行. swiftc?main.swift?-emit-sil???//?5?IRGen會(huì)將正式SIL降級(jí)為?LLVM?IR(.ll文件) swiftc?main.swift?-emit-ir????//?6?LLVM后端優(yōu)化,?生成LLVM中間體語(yǔ)言?(.bc文件) swiftc?main.swift?-emit-bc????//?7?生成匯編 swiftc?main.swift?-emit-assembly?//?8?生成二進(jìn)制機(jī)器碼,?編譯成可執(zhí)行.out文件 swiftc?-o?main.o?main.swift

一般我們?cè)诜治?sil 文件的時(shí)候,通過(guò)下面這條命令把 swift 文件直接轉(zhuǎn)成 sil 文件:

swiftc?-emit-sil?main.swift?>?main.sil

類的生命周期

下面分析一下類的創(chuàng)建過(guò)程, 如下代碼

class?Human?{var?name:?Stringinit(_?name:?String)?{self.name?=?name}func?eat(food:String)?{print("eat?\(food)")} }var?h?=?Human("hali")

轉(zhuǎn)成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:?%5return?%4?:?$Human??????????????????????????????//?id:?%5 }?//?end?sil?function?'$s4main5HumanCyACSScfC'

接下來(lái)在Xcode打上符號(hào)斷點(diǎn) __allocating_init,

調(diào)用的是 swift_allocObject 這個(gè)方法, 而如果 Human繼承自NSObject, 會(huì)調(diào)用objc的 objc_allocWithZone 方法, 走OC的初始化流程.

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

內(nèi)部調(diào)用 swift_slowAlloc,

至此, 通過(guò)分析 sil, 匯編, 源代碼,我們可以得出swift對(duì)象的初始化過(guò)程如下:

__allocating_init?->?swift_allocObject?->?_swift_allocObject_?->?swift_slowAlloc?->?Malloc

類的內(nèi)存結(jié)構(gòu)

通過(guò)上面的源碼, 發(fā)現(xiàn)初始化返回的是一個(gè) 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?一起構(gòu)成默認(rèn)16字節(jié)實(shí)例對(duì)象的內(nèi)存大小#ifndef?__swift__//?......#endif?//?__swift__ };

HeapObject的metadata是一個(gè)HeapMetadata類型, 本質(zhì)上是 TargetHeapMetadata, 我們可以在源碼中找到這個(gè)定義

using?HeapMetadata?=?TargetHeapMetadata<InProcess>;

再點(diǎn)擊跳轉(zhuǎn)到 TargetHeapMetadata,

template?<typename?Runtime> struct?TargetHeapMetadata?:?TargetMetadata<Runtime>?{?//繼承自TargetMetadatausing?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,就會(huì)給入 kind, 如果是OC就給入 isa.

再繼續(xù)點(diǎn)擊跳轉(zhuǎn)分析 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))?{} #endifprivate:///?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強(qiáng)轉(zhuǎn)成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就是最終的基類, 其中有個(gè) Kind 的成員變量, 它是一個(gè)固定值 0x7FF.

TargetMetadata 中根據(jù) kind 種類強(qiáng)轉(zhuǎn)成其它類型, 所以 這個(gè) TargetMetadata 就是所有元類型的基類.

在強(qiáng)轉(zhuǎn)成類的時(shí)候, 強(qiáng)轉(zhuǎn)類型是 TargetClassMetadata, 點(diǎn)擊跳轉(zhuǎn)然后分析它的繼承連如下

TargetClassMetadata?:?TargetAnyClassMetadata?:?TargetHeapMetadata?:?TargetMetadata

通過(guò)分析源碼, 可以得出關(guān)系圖

所以綜合繼承鏈上的成員變量, 可以得出類的內(nèi)存結(jié)構(gòu):

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 }

PS: 補(bǔ)充kind種類, 這個(gè)是固定值

通過(guò)SIL分析異變方法

Class 和 struct 都可以定義方法,但是默認(rèn)情況下,值類型不能被自身修改,也就意味著 struct方法不能修改自身的屬性。所以如下的代碼就會(huì)報(bào)錯(cuò) Left side of mutating operator isn't mutable: 'self' is immutable

struct?Point?{var?x?=?0.0,?y?=?0.0func?moveBy(x?deltaX:?Double,?y?deltaY:?Double)?{self.x?+=?deltaX?//Left?side?of?mutating?operator?isn't?mutable:?'self'?is?immutableself.y?+=?deltaY?//Left?side?of?mutating?operator?isn't?mutable:?'self'?is?immutable} }

此時(shí)在方法前面添加 mutating 關(guān)鍵字即可。

struct?Point?{var?x?=?0.0,?y?=?0.0func?test()?{print("test")}mutating?func?moveBy(x?deltaX:?Double,?y?deltaY:?Double)?{self.x?+=?deltaXself.y?+=?deltaY} }

什么是 mutating ?我們把代碼轉(zhuǎn)成 sil 來(lái)分析 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個(gè)默認(rèn)參數(shù)self,且作為最后一個(gè)參數(shù)傳入, 默認(rèn)放在 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:?%3debug_value?%1?:?$Double,?let,?name?"deltaY",?argno?2?//?id:?%4debug_value_addr?%2?:?$*Point,?var,?name?"self",?argno?3?//?id:?%5

比較上面2斷sil代碼,發(fā)現(xiàn) mutating 的方法 moveBy 的默認(rèn)參數(shù)self 多了一個(gè) @inout修飾,它表示當(dāng)前參數(shù)類型是間接的,傳遞的是已經(jīng)初始化過(guò)的地址通過(guò)下面的 debug_value_addr 也可以看出, 取的是 *Point這個(gè)內(nèi)容的地址,通過(guò)指針對(duì)self進(jìn)行修改。

函數(shù)定義形參的時(shí)候,函數(shù)內(nèi)參數(shù)的改變并不會(huì)影響外部, 但是在前面加上 inout 關(guān)鍵字就變成一個(gè)輸入輸出形式參數(shù),在函數(shù)外部這些參數(shù)的改變將被保留.

方法調(diào)度

Swift函數(shù)的3種派發(fā)機(jī)制

Swift有3種函數(shù)派發(fā)機(jī)制:

  • 靜態(tài)派發(fā) (static dispatch)

    是在編譯期就能確定調(diào)用方法的派發(fā)方式, Swift中的靜態(tài)派發(fā)直接使用函數(shù)地址.

  • 動(dòng)態(tài)派發(fā) (dynamic dispatch) / 虛函數(shù)表派發(fā)

    動(dòng)態(tài)派發(fā)是指編譯期無(wú)法確定應(yīng)該調(diào)用哪個(gè)方法,需要在運(yùn)行時(shí)才能確定方法的調(diào)用, 通過(guò)虛函數(shù)表查找函數(shù)地址再調(diào)用.

  • 消息派發(fā) (message dispatch)

    使用objc的消息派發(fā)機(jī)制, objc采用了運(yùn)行時(shí)objc_msgSend進(jìn)行消息派發(fā),所以O(shè)bjc的一些動(dòng)態(tài)特性在Swift里面也可以被限制的使用。

  • 靜態(tài)派發(fā)相比于動(dòng)態(tài)派發(fā)更快,而且靜態(tài)派發(fā)還會(huì)進(jìn)行內(nèi)聯(lián)等一些優(yōu)化,減少函數(shù)的尋址過(guò)程, 減少內(nèi)存地址的偏移計(jì)算等一系列操作,使函數(shù)的執(zhí)行速度更快,性能更高。

    一般情況下, 不同類型的函數(shù)調(diào)度方式如下

    類型調(diào)度方式extension
    值類型靜態(tài)派發(fā)靜態(tài)派發(fā)
    函數(shù)表派發(fā)靜態(tài)派發(fā)
    NSObject 子類函數(shù)表派發(fā)靜態(tài)派發(fā)

    類函數(shù)的動(dòng)態(tài)派發(fā)

    通過(guò)一個(gè)案例探究 動(dòng)態(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()

    在程序中, 斷點(diǎn)在函數(shù)處, 進(jìn)入?yún)R編代碼讀取寄存器匯中的值,

    image-20220110120757351

    這個(gè) 0x10004bab4 就是 teach() 函數(shù)的地址, 下面我們具體探究下中個(gè)地址是怎么來(lái)的.

    源碼的解讀

    一般來(lái)講, Swift會(huì)把所有的方法都被存在類的虛表中, 我們可以在 sil 文件中發(fā)現(xiàn)這個(gè) vtable.

    根據(jù)之前的分析, 類的結(jié)構(gòu) TargetClassMetadata 有個(gè)屬性 Description, 這個(gè)是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 它的內(nèi)存結(jié)構(gòu)如下

    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? }

    在這個(gè)描述的開(kāi)始到vtable之間的屬性有 13 ?? 4 = 52 字節(jié),后面就是存儲(chǔ)方法描述TargetMethodDescriptor的 vtable 了。

    struct?TargetMethodDescriptor?{///?Flags?describing?the?method.MethodDescriptorFlags?Flags;?//?4字節(jié),?標(biāo)識(shí)方法的種類,?初始化/getter/setter等等///?The?method?implementation.TargetRelativeDirectPointer<Runtime,?void>?Impl;?//?相對(duì)地址,?Offset?//?TODO:?add?method?types?or?anything?else?needed?for?reflection. };

    TargetMethodDescriptor 是對(duì)方法的描述, Flags表示方法的種類,占據(jù)4個(gè)字節(jié), Impl里面并不是真正的方法imp, 而是一個(gè)相對(duì)偏移量,所以需要找到這個(gè) TargetMethodDescriptor + 4字節(jié) + 相對(duì)偏移量 才能得到方法的真正地址。

    可執(zhí)行文件的解讀

    在可執(zhí)行文件中, Class、Struct、Enum 的 Discripter 地址信息一般存在 _TEXT,_swift5_types 段.

    image-20220110114300935

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

    找到 B85C,

    根據(jù) TargetClassDescriptor 的內(nèi)存結(jié)構(gòu),從 B85C 往后讀 52個(gè)字節(jié)就是 vtable,對(duì)應(yīng)的偏移量 B890.

    vtable是個(gè)數(shù)組,所以第一個(gè)元素 10 00 00 00 20 C2 FF FF 是 TargetMethodDescriptor, 再根據(jù) TargetMethodDescriptor 的內(nèi)存結(jié)構(gòu), 前面4字節(jié)是Flags, 后面4字節(jié)就是 Impl 的偏移量 Offset FFFFC220.

    回到程序中,

    通過(guò) image list 輸出可執(zhí)行文件加載的地址,其中第一個(gè)就是程序運(yùn)行首地址,0x100044000 加上 v-table偏移量,就得到v-table在程序運(yùn)行中的地址,也就是第一個(gè)函數(shù) teach() 的 TargetMethodDescriptor的地址 0x100044000 + 0xB890 = 0x10004F890

    然后加上 Flags 的4字節(jié),0x10004F890 + 0x4 = 0x10004F894 得到 Impl,

    加上Offset再減去虛擬內(nèi)存基地址 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, 那么就是直接調(diào)用的函數(shù)地址, 屬于靜態(tài)派發(fā).

    extension

    不論是 Class 或者 Struct, extension里的函數(shù)都是靜態(tài)派發(fā), 無(wú)法在運(yùn)行時(shí)做任何替換和改變, 因?yàn)槠淅锩娴姆椒ǘ际窃诰幾g期確定好的, 程序中以硬編碼的方式存在, 不會(huì)放在vtable中.

    extension?LGTeacher{func?teach3(){print("teach3")} }?var?t?=?LGTeacher() t.teach3()

    都是直接調(diào)用函數(shù)地址

    所以, 無(wú)法通過(guò) extension 支持多態(tài).

    那么為什么 Swift 會(huì)把 extension 設(shè)計(jì)成靜態(tài)的呢?

    OC中子類繼承后不重寫(xiě)方法的話是去父類中找方法實(shí)現(xiàn), 但是 Swift類在繼承的時(shí)候, 是把父類的方法形成一張vtable存在自己身上,這樣做也是為了節(jié)省方法的查找時(shí)間, 如果想讓 extension 加到 vtable 中, 并不是直接在子類vtable的最后直接追加就可以的, 需要在子類中記錄下父類方法的index,把父類的extension方法插入到子類vtable中父類方法index后相鄰的位置,再把子類自己的方法往后移動(dòng),這樣的一番操作消耗是很大的.

    關(guān)鍵字最派發(fā)方式的影響

    不同的函數(shù)修飾關(guān)鍵字對(duì)派發(fā)方式也有這不同的影響

    final

    final:添加了 final 關(guān)鍵字的函數(shù)無(wú)法被重寫(xiě)/繼承,使用靜態(tài)派發(fā),不會(huì)在 vtable 中出現(xiàn),且對(duì) objc 運(yùn)行時(shí)不可見(jiàn)。

    dynamic

    dynamic: 函數(shù)均可添加 dynamic 關(guān)鍵字,為非objc類和值類型的函數(shù)賦予動(dòng)態(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 賦予動(dòng)態(tài)性, 與 @_dynamicReplacement(for: teach()) 關(guān)鍵字配合使用, 把 teach()函數(shù)的實(shí)現(xiàn)改為 teach3()的實(shí)現(xiàn), 相當(dāng)于OC中把 teach()的SEL對(duì)應(yīng)為teach3()的imp, 實(shí)現(xiàn)方法的替換.

    這個(gè)具體的實(shí)現(xiàn)是 llvm 編譯器處理的, 在中間語(yǔ)言 IR 中, teach() 函數(shù)中有2個(gè)分支, 一個(gè) original, 一個(gè) forward, 如果我們有替換的函數(shù), 就走 forward 分支.

    #?轉(zhuǎn)成?IR?中間語(yǔ)言?.ll?文件 swiftc?-emit-ir?main.swift?>?dynamic.ll

    @objc

    @objc:該關(guān)鍵字可以將Swift函數(shù)暴露給Objc運(yùn)行時(shí),依舊是函數(shù)表派發(fā)。

    @objc dynamic

    @objc dynamic:消息派發(fā)的方式, 和 OC 一樣. 實(shí)際開(kāi)發(fā)中, Swift 和 OC 交互大多會(huì)使用這種方式.

    對(duì)于純Swift類, @objc dynamic 可以讓方法和OC一樣使用 Runtime API.

    如果需要和OC進(jìn)行交互, 需要把類繼承自 NSObjec.

    參考資料

    《Swift高級(jí)進(jìn)階班》[2]

    GitHub: apple[3] - swift源碼[4]

    《跟戴銘學(xué)iOS編程: 理順核心知識(shí)點(diǎn)》

    《程序員的自我修養(yǎng)》

    Swift編程語(yǔ)言 - 類和結(jié)構(gòu)體[5]

    Swift Intermediate Language 初探[6]

    Swift性能高效的原因深入分析[7]

    Swift編譯器中間碼SIL[8]

    Swift的高級(jí)中間語(yǔ)言:SIL[9]

    參考資料

    [1]

    Swift源碼: https://github.com/apple/swift

    [2]

    《Swift高級(jí)進(jìn)階班》: https://ke.qq.com/course/3202559

    [3]

    apple: https://github.com/apple

    [4]

    swift源碼: https://github.com/apple/swift

    [5]

    類和結(jié)構(gòu)體: 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簡(jiǎn)介

    [9]

    Swift的高級(jí)中間語(yǔ)言:SIL: https://www.jianshu.com/p/c2880460c6cd

    轉(zhuǎn)自:掘金-Jerod

    鏈接:https://juejin.cn/post/7051492093889347615

    -End-

    最近有一些小伙伴,讓我?guī)兔φ乙恍?面試題?資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來(lái),可以說(shuō)是程序員面試必備!所有資料都整理到網(wǎng)盤(pán)了,歡迎下載!

    點(diǎn)擊👆卡片,關(guān)注后回復(fù)【面試題】即可獲取

    在看點(diǎn)這里好文分享給更多人↓↓

    總結(jié)

    以上是生活随笔為你收集整理的Swift 中的类与结构体的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。