iOS開發·runtime原理與實踐: 基本知識篇

JefferyChen發表於2019-10-14

摘要:這篇文章首先介紹runtime原理,包括類,超類,元類,super_class,isa,物件,方法,SEL,IMP等概念,同時分別介紹與這些概念有關的API。接著介紹方法呼叫流程,以及尋找IMP的過程。然後,介紹一下這些API的常見用法,並介紹runtime的冷門知識。最後介紹一下runtime的實戰指南。

 

iOS開發·runtime原理與實踐: 基本知識篇

1. 執行時

1.1 基本概念: 執行時

Runtime 的概念

Runtime 又叫執行時,是一套底層的 C 語言 API,其為 iOS 內部的核心之一,我們平時編寫的 OC 程式碼,底層都是基於它來實現的。比如:

iOS開發·runtime原理與實踐: 基本知識篇

以上你可能看不出它的價值,但是我們需要了解的是 Objective-C 是一門動態語言,它會將一些工作放在程式碼執行時才處理而並非編譯時。也就是說,有很多類和成員變數在我們編譯的時是不知道的,而在執行時,我們所編寫的程式碼會轉換成完整的確定的程式碼執行。

因此,編譯器是不夠的,我們還需要一個執行時系統(Runtime system)來處理編譯後的程式碼。Runtime 基本是用 C 和彙編寫的,由此可見蘋果為了動態系統的高效而做出的努力。蘋果和 GNU 各自維護一個開源的 Runtime 版本,這兩個版本之間都在努力保持一致。

Runtime 的作用

Objc 在三種層面上與 Runtime 系統進行互動:

透過 Objective-C 原始碼

透過 Foundation 框架的 NSObject 類定義的方法

透過對 Runtime 庫函式的直接呼叫

1.2 各種基本概念的C表達

在 Objective-C 中,類、物件和方法都是一個 C 的結構體,從  objc/objc.h(物件,objc_object,id)以及objc/runtime.h(其它,類,方法,方法列表,變數列表,屬性列表等相關的)以及中,我們可以找到他們的定義。

① 類

類物件(Class)是由程式設計師定義並在執行時由編譯器建立的,它沒有自己的例項變數,這裡需要注意的是類的 成員變數例項方法列表是屬於例項物件的,但其儲存於類物件當中的。我們在objc/objc.h下看看Class的定義:

Class

iOS開發·runtime原理與實踐: 基本知識篇

在這裡我還是要推薦下我自己建的iOS開發學習群:680565220,群裡都是學ios開發的,如果你正在學習ios ,小編歡迎你加入,今天分享的這個案例已經上傳到群檔案,大家都是軟體開發黨,不定期分享乾貨(只有iOS軟體開發相關的),包括我自己整理的一份2018最新的iOS進階資料和高階開發教程

可以看到類是由Class型別來表示的,它是一個objc_class結構型別的指標。我們接著來看objc_class結構體的定義:

objc_class

iOS開發·runtime原理與實踐: 基本知識篇

引數解析

isa指標是和Class同型別的objc_class結構指標,類物件的指標指向其所屬的類,即元類。元類中儲存著類物件的類方法,當訪問某個類的類方法時會透過該isa指標從元類中尋找方法對應的函式指標。

super_class指標指向該類所繼承的 父類物件,如果該類已經是最頂層的根類(如NSObject或NSProxy), 則 super_class為NULL。

iOS開發·runtime原理與實踐: 基本知識篇

cache:用於快取最近使用的方法。一個接收者物件接收到一個訊息時,它會根據isa指標去查詢能夠響應這個訊息的物件。在實際使用中,這個物件只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次訊息來時,我們都是methodLists中遍歷一遍,效能勢必很差。這時,cache就派上用場了。在我們每次呼叫過一個方法後,這個方法就會被快取到cache列表中,下次呼叫的時候runtime就會優先去cache中查詢,如果cache沒有,才去methodLists中查詢方法。這樣,對於那些經常用到的方法的呼叫,但提高了呼叫的效率。

version:我們可以使用這個欄位來提供類的版本資訊。這對於物件的序列化非常有用,它可是讓我們識別出不同類定義版本中例項變數佈局的改變。

protocols:當然可以看出這一個objc_protocol_list的指標。關於objc_protocol_list的結構體構成後面會講。

獲取類的類名

iOS開發·runtime原理與實踐: 基本知識篇

例項物件是我們對類物件alloc或者new操作時所建立的,在這個過程中會複製例項所屬的類的成員變數,但並不複製類定義的方法。呼叫例項方法時,系統會根據例項的isa指標去類的方法列表及父類的方法列表中尋找與訊息對應的selector指向的方法。同樣的,我們也來看下其定義:

objc_object

iOS開發·runtime原理與實踐: 基本知識篇

