摘要:這篇文章首先介紹runtime原理,包括類,超類,元類,super_class,isa,對象,方法,SEL,IMP等概念,同時分別介紹與這些概念有關的API。接着介紹方法調用流程,以及尋找IMP的過程。而後,介紹一下這些API的常見用法,並介紹runtime的冷門知識。最後介紹一下runtime的實戰指南。ios
1. 運行時程序員
1.1 基本概念: 運行時算法
Runtime 的概念數組
Runtime 又叫運行時,是一套底層的 C 語言 API,其爲 iOS 內部的核心之一,咱們平時編寫的 OC 代碼,底層都是基於它來實現的。好比:緩存
以上你可能看不出它的價值,可是咱們須要瞭解的是 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開發學習羣:680565220,羣裏都是學ios開發的,若是你正在學習ios ,小編歡迎你加入,今天分享的這個案例已經上傳到羣文件,你們都是軟件開發黨,不按期分享乾貨(只有iOS軟件開發相關的),包括我本身整理的一份2018最新的iOS進階資料和高級開發教程
能夠看到類是由Class類型來表示的,它是一個objc_class結構類型的指針。咱們接着來看objc_class結構體的定義:
objc_class
參數解析
isa指針是和Class同類型的objc_class結構指針,類對象的指針指向其所屬的類,即元類。元類中存儲着類對象的類方法,當訪問某個類的類方法時會經過該isa指針從元類中尋找方法對應的函數指針。
super_class指針指向該類所繼承的父類對象,若是該類已是最頂層的根類(如NSObject或NSProxy), 則 super_class爲NULL。
cache:用於緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找可以響應這個消息的對象。在實際使用中,這個對象只有一部分方法是經常使用的,不少方法其實不多用或者根本用不上。這種狀況下,若是每次消息來時,咱們都是methodLists中遍歷一遍,性能勢必不好。這時,cache就派上用場了。在咱們每次調用過一個方法後,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,若是cache沒有,纔去methodLists中查找方法。這樣,對於那些常常用到的方法的調用,但提升了調用的效率。
version:咱們可使用這個字段來提供類的版本信息。這對於對象的序列化很是有用,它但是讓咱們識別出不一樣類定義版本中實例變量佈局的改變。
protocols:固然能夠看出這一個objc_protocol_list的指針。關於objc_protocol_list的結構體構成後面會講。
獲取類的類名
實例對象是咱們對類對象alloc或者new操做時所建立的,在這個過程當中會拷貝實例所屬的類的成員變量,但並不拷貝類定義的方法。調用實例方法時,系統會根據實例的isa指針去類的方法列表及父類的方法列表中尋找與消息對應的selector指向的方法。一樣的,咱們也來看下其定義:
objc_object
能夠看到,這個結構體只有一個isa變量,指向實例對象所屬的類。任何帶有以指針開始並指向類結構的結構均可以被視做objc_object, 對象最重要的特色是能夠給其發送消息。 NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來建立objc_object數據結構。
另外咱們常見的id類型,它是一個objc_object結構類型的指針。該類型的對象能夠轉換爲任何一種對象,相似於C語言中void *指針類型的做用。其定義以下所示:
id
元類(Metaclass)就是類對象的類,每一個類都有本身的元類,也就是objc_class結構體裏面isa指針所指向的類. Objective-C的類方法是使用元類的根本緣由,由於其中存儲着對應的類對象調用的方法即類方法。
當向對象發消息,runtime會在這個對象所屬類方法列表中查找發送消息對應的方法,但當向類發送消息時,runtime就會在這個類的meta class方法列表裏查找。全部的meta class,包括Root class,Superclass,Subclass的isa都指向Root class的meta class,這樣可以造成一個閉環。
因此由上圖能夠看到,在給實例對象或類對象發送消息時,尋找方法列表的規則爲:
當發送消息給實例對象時,消息是在尋找這個對象的類的方法列表(實例方法)
當發送消息給類對象時,消息是在尋找這個類的元類的方法列表(類方法)
元類,就像以前的類同樣,它也是一個對象,也能夠調用它的方法。因此這就意味着它必須也有一個類。全部的元類都使用根元類做爲他們的類。好比全部NSObject的子類的元類都會以NSObject的元類做爲他們的類。
根據這個規則,全部的元類使用根元類做爲他們的類,根元類的元類則就是它本身。也就是說基類的元類的isa指針指向他本身。
操做函數
super_class和meta-class
在Objective-C中,屬性(property)和成員變量是不一樣的。那麼,屬性的本質是什麼?它和成員變量之間有什麼區別?簡單來講屬性是添加了存取方法的成員變量,也就是:
@property = ivar + getter + setter;
所以,咱們每定義一個@property都會添加對應的ivar, getter和setter到類結構體objc_class中。具體來講,系統會在objc_ivar_list中添加一個成員變量的描述,而後在methodLists中分別添加setter和getter方法的描述。下面的objc_property_t是聲明的屬性的類型,是一個指向objc_property結構體的指針。
用法舉例
另外,關於屬性有一個objc_property_attribute_t結構體列表,objc_property_attribute_t結構體包含name和value
objc_property_attribute_t
經常使用的屬性以下:
屬性類型 name值:T value:變化
編碼類型 name值:C(copy) &(strong) W(weak)空(assign) 等 value:無
非/原子性 name值:空(atomic) N(Nonatomic) value:無
變量名稱 name值:V value:變化
例如
⑤ 成員變量
Ivar: 實例變量類型,是一個指向objc_ivar結構體的
指針
Ivar
在objc_class中,全部的成員變量、屬性的信息是放在鏈表ivars中的。ivars是一個數組,數組中每一個元素是指向Ivar(變量信息)的指針。
objc_ivar_list
其中,
方法名類型爲 SEL
方法類型 method_types 是個 char 指針,存儲方法的參數類型和返回值類型
method_imp 指向了方法的實現,本質是一個函數指針
簡言之,Method = SEL + IMP + method_types,至關於在SEL和IMP之間創建了一個映射。
操做函數
在源碼中沒有直接找到 objc_selector 的定義,從一些書籍上與 Blog 上看到能夠將 SEL 理解爲一個 char* 指針。
具體這 objc_selector 結構體是什麼取決與使用GNU的仍是Apple的運行時, 在Mac OS X中SEL其實被映射爲一個C字符串,能夠看做是方法的名字,它並不一個指向具體方法實現(IMP類型纔是)。
對於全部的類,只要方法名是相同的,產生的selector都是同樣的。
操做函數
實際上就是一個函數指針,指向方法實現的首地址。經過取得 IMP,咱們能夠跳過 runtime 的消息傳遞機制,直接執行 IMP指向的函數實現,這樣省去了 runtime 消息傳遞過程當中所作的一系列查找操做,會比直接向對象發送消息高效一些,固然必須說明的是,這種方式只適用於極特殊的優化場景,如效率敏感的場景下大量循環的調用某方法。
操做函數
上面提到了objc_class結構體中的cache字段,它用於緩存調用過的方法。這個字段是一個指向objc_cache結構體的指針,其定義以下:
Cache
該結構體的字段描述以下:
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
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
3. 運行時相關的API
3.1 經過 Foundation 框架的 NSObject 類定義的方法
Cocoa 程序中絕大部分類都是 NSObject 類的子類,因此都繼承了 NSObject 的行爲。(NSProxy 類時個例外,它是個抽象超類)
一些狀況下,NSObject 類僅僅定義了完成某件事情的模板,並無提供所須要的代碼。例如 -description 方法,該方法返回類內容的字符串表示,該方法主要用來調試程序。NSObject 類並不知道子類的內容,因此它只是返回類的名字和對象的地址,NSObject 的子類能夠從新實現。
還有一些 NSObject 的方法能夠從 Runtime 系統中獲取信息,容許對象進行自我檢查。例如:
-class方法返回對象的類;
-isKindOfClass: 和 -isMemberOfClass: 方法檢查對象是否存在於指定的類的繼承體系中(是不是其子類或者父類或者當前類的成員變量);
-respondsToSelector: 檢查對象可否響應指定的消息;
-conformsToProtocol:檢查對象是否實現了指定協議類的方法;
-methodForSelector: 返回指定方法實現的地址。
常見的一個例子:
id 和 void * 轉換API:(__bridge void *)
在 ARC 有效時,經過 (__bridge void *)轉換 id 和 void * 就可以相互轉換。爲何轉換?這是由於objc_getAssociatedObject的參數要求的。先看一下它的API:
能夠知道,這個「屬性名」的key是必須是一個void *類型的參數。因此須要轉換。關於這個轉換,下面給一個轉換的例子:
4. 運行時實戰指南
上面的API不是提供你們背的,而是用來查閱的,當你要用到的時候查閱。由於這些原理和API光看沒用,須要實戰以後再回過頭來查閱和理解。筆者另外寫了runtime的原理與實踐。若是想了解runtime的更多知識,能夠關注微信公衆號:程序員大牛,天天分享乾貨!