Swift 一、類與結(jié)構(gòu)體(下)

類和結(jié)構(gòu)體下.png

一、異變方法

1.1 值類型添加/不添加mutating關(guān)鍵字的區(qū)別

Swift語言中的類型有值類型引用類型之分,對于引用類型,在實例方法中對實例屬性進(jìn)行修改是沒有問題的,但是對于值類型,讀者需要格外注意,默認(rèn)情況下,值類型屬性不能被修改。示例代碼如下:

///創(chuàng)建一個結(jié)構(gòu)體
struct Point {
    var x: Double
    var y: Double
    func move(x deltaX: Double, y deltaY: Double)  {
        x += deltaX
        y += deltaY
    }
}

編譯上面的代碼會報如下圖所示錯誤:


錯誤.png

對于值類型,使用mutating關(guān)鍵字修飾實例方法才能對屬性進(jìn)行修改,示例代碼如下:

///創(chuàng)建一個結(jié)構(gòu)體
struct Point {
    var x: Double
    var y: Double
    ///將點進(jìn)行移動,因為修改了屬性的值,需要用mutating修飾方法
    mutating func move(x deltaX: Double, y deltaY: Double)  {
        x += deltaX
        y += deltaY
    }
}

var point = Point(x: 3, y: 3)
///進(jìn)行移動,此時位置為(6,6)
point.move(x: 3, y: 3)

實際上,在值類型實例方法中修改值類型屬性的值就相當(dāng)于創(chuàng)建了一個新的實例,上面的代碼和下面的代碼原理是一致的:

///創(chuàng)建一個結(jié)構(gòu)體
struct Point {
    var x: Double
    var y: Double
    ///將點進(jìn)行移動,直接創(chuàng)建新的實例
    mutating func move(x deltaX: Double, y deltaY: Double)  {
        self = Point(x: self.x + x, y: self.y + y)
    }
}

var point = Point(x: 3, y: 3)
///進(jìn)行移動,此時位置為(6,6)
point.move(x: 3, y: 3)

1.2 SIL文檔探究異變方法本質(zhì)

下面我們通過SIL來對比一下,不添加mutating和添加mutating兩者有什么區(qū)別:

///創(chuàng)建一個結(jié)構(gòu)體
struct Point {
    var x: Double
    var y: Double
    ///沒有用mutating修飾,和下面的move函數(shù)進(jìn)行對比
    func test()  {
        let tmp = self.x
        print(tmp)
    }
    ///將點進(jìn)行移動,直接創(chuàng)建新的實例
    mutating func move(x deltaX: Double, y deltaY: Double)  {
        self = Point(x: self.x + x, y: self.y + y)
    }
}

var point = Point(x: 3, y: 3)
///進(jìn)行移動,此時位置為(6,6)
point.move(x: 3, y: 3)
// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Point):
  debug_value %0 : $Point, let, name "self", argno 1 // id: %1

分析上面的代碼,可以知道test函數(shù),有一個默認(rèn)參數(shù)self,類型是Point類型。這里的test函數(shù)實際就是let self = Point,是直接取值。

// Point.move(x:y:)
sil hidden @$s4main5PointV4move1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {
// %0 "deltaX"                                    // user: %3
// %1 "deltaY"                                    // user: %4
// %2 "self"                                      // users: %33, %23, %19, %11, %7, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*Point):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5

分析上面的代碼,可以知道move函數(shù),有兩個參數(shù)x,y,有一個默認(rèn)參數(shù)self,類型是Point類型,一個是一個@inout關(guān)鍵字,那么我們先來看一下inout在官方文檔的解釋是什么。

SIL 文檔的解釋 - inout
An @inout parameter is indirect. The address must be of an initialized object.(當(dāng)前參數(shù)類型是間接的,傳遞的是已經(jīng)初始化過的地址)

這里的move函數(shù)實際就是var self = *Point,由原來的直接取值改變?yōu)榱俗兞咳〉刂贰?/p>

由此我們可以得出異變方法的本質(zhì): 對于異變方法, 傳入的 self被標(biāo)記為 inout 參數(shù)。無論在mutating 方法內(nèi)部發(fā)生什么,都會影響外部依賴類型的一切。
如果在開發(fā)中真的需要在函數(shù)內(nèi)部修改傳遞參數(shù)的變量的值,可以將此參數(shù)聲明為inout 類型。

