Swift中的类和结构体(2)
Swift中的類和結構體(2)
- 異變方法
- 方法調度
- 影響函數派發方式
異變方法
在Swift中,值類型屬性不能被自身的實例方法修改,編譯器不會通過編譯,報錯Left side of mutating operator isn't mutable: 'self' is immutable,自身是不能修改自身的。
當加上mutating關鍵字后就可以通過編譯
可以通過sil來進行分析:
// LLPerson相當于self // LLPerson.test() sil hidden @$s4main8LLPersonV4testyyF : $@convention(method) (LLPerson) -> () { // %0 "self" // users: %2, %1 bb0(%0 : $LLPerson): //$ LLPerson, let, name "self" 是直接傳入的值類型selfdebug_value %0 : $LLPerson, let, name "self", argno 1 // id: %1在test方法中,相當于傳入了值類型self,而在moveBy方法中
// 傳入了@inout關鍵字的LLPerson,及@inout關鍵字的self // LLPerson.moveBy(x:y:) sil hidden @$s4main8LLPersonV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout LLPerson) -> () { // %0 "deltaX" // users: %10, %3 // %1 "deltaY" // users: %20, %4 // %2 "self" // users: %16, %6, %5 bb0(%0 : $Double, %1 : $Double, %2 : $*LLPerson):debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4 // 從這里可以看出,是*LLPerson類型的self,即LLPerson的指針debug_value_addr %2 : $*LLPerson, var, name "self", argno 3 // id: %5在moveBy方法中傳入了*LLPerson,即指針self。
SIL 文檔的解釋為:
An @inout parameter is indirect. The address must be of an initialized object.(當前參數 類型是間接的,傳遞的是已經初始化過的地址)
異變方法的本質:
對于變異方法,傳入的 self 被標記為 inout 參數。無論在 mutating 方法內部發生什么,都會影響外部依賴類型的一切。
inout輸入輸出參數:
如果我們想函數能夠修改一個形式參數的值,而且希望這些改變在函數結束之后依然生效,那么就需要將形式參數定義為輸入輸出形式參數 。在形式參數定義開始的時候在前邊添加一個 inout關鍵字可以定義一個輸入輸出形式參數。
我們可以在函數中看這個inout:
var age = 10 //添加了inout func modifyage(_ age: inout Int) {// 如果使用temp,只是一個普通的賦值,不會改變外部agevar temp = agetemp += 1 } // 必須使用&age modifyage(&age) print(age)
當我們這樣時:
相當于我們在寫C函數時:
我們修改不了a的值。
但當我們這樣時:
我們是可以修改a的值的。
方法調度
在OC中,方法調度是objc_mgsend的方式。而在Swift中,我們可以通過代碼和匯編來探究。
class LLTeacher {func teach() {print("teach")}func teach1() {print("teach1")}func teach2() {print("teach2")} }let teacher = LLTeacher() teacher.teach() teacher.teach1() teacher.teach2()我們通過斷點調試,獲取相關匯編代碼
0x1026d7888 <+116>: bl 0x1000077c4 ; _23456.LLTeacher.__allocating_init() -> _23456.LLTeacher at ViewController.swift:10 // __allocating_init返回LLTeacher 返回值存儲到x0寄存器中 x0的第一個8字節存儲的是metadata 0x1026d788c <+120>: str x0, [sp, #0x20] // 將x0的值保存到棧中 0x1026d7890 <+124>: ldr x8, [x0] // 將x0的地址給x8 0x1026d7894 <+128>: ldr x8, [x8, #0x50] // x8加上0x50個字節的地址給x8 即metadate + 0x50就是 teach 這時候拿到了teach 0x1026d7898 <+132>: mov x20, x0 0x1026d789c <+136>: str x0, [sp, #0x8] 0x1026d78a0 <+140>: blr x8 // 執行teach 0x1026d78a4 <+144>: ldr x8, [sp, #0x8] 0x1026d78a8 <+148>: ldr x9, [x8] 0x1026d78ac <+152>: ldr x9, [x9, #0x58] // 0x50 0x58 相差8字節,正好是指針8字節,所以這些函數時連續的內存空間 0x1026d78b0 <+156>: mov x20, x8 0x1026d78b4 <+160>: blr x9 // 執行teach1 0x1026d78b8 <+164>: ldr x8, [sp, #0x8] 0x1026d78bc <+168>: ldr x9, [x8] 0x1026d78c0 <+172>: ldr x9, [x9, #0x60] 0x1026d78c4 <+176>: mov x20, x8 0x1026d78c8 <+180>: blr x9 // 執行teach2 0x1026d78cc <+184>: ldr x0, [sp, #0x8] 0x1026d78d0 <+188>: bl 0x10000992c ; symbol stub for: swift_release寄存器x0讀取出來,正好是metadata。
通過讀取x8寄存器的值,可以得出,位置為teach方法
匯編相關資料可以參考:常用指令
所以我們可以猜測:teach函數的調用過程是找到 Metadata ,確定函數地址(metadata + 偏移量),然后執行函數,是基于函數表的調度。
我們還可以通過sil查看一些相關內容:
一種vtable的形式存儲。
同時,我們可以從源碼來看一下相關的結構。上一章節我們獲取到Metdata的一個結構:
從這里我們需要關注 typeDescriptor,不管是Class,Struct,Enum都有自己 的 Descriptor,就是對類的一個詳細描述。我們通過對源碼的分析,可以得出typeDescriptor的結構
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 //V-Table位置 }下面我們通過mach-o文件來查看V-Table的位置。
我們通過MachOView打開mach-o文件:
下面我們通過內存地址,來查找V-Table:
首先,我們拿到存放Swift類的前四個字節地址0xFFFFFBA8 + 0x0000BBCC
接著我們通過0x10000B774減去虛擬內存基地址0x100000000拿到Descriptor的位置
下面,我們通過MachOView查找到0xB774(Descriptor)的位置:
在接下來,我們通過Descriptor的結構,去標記V-Table的位置:
這時候我們獲取了V-Table的位置,即teach的地址0x0000B7A8。
我們通過0x0000B7A8 + ASLR(隨機偏移地址,即程序運行的基地址)。
通過image list獲取程序運行的基地址是0x0000000104790000:
0x0000B7A8 + ASLR,我們可以獲取teach函數在內存中的地址:
我們通過源碼,可以獲取函數在內存中的結構為:
struct TargetMethodDescriptor {MethodDescriptor Flags; //4字節//存儲的offsetTargetRelativeDriectPointer<Runtime, void> Impl; }接下來,我們通過0x10479B7A8(TargetMethodDescriptor的地址) + 0x4( Flags) + 0xFFFFC234(Impl offset),就可以得到teach的函數地址。
0x10479B7A80x4 + 0xFFFFC234 -----------------------0x2047979E0 - 0x100000000 //還需要減去虛擬內存基地址0x100000000 -----------------------0x1047979E0 //teach的函數地址我們通過上面的匯編,可以讀取x8寄存器里面的內容,就是我們計算的地址:
Mahco的一些知識:
Mahco: Mach-O 其實是Mach Object文件格式的縮寫,是 mac 以及 iOS上可執行文件的格 式, 類似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常見的 .o,.a .dylib Framework,dyld .dsym。
Macho文件格式:
首先是文件頭,表明該文件是 Mach-O 格式,指定目標架構,還有一些其他的文件屬性信 息,文件頭信息影響后續的文件結構安排
Load commands是一張包含很多內容的表。內容包括區域的位置、符號表、動態符號表等。
Data 區主要就是負責代碼和數據記錄的。Mach-O 是以 Segment 這種結構來組織數據 的,一個 Segment 可以包含 0 個或多個 Section。根據 Segment 是映射的哪一個 Load Command,Segment 中 section 就可以被解讀為是是代碼,常量或者一些其他的數據類 型。在裝載在內存中時,也是根據 Segment 做內存映射的。
當我們將class改為struct時
struct LLTeacher {func teach() {print("teach")}func teach1() {print("teach1")}func teach2() {print("teach2")} }let teacher = LLTeacher() teacher.teach() teacher.teach1() teacher.teach2()通過匯編我們發現,結構體的函數調用是直接靜態派發,即編譯的時候就已經確定了地址。
當我們使用extension時,調用方式也是使用的靜態派發,包括類的extension也是如此。
這樣做的好處是,不需要再V-Table中插入extension中的方法,減少消耗。
最后總結方法調度方式:
| 值類型 | 靜態派發 | 靜態派發 |
| 類 | 函數表派發 | 靜態派發 |
| NSObject子類 | 函數表派發 | 靜態派發 |
影響函數派發方式
final:添加了 final 關鍵字的函數無法被重寫,使用靜態派發,不會在vtable 中出現,且對 objc 運行時不可見。
final func teach() {print("teach") }dynamic:函數均可添加 dynamic 關鍵字,為非objc類和值類型的函數賦予動態性,但派發方式還是函數表派發。配合@dynamicReplacement(for:)使用
@objc:該關鍵字可以將Swift函數暴露給Objc運行時,依舊是函數表派發。
@objc + dynamic:消息派發的方式
class LLPerson: NSObject {@objc dynamic func teach() {print("teach")} }
會暴露函數給OC使用,可以使用methed-swizzing等
總結
以上是生活随笔為你收集整理的Swift中的类和结构体(2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 仿微信弹出二维码
- 下一篇: vue全局自定义字体,提高项目字体美化