001@瞭解Objective-C語言的起源

Zack_Go發表於2018-12-28

瞭解Objective-C語言的起源

  • Objective-C為C語言新增了物件導向特性,是其超集。Objective-C使用了訊息結構,也就是說,在執行時才會檢查物件型別。接收一條訊息之後,究竟應執行何種程式碼,由執行期而非編譯器來決定,執行時才會去查詢所要執行的方法。
  • 理解C語言的核心概念有助於寫好Objective-C程式。尤其要掌握記憶體模型與指標。

使用了函式呼叫的語言(例如C++),則由編譯器決定。

執行元件

其實現原理是由執行期元件(runtime component)來實現的,使用Objective-c的物件導向特性所需的全部資料結構和函式都在執行元件裡面。執行元件本質上是一種與開發者所編程式碼相連結的“動態庫”,其程式碼能把開發者編寫的所有程式粘合起來,所以只要更新執行元件,即可提升程式效能。

OC記憶體模型與製作

Objective-C中的指標是用來指示物件的

物件分配到堆空間,指標分配到棧空間分配在堆中的記憶體必須直接管理,而分配在棧上用於儲存變數的記憶體會在其棧幀彈出時自動清理。

不含*的變數,可能會使用棧空間。例如結構體CGRect,使用物件來做,影響效能。因為與結構體相比,建立物件型別需要額外的開銷。

編譯型語言派發機制

程式派發的目的是為了告訴 CPU 需要被呼叫的函式在哪裡

編譯型語言有三種基礎的函式派發方式:

  • 直接派發(Direct Dispatch)
  • 函式表派發(Table Dispatch)
  • 訊息機制派發(Message Dispatch)

大多數語言都會支援一到兩種, Java 預設使用函式表派發, 但你可以通過 final 修飾符修改成直接派發. C++ 預設使用直接派發, 但可以通過加上 virtual 修飾符來改成函式表派發. 而 Objective-C 則總是使用訊息機制派發, 但允許開發者使用 C 直接派發來獲取效能的提高. 這樣的方式非常好, 但也給很多開發者帶來了困擾,

直接派發 (Direct Dispatch)

直接派發是最快的, 不止是因為需要呼叫的指令集會更少, 並且編譯器還能夠有很大的優化空間, 例如函式內聯等.直接派發也有人稱為靜態呼叫.

然而, 對於程式設計來說直接呼叫也是最大的侷限, 而且因為缺乏動態性所以沒辦法支援繼承.

函式表派發 (Table Dispatch )

函式表派發是編譯型語言實現動態行為最常見的實現方式. 函式表使用了一個陣列來儲存類宣告的每一個函式的指標. 大部分語言把這個稱為 virtual table(虛擬函式表), Swift 裡稱為 witness table. 每一個類都會維護一個函式表, 裡面記錄著類所有的函式, 如果父類函式被 override 的話, 表裡面只會儲存被 override 之後的函式. 一個子類新新增的函式, 都會被插入到這個陣列的最後. 執行時會根據這一個表去決定實際要被呼叫的函式.

舉個例子, 看看下面兩個類:

class ParentClass {
    func method1() {}
    func method2() {}
}
class ChildClass: ParentClass {
    override func method2() {}
    func method3() {}
}
複製程式碼

在這個情況下, 編譯器會建立兩個函式表, 一個是 ParentClass 的, 另一個是 ChildClass的: 這張表展示了 ParentClass 和 ChildClass 虛數表裡 method1, method2, method3 在記憶體裡的佈局.

let obj = ChildClass()
obj.method2()
複製程式碼

當一個函式被呼叫時, 會經歷下面的幾個過程:

  1. 讀取物件 0xB00 的函式表.
  2. 讀取函式指標的索引. 在這裡, method2 的索引是1(偏移量), 也就是 0xB00 + 1.
  3. 跳到 0x222 (函式指標指向 0x222)

查表是一種簡單, 易實現, 而且效能可預知的方式. 然而, 這種派發方式比起直接派發還是慢一點. 從位元組碼角度來看,多了兩次讀和一次跳轉, 由此帶來了效能的損耗.另一個慢的原因在於編譯器可能會由於函式內執行的任務導致無法優化. (如果函式帶有副作用的話)

這種基於陣列的實現, 缺陷在於函式表無法擴充,子類會在虛數函式表的最後插入新的函式, 沒有位置可以讓 extension 安全地插入函式