1.3 輸入輸出參數(shù)inout

輸入輸出參數(shù): 如果我們想函數(shù)能夠修改一個形式參數(shù)的值,而且希望這些改變在函數(shù)結(jié)束之后 依然生效,那么就需要將形式參數(shù)定義為 輸入輸出形式參數(shù) 。在形式參數(shù)定義開始的時候在前邊添加一個inout 關(guān)鍵字可以定義一個輸入輸出形式參數(shù)。

///在函數(shù)內(nèi)部修改參數(shù)變量的值
func myFunc(a: inout Int)  {
    a += 1
}

var a = 15;
myFunc(a: &a)
///將打印16
print(a)

上面的代碼中將參數(shù)a聲明為inout 類型,在傳參時需要使用‘&’ 符號,這個符號將傳遞參數(shù)變量的內(nèi)存地址。

二、方法調(diào)度

2.1 函數(shù)調(diào)用過程

在OC中,方法調(diào)度是通過消息發(fā)送機制,也就是objc_msgsend。那么在Swift中的方法調(diào)度又是怎樣的一種形式哪?我們一起通過下面的代碼來求證分析一下。

import UIKit

class ZGTeacher {
    func teach()  {
        print("teach")
    }
    func teach1()  {
        print("teach1")
    }
    func teach2()  {
        print("teach2")
    }
}



class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let t = ZGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }


}

給三個函數(shù)方法打上斷點,編譯時選擇Xcode->Debug -> Debug Workflow ->Always Show Disassembly,我們來看一下對應(yīng)的匯編打印

0x109299e8c <+108>: callq  0x109299dd0               ; ZGSwiftAPPTest.ZGTeacher.__allocating_init() -> ZGSwiftAPPTest.ZGTeacher at ViewController.swift:10
    0x109299e91 <+113>: movq   %rax, %r13
    0x109299e94 <+116>: movq   %r13, -0x30(%rbp)
    0x109299e98 <+120>: movq   %r13, -0x28(%rbp)
->  0x109299e9c <+124>: movq   (%r13), %rax
    0x109299ea0 <+128>: movq   0x50(%rax), %rax
    0x109299ea4 <+132>: callq  *%rax
    0x109299ea6 <+134>: movq   -0x30(%rbp), %r13
    0x109299eaa <+138>: movq   (%r13), %rax
    0x109299eae <+142>: movq   0x58(%rax), %rax
    0x109299eb2 <+146>: callq  *%rax
    0x109299eb4 <+148>: movq   -0x30(%rbp), %r13
    0x109299eb8 <+152>: movq   (%r13), %rax
    0x109299ebc <+156>: movq   0x60(%rax), %rax
    0x109299ec0 <+160>: callq  *%rax
    0x109299ec2 <+162>: movq   -0x30(%rbp), %rdi
    0x109299ec6 <+166>: callq  0x10929bac6               ; symbol stub for: swift_release

我們看到關(guān)鍵字__allocating_init(),這很明顯是在開辟空間,而關(guān)鍵字swift_release告知我們這里是在銷毀空間,而斷點停在第124行,0x109299e9c <+124>: movq (%r13), %rax,很明顯這里是我們的teach函數(shù)開始執(zhí)行的地方,同樣的,第138行和152行分別代表了teach1函數(shù)teach2函數(shù)執(zhí)行。
第128行 0x50(%rax)
第142行 0x58(%rax)
第156行 0x60(%rax)
這三行地址的值,每一個相差8個字節(jié),說明他們函數(shù)地址的值在內(nèi)存里是連續(xù)的一塊內(nèi)存空間。

通過上面匯編指令的對應(yīng)分析,可以知道函數(shù)teach的調(diào)用過程

  • Metadata
  • 確定函數(shù)地址(metadata + 偏移量)
  • 執(zhí)行函數(shù)xxx
    它們是基于函數(shù)表的調(diào)度,下面我們通過SIL文件的角度來看一下它的調(diào)度

2.2 基于函數(shù)表V-table的調(diào)度

下面我們?nèi)サ鬤code ->Debug -> Debug Workflow ->Always Show Disassembly,匯編指令打印,在項目中添加如下路徑的sh文件

路徑1.png

選擇other->Aggregate,創(chuàng)建一個Script的Target
路徑2.png

選擇New Run Script Phase添加一個新的sh文件,并在Run Script添加以下sh代碼