可以看到,這個結構體只有一個isa變數,指向例項物件所屬的類。任何帶有以指標開始並指向類結構的結構都可以被視作objc_object, 物件最重要的特點是可以給其傳送訊息。 NSObject類的alloc和allocWithZone:方法使用函式class_createInstance來建立objc_object資料結構。

另外我們常見的id型別,它是一個objc_object結構型別的指標。該型別的物件可以轉換為任何一種物件,類似於C語言中void *指標型別的作用。其定義如下所示:

id

iOS開發·runtime原理與實踐: 基本知識篇

元類(Metaclass)就是類物件的類,每個類都有自己的元類,也就是objc_class結構體裡面isa指標所指向的類. Objective-C的類方法是使用元類的根本原因,因為其中儲存著對應的類物件呼叫的方法即類方法。

當向物件發訊息,runtime會在這個物件所屬類方法列表中查詢傳送訊息對應的方法,但當向類傳送訊息時,runtime就會在這個類的meta class方法列表裡查詢。所有的meta class,包括Root class,Superclass,Subclass的isa都指向Root class的meta class,這樣能夠形成一個閉環。

 

iOS開發·runtime原理與實踐: 基本知識篇

所以由上圖可以看到,在給例項物件或類物件傳送訊息時,尋找方法列表的規則為:

當傳送訊息給例項物件時,訊息是在尋找這個物件的類的方法列表(例項方法)

當傳送訊息給類物件時,訊息是在尋找這個類的元類的方法列表(類方法)

元類,就像之前的類一樣,它也是一個物件,也可以呼叫它的方法。所以這就意味著它必須也有一個類。所有的元類都使用根元類作為他們的類。比如所有NSObject的子類的元類都會以NSObject的元類作為他們的類。

根據這個規則,所有的元類使用根元類作為他們的類,根元類的元類則就是它自己。也就是說基類的元類的isa指標指向他自己。

操作函式

super_class和meta-class

iOS開發·runtime原理與實踐: 基本知識篇

 

在Objective-C中,屬性(property)和成員變數是不同的。那麼,屬性的本質是什麼?它和成員變數之間有什麼區別?簡單來說屬性是新增了存取方法的成員變數,也就是:

 = ivar + getter + setter;

因此,我們每定義一個@property都會新增對應的ivar, getter和setter到類結構體objc_class中。具體來說,系統會在objc_ivar_list中新增一個成員變數的描述,然後在methodLists中分別新增setter和getter方法的描述。下面的objc_property_t是宣告的屬性的型別,是一個指向objc_property結構體的指標。

用法舉例

iOS開發·runtime原理與實踐: 基本知識篇

另外,關於屬性有一個objc_property_attribute_t結構體列表,objc_property_attribute_t結構體包含name和value

objc_property_attribute_t

iOS開發·runtime原理與實踐: 基本知識篇

常用的屬性如下:

屬性型別 name值:T value:變化

編碼型別 name值:C(copy) &(strong) W(weak)空(assign) 等 value:無

非/原子性 name值:空(atomic) N(Nonatomic) value:無

變數名稱 name值:V value:變化

例如

iOS開發·runtime原理與實踐: 基本知識篇

⑤ 成員變數

Ivar: 例項變數型別,是一個指向objc_ivar結構體的

iOS開發·runtime原理與實踐: 基本知識篇

指標

Ivar

iOS開發·runtime原理與實踐: 基本知識篇

在objc_class中,所有的成員變數、屬性的資訊是放在連結串列ivars中的。ivars是一個陣列,陣列中每個元素是指向Ivar(變數資訊)的指標。

objc_ivar_list

iOS開發·runtime原理與實踐: 基本知識篇

 

iOS開發·runtime原理與實踐: 基本知識篇

其中,

方法名型別為 SEL

方法型別 method_types 是個 char 指標,儲存方法的引數型別和返回值型別

method_imp 指向了方法的實現,本質是一個函式指標

簡言之,Method = SEL + IMP + method_types,相當於在SEL和IMP之間建立了一個對映。

操作函式

iOS開發·runtime原理與實踐: 基本知識篇

 

iOS開發·runtime原理與實踐: 基本知識篇

 

iOS開發·runtime原理與實踐: 基本知識篇

在原始碼中沒有直接找到 objc_selector 的定義,從一些書籍上與 Blog 上看到可以將 SEL 理解為一個 char* 指標。

具體這 objc_selector 結構體是什麼取決與使用GNU的還是Apple的執行時, 在Mac OS X中SEL其實被對映為一個C字串,可以看作是方法的名字,它並不一個指向具體方法實現(IMP型別才是)。

對於所有的類,只要方法名是相同的,產生的selector都是一樣的。

操作函式

iOS開發·runtime原理與實踐: 基本知識篇