訊息機制派發 (Message Dispatch )

訊息機制是呼叫函式最動態的方式. 也是 Cocoa 的基石, 這樣的機制催生了 KVO, UIAppearence 和 CoreData 等功能. 這種運作方式的關鍵在於開發者可以在執行時改變函式的行為. 不止可以通過 swizzling 來改變, 甚至可以用 isa-swizzling 修改物件的繼承關係, 可以在物件導向的基礎上實現自定義派發.

舉個例子, 看看下面兩個類:

class ParentClass {
    dynamic func method1() {}
    dynamic func method2() {}
}
class ChildClass: ParentClass {
    override func method2() {}
    dynamic func method3() {}
}
複製程式碼

Swift 會用樹來構建這種繼承關係:

這張圖很好地展示了 Swift 如何使用樹來構建類和子類.

當一個訊息被派發, 執行時會順著類的繼承關係向上查詢應該被呼叫的函式. 如果你覺得這樣做效率很低, 它確實很低! 然而, 只要快取建立了起來, 這個查詢過程就會通過快取來把效能提高到和函式表派發一樣快. 但這只是訊息機制的原理。

Swift的派發機制與記憶體機制

行資料解析、資料對映和資料儲存的時候,Swift 的特性(協議、泛型、結構體和類)是如何影響應用效能的?

理解 Swift 派發機制

譯者注: 想要了解 Swift 底層結構的人, 極度推薦這段視訊

Swift 是一門靜態語言,所有在 Swift 中宣告的方法和屬性都是靜態編譯期就確定了的,同時,Swift 也支援動態繫結和動態派發,只需要將class裡的屬性或方法宣告為@objc dynamic即可,此時,Swift 的動態特性將使用 ObjC Runtime 來實現,完全相容 ObjC

swift中有四個選擇具體派發方式的因素存在:

  1. 宣告的位置
  2. 引用型別
  3. 特定的行為
  4. 顯式地優化(Visibility Optimizations)

Swift記憶體機制

物件的記憶體分配 (allocation) 和記憶體釋放 (deallocation) 是程式碼中最大的開銷之一,同時通常也是不可避免的。Swift 會自行分配和釋放記憶體,此外它存在兩種型別的分配方式。

  • 基於棧 (stack-based) 的記憶體分配。Swift 會盡可能選擇在棧上分配記憶體。棧是一種非常簡單的資料結構;資料從棧的底部推入 (push),從棧的頂部彈出 (pop)。由於我們只能夠修改棧的末端,因此我們可以通過維護一個指向棧末端的指標來實現這種資料結構,並且在其中進行記憶體的分配和釋放只需要重新分配該整數即可
  • 基於堆 (heap-based) 的記憶體分配。這使得記憶體分配將具備更加動態的生命週期,但是這需要更為複雜的資料結構。要在堆上進行記憶體分配的話,您需要鎖定堆當中的一個空閒塊 (free block),其大小能夠容納您的物件。因此,我們需要找到未使用的塊,然後在其中分配記憶體。當我們需要釋放記憶體的時候,我們就必須搜尋何處能夠重新插入該記憶體塊。這個操作很緩慢。主要是為了執行緒安全,我們必須要對這些東西進行鎖定和同步。

引用計數

引用計數是 Objective-C 和 Swift 中用於確定何時該釋放物件的安全機制。目前,Swift 當中的引用計數是強制自動管理的,無法優化。

結構體

蘋果推薦使用結構體,因為結構體會儲存在棧上,並且通常會使用靜態排程或者內聯排程。

排程與物件

Swift 擁有三種型別的排程方式。Swift 會盡可能將函式內聯 (inline),這樣的話使用這個函式將不會有額外的效能開銷。這個函式可以直接呼叫。靜態排程 (static dispatch) 本質上是通過 V-table 進行的查詢和跳轉,這個操作會花費一納秒的時間。 然後**動態排程(dynamic dispatch)**將會花費大概五納秒的時間,如果您只有幾個這樣的方法呼叫的話,這實際上並不會帶來多大的問題,問題是當您在一個巢狀迴圈或者執行上千次操作當中使用了動態排程的話,那麼它所帶來的效能耗費將成百上千地累積起來,最終影響應用效能。 參考文件:
《Effective Objective-C 2.0》
《深入理解 Swift 派發機制》
《真實世界中的 Swift 效能優化》

相關文章