swiftc -emit-silgen -Onone -target x86_64-apple-ios15.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/ZGSwiftAPPTest/ViewController.swift > ./ViewController.sil && open ViewController.sil

編譯運行這個Script Target,就可以生成并打開對應(yīng)的sil文件。下面我們來分析一下這份sil文件。

sil_vtable ZGTeacher {
  #ZGTeacher.teach: (ZGTeacher) -> () -> () : @$s14ViewController9ZGTeacherC5teachyyF   // ZGTeacher.teach()
  #ZGTeacher.teach1: (ZGTeacher) -> () -> () : @$s14ViewController9ZGTeacherC6teach1yyF // ZGTeacher.teach1()
  #ZGTeacher.teach2: (ZGTeacher) -> () -> () : @$s14ViewController9ZGTeacherC6teach2yyF // ZGTeacher.teach2()
  #ZGTeacher.init!allocator: (ZGTeacher.Type) -> () -> ZGTeacher : @$s14ViewController9ZGTeacherCACycfC // ZGTeacher.__allocating_init()
  #ZGTeacher.deinit!deallocator: @$s14ViewController9ZGTeacherCfD   // ZGTeacher.__deallocating_deinit
}

這里就羅列了我們的 ZGTeacher函數(shù)里都有哪些函數(shù),是以vtable存放并羅列對應(yīng)函數(shù)的函數(shù)表。

2.3 typeDescriptor源碼分析

之前我們在第一節(jié)課講到了 Metdata 的數(shù)據(jù)結(jié)構(gòu),那么 V-Table是存放在什么地方那? 我們先來回顧一下當(dāng)前的數(shù)據(jù)結(jié)構(gòu)。

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

}

這里我們有一個東西需要關(guān)注typeDescriptor ,不管是 Class , Struct , Enum 都有自己 的 Descriptor ,就是對類的一個詳細(xì)描述。
我們通過查看Swift源碼,找到這個Metadata.h文件

ConstTargetMetadataPointer<Runtime, TargetClassDescriptor>
  getDescription() const {
    return Description;
  }
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
    
}

2.4 Mach-o文件讀取分析

Mach-O: Mach-O 其實是Mach Object文件格式的縮寫,是 mac 以及 iOS 上可執(zhí)行文件的格 式, 類似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常見的 .o,.a .dylib Framework,dyld .dsym
Mach-O文件格式:

Mach-O文件格式.png

  • 首先是文件頭,表明該文件是 Mach-O 格式,指定目標(biāo)架構(gòu),還有一些其他的文件屬性信 息,文件頭信息影響后續(xù)的文件結(jié)構(gòu)安排
  • Load commands是一張包含很多內(nèi)容的表。內(nèi)容包括區(qū)域的位置、符號表、動態(tài)符號表 等。
    Load commands.png
  • Data 區(qū)主要就是負(fù)責(zé)代碼和數(shù)據(jù)記錄的。Mach-O 是以 Segment 這種結(jié)構(gòu)來組織數(shù)據(jù)的,一個 Segment 可以包含 0 個或多個 Section。根據(jù) Segment 是映射的哪一個 Load CommandSegmentSection 就可以被解讀為是是代碼,常量或者一些其他的數(shù)據(jù)類 型。在裝載在內(nèi)存中時,也是根據(jù) Segment 做內(nèi)存映射的。
    在新建項目的時候,Xcode 不會自動生成 Products文件,可以參考這篇文章:Xcode13 新建項目 Products 目錄顯示方法

MachOView 工具打開 Mach-O 文件的格式大概長這樣:

Descriptor.png

前面的四個字節(jié) 80 FB FF FF 就是 ZGTeacherDescriptor 信息,那用 80 FB FF FF 加上前面的 00007CAC 得到的就是 Descriptor 在當(dāng)前 Mach-O 文件的內(nèi)存地址。

它們怎么相加呢,iOS 屬于小端模式,所以 80 FB FF FF 要從右邊往左讀。也就是:

0xFFFFFB80 + 0x00007CAC = 0x10000782C

0x10000782C 這個值是我拿計算器算的,那么 0x100000000 就是 Mach-O 文件中虛擬內(nèi)存的基地址,如下圖所示:

虛擬內(nèi)存的基地址.png

我們用0x10000782C - 0x100000000 = 0x782C 就是 ZGTeacher 在整個 Data 區(qū)的內(nèi)存地址。我們找到 TEXT, const。