實際上就是一個函式指標,指向方法實現的首地址。透過取得 IMP,我們可以跳過 runtime 的訊息傳遞機制,直接執行 IMP指向的函式實現,這樣省去了 runtime 訊息傳遞過程中所做的一系列查詢操作,會比直接向物件傳送訊息高效一些,當然必須說明的是,這種方式只適用於極特殊的最佳化場景,如效率敏感的場景下大量迴圈的呼叫某方法。

操作函式

iOS開發·runtime原理與實踐: 基本知識篇

上面提到了objc_class結構體中的cache欄位,它用於快取呼叫過的方法。這個欄位是一個指向objc_cache結構體的指標,其定義如下:

Cache

 

iOS開發·runtime原理與實踐: 基本知識篇

該結構體的欄位描述如下:

mask:一個整數,指定分配的快取bucket的總數。在方法查詢過程中,Objective-C runtime使用這個欄位來確定開始線性查詢陣列的索引位置。指向方法selector的指標與該欄位做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash雜湊演算法。

occupied:一個整數,指定實際佔用的快取bucket的總數。

buckets:指向Method資料結構指標的陣列。這個陣列可能包含不超過mask+1個元素。需要注意的是,指標可能是NULL,表示這個快取bucket沒有被佔用,另外被佔用的bucket可能是不連續的。這個陣列可能會隨著時間而增長。

⑪ 協議連結串列

前面objc_class的結構體中有個協議連結串列的引數,協議連結串列用來儲存宣告遵守的正式協議

objc_protocol_list

iOS開發·runtime原理與實踐: 基本知識篇

 

iOS開發·runtime原理與實踐: 基本知識篇

2. 方法呼叫流程

objc_msgSend() Tour 系列文章透過對 objc_msgSend 的彙編原始碼分析,總結出以下流程:

2.1 方法呼叫流程

檢查 selector 是否需要忽略

檢查 target 是否為 nil,如果是 nil 就直接 cleanup,然後 return

在 target 的 Class 中根據 selector 去找 IMP

2.2 尋找 IMP 的過程:

在當前 class 的方法快取裡尋找(cache methodLists)

找到了跳到對應的方法實現,沒找到繼續往下執行

從當前 class 的 方法列表裡查詢(methodLists),找到了新增到快取列表裡,然後跳轉到對應的方法實現;沒找到繼續往下執行

從 superClass 的快取列表和方法列表裡查詢,直到找到基類為止

以上步驟還找不到 IMP,則進入訊息動態處理和訊息轉發流程,詳見請關注微信公眾號:程式設計師大牛!

我們能在  objc4官方原始碼 中找到上述尋找 IMP 的過程,具體對應的程式碼如下:

objc-class.mm

iOS開發·runtime原理與實踐: 基本知識篇

3. 執行時相關的API

3.1 透過 Foundation 框架的 NSObject 類定義的方法

Cocoa 程式中絕大部分類都是 NSObject 類的子類,所以都繼承了 NSObject 的行為。(NSProxy 類時個例外,它是個抽象超類)

一些情況下,NSObject 類僅僅定義了完成某件事情的模板,並沒有提供所需要的程式碼。例如 -description 方法,該方法返回類內容的字串表示,該方法主要用來除錯程式。NSObject 類並不知道子類的內容,所以它只是返回類的名字和物件的地址,NSObject 的子類可以重新實現。

還有一些 NSObject 的方法可以從 Runtime 系統中獲取資訊,允許物件進行自我檢查。例如:

-class方法返回物件的類;

-isKindOfClass: 和 -isMemberOfClass: 方法檢查物件是否存在於指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變數);

-respondsToSelector: 檢查物件能否響應指定的訊息;

-conformsToProtocol:檢查物件是否實現了指定協議類的方法;

-methodForSelector: 返回指定方法實現的地址。

常見的一個例子:

iOS開發·runtime原理與實踐: 基本知識篇

 

iOS開發·runtime原理與實踐: 基本知識篇

 

iOS開發·runtime原理與實踐: 基本知識篇

 

id 和 void * 轉換API:(__bridge void *)

在 ARC 有效時,透過 (__bridge void *)轉換 id 和 void * 就能夠相互轉換。為什麼轉換?這是因為objc_getAssociatedObject的引數要求的。先看一下它的API:

iOS開發·runtime原理與實踐: 基本知識篇

可以知道,這個“屬性名”的key是必須是一個void *型別的引數。所以需要轉換。關於這個轉換,下面給一個轉換的例子:

 

iOS開發·runtime原理與實踐: 基本知識篇

4. 執行時實戰指南

上面的API不是提供大家背的,而是用來查閱的,當你要用到的時候查閱。因為這些原理和API光看沒用,需要實戰之後再回過頭來查閱和理解()

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69950672/viewspace-2659941/,如需轉載,請註明出處,否則將追究法律責任。

相關文章