0x782C.png

如圖所示,這個0x7820是首地址,偏移12個字節(jié)就是0x782c,也是意味著,它后面的數(shù)據(jù)是 TargetClassDescriptor的數(shù)據(jù),所以我們可以在這里拿到 ZGTeacher 的虛函數(shù)表 - ZGTeacher 方法的地址。

計算 TargetClassDescriptorVTable 前面的數(shù)據(jù)大小,求得偏移量。一共 12 個 4 字節(jié)(48字節(jié))的成員變量,12 個四字節(jié)的成員變量再加上 size(4字節(jié))得到 52 字節(jié),再往后的 24 字節(jié)就是teach,teach1,teach2 方法的結(jié)構(gòu)地址(一個函數(shù)地址占 8 字節(jié))。如圖所示:

Teach方法地址@2x.png

如圖中所示,0x7860 - 0x7867teach 結(jié)構(gòu)在 Mach-O 文件的地址。那么在程序中如何找到該地址呢。

ASLR 是一個隨機偏移地址,這個隨機偏移地址的目的是為了給應(yīng)用程序一個隨機內(nèi)存地址。

image list 是列出應(yīng)用程序運行的模塊,我們找到第一個,其內(nèi)存地址為 0x000000010eaeb000,這個地址就是當(dāng)前應(yīng)用程序的基地址。

接下來我在Swift源碼中找到這么一個結(jié)構(gòu)體TargetMethodDescriptor

struct TargetMethodDescriptor {
  /// 4字節(jié)
  MethodDescriptorFlags Flags;

  /// 這里存儲的是相對指針,offset
  TargetRelativeDirectPointer<Runtime, void> Impl;

};

到這里,TargetMethodDescriptor 結(jié)構(gòu)體的地址就可以確定了,那么要找到函數(shù)地址,還需要偏移 Flags + Impl,得到的就是函數(shù)的地址。 綜合以上的邏輯開始計算:

// 應(yīng)用程序的基地址:0x000000010eaeb000,teach 結(jié)構(gòu)地址:0x7860,F(xiàn)lags:0x4,offset:1C C2 FF FF
// 注意!小端模式要從右往左,所以為 FFFFC21C
0x000000010C493000 + 7860 + 0x4 + FFFFC21C = 0x20EAEEA80

// 接下來需要減掉 Mach-O 文件的虛擬地址 0x100000000,得到的就是函數(shù)的地址。
0x20EAEEA80 - 0x100000000 = 0x10EAEEA80

打開匯編調(diào)試,讀取匯編中 teach 的地址,驗證 0x10EAEEA80 就是否就是 teach 的地址。到這里就完全驗證了 Swift 類的方法確實是存放在 VTable - 虛函數(shù)表里面的。

2.5 方法調(diào)度方式總結(jié)

方法調(diào)度方式總結(jié).png

三、影響函數(shù)派發(fā)方式

3.1 final

添加了final 關(guān)鍵字的函數(shù)無法被重寫,使用靜態(tài)派發(fā),不會在vtable中出現(xiàn),且對objc運行時不可見。
示例如下:

class Shape {
    final var center:(Double, Double)
    init() {
        center = (0, 0)
    }
}

3.2 dynamic

函數(shù)均可添加 dynamic 關(guān)鍵字,為非objc類和值類型的函數(shù)賦予動態(tài)性,但派發(fā)方式還是函數(shù)表派發(fā)。

3.3 @objc

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

3.4 @objc + dynamic

消息派發(fā)的方式

四、函數(shù)內(nèi)聯(lián)

4.1 什么是函數(shù)內(nèi)聯(lián)

函數(shù)內(nèi)聯(lián) 是一種編譯器優(yōu)化技術(shù),它通過使用方法的內(nèi)容替換直接調(diào)用該方法,從而優(yōu) 化性能。

4.2 @inline(__always)

將確保始終內(nèi)聯(lián)函數(shù)。通過在函數(shù)前添加 @inline(__always) 來實現(xiàn)此行為。

4.3 @inline(never)

將確保永遠(yuǎn)不會內(nèi)聯(lián)函數(shù)。這可以通過在函數(shù)前添加 @inline(never) 來實現(xiàn)。 如果函數(shù)很長并且想避免增加代碼段大小,請使用@inline(never)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容