本文主要跟你們分享iOS攻城獅比較感興趣的知識點runtime。示例代碼在這裏:WHRuntimeDemo 讀完並理解這篇文章以後,你將掌握下面這幾個問題的答案。git
- 什麼是runtime運行時
- 什麼是isa指針
- 什麼是SEL,什麼是IMP, 什麼是Method
- 什麼是消息機制
- runtime運行時的8種使用場景
**runtime:**Objective-C是動態語言,它將不少靜態語言在編譯和連接時作的事放到了運行時,這個運行時系統就是runtime。 runtime其實就是一個庫,它基本上是用C和彙編寫的一套API,這個庫使C語言有了面向對象的能力。 靜態語言:在編譯的時候會決定調用哪一個函數。 動態語言(OC):在運行的時候根據函數的名稱找到對應的函數來調用。github
**isa:**OC中,類和類的實例在本質上沒有區別,都是對象,任何對象都有isa指針,它指向類或元類(元類後面會講解)。數組
**SEL:**SEL(選擇器)是方法的selector的指針。方法的selector表示運行時方法的名字。OC在編譯時,會依據每個方法的名字、參數,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL。緩存
**IMP:**IMP是一個函數指針,指向方法最終實現的首地址。SEL就是爲了查找方法的最終實現IMP。網絡
**Method:**用於表示類定義中的方法,它的結構體中包含一個SEL和IMP,至關於在SEL和IMP之間做了一個映射。併發
**消息機制:**任何方法的調用本質就是發送一個消息。編譯器會將消息表達式[receiver message]轉化爲一個消息函數objc_msgSend(receiver, selector)。函數
**Runtime的使用:**獲取屬性列表,獲取成員變量列表,得到方法列表,獲取協議列表,方法交換(黑魔法),動態的添加方法,調用私有方法,爲分類添加屬性。ui
概述中已經說了,runtime其實就是一個庫,這個庫主要作了兩件事情:spa
- 封裝:runtime把對象用C語言的結構體來表示,方法用C語言的函數來表示。這些結構體和函數被runtime封裝後,咱們就能夠在程序運行的時候,對類/對象/方法進行操做。
- 尋找方法的最終執行:當執行[receiver message]的時候,至關於向receiver發送一條消息message。runtime會根據reveiver可否處理這條message,從而作出不一樣的反應。
在OC中,類是用Class來表示的,而Class其實是一個指向objc_class結構體的指針。 設計
來看一下objc_class的定義
在這裏只說一下cache
Cache用於緩存最近使用的方法。一個類只有一部分方法是經常使用的,每次調用一個方法以後,這個方法就被緩存到cache中,下次調用時runtime會先在cache中查找,若是cache中沒有,纔會去methodList中查找。有了cache,常常用到的方法的調用效率就提升了!
你只要記住,runtime其實就是一個庫,它是一套API,這個庫使C語言有了面向對象的能力。咱們能夠運用runtime這個庫裏面的各類方法,在程序運行的時候對類/實例對象/變量/屬性/方法等進行各類操做。
在解釋isa以前,你須要知道,在Objective-C中,全部的類自身也是一個對象,咱們能夠向這個對象發送消息(調用類方法)。
先來看一下runtime中實例對象的結構體objc_object。
從結構體中能夠看到,這個結構體只包含一個指向其類的isa指針。
**isa指針的做用:**當咱們向一個對象發送消息時,runtime會根據這個對象的isa指針找到這個對象所屬的類,在這個類的方法列表及父類的方法列表中,尋找與消息對應的selector指向的方法,找到後就運行這個方法。
要完全理解isa,還須要瞭解一下元類的概念。下面咱們用類方法建立了一個字典。
這句代碼把+dictionary消息發送給NSDictionary類,而這個NSDictionary也是一個對象,既然是對象,那麼它也會有一個isa指針,類的isa指針指向什麼呢?
爲了調用+dictionary方法,這個類的isa指針必須指向一個包含這些類方法的objc_class結構體,這就引出了元類的概念。meta-class(元類)存儲着一個類的全部類方法。
向一個對象發送消息時,runtime會在這個對象所屬的類的方法列表中查找方法; 向一個類發送消息時,會在這個類的meta-class(元類)的方法列表中查找。
meta-class是一個類,也能夠向它發送消息,那麼它的isa又是指向什麼呢?爲了避免讓這種結構無限延伸下去,Objective-C的設計者讓全部的meta-class的isa指向基類(NSObject)的meta-class,而基類的meta-class的isa指針是指向本身(NSObject)。
下圖中的虛線箭頭表示的是isa指針,實線箭頭表示的是父類。
能夠看出,全部實例對象的isa都指向它所屬的類,而類的isa是指向它的元類,全部元類的isa指向基類的meta-class,基類的meta-class的isa指向本身。須要注意的是,root-class(基類)的superclass是nil。
SEL又叫選擇器,是方法的selector的指針。
方法的selector用於表示運行時方法的名字。Objective-C在編譯時,會依據每個方法的名字、參數序列,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL。
兩個類之間,不管它們是父子關係,仍是沒有關係,只要方法名相同,那麼方法的SEL就是同樣的,每個方法都對應着一個SEL,因此在 Objective-C同一個類中,不能存在2個同名的方法,即便參數類型不一樣也不行。像下面這種狀況就會報錯。
SEL是一個指向方法的指針,是根據方法名hash化了的一個字符串,而對於字符串的比較僅僅須要比較他們的地址就能夠了,因此速度上很是優秀,它的存在只是爲了加快方法的查詢速度。
不一樣的類能夠擁有相同的selector,不一樣類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector尋找對應的IMP。SEL就是爲了查找方法的最終實現IMP。
IMP其實是一個函數指針,指向方法實現的首地址。表明了方法的最終實現。
第一個參數是指向self的指針(若是是實例方法,則是類實例的內存地址;若是是類方法,則是指向元類的指針),第二個參數是方法選擇器(selector),省略號是方法的參數。
每一個方法對應惟一的SEL,經過SEL快速準確地得到對應的 IMP,取得IMP後,就得到了執行這個方法代碼了。
Method是用於表示類的方法。
Method結構體中包含一個SEL和IMP,實際上至關於在SEL和IMP之間做了一個映射。有了SEL,咱們即可以找到對應的IMP,從而調用方法的實現代碼。
當執行了[receiver message]的時候,至關於向receiver發送一條消息message。runtime會根據reveiver可否處理這條message,從而作出不一樣的反應。
消息直到運行時才綁定到方法的實現上。編譯器會將消息表達式[receiver message]轉化爲一個消息函數,即objc_msgSend(receiver, selector)。
objc_msgSend作了以下事情:
- 經過對象的isa指針獲取類的結構體。
- 在結構體的方法表裏查找方法的selector。
- 若是沒有找到selector,則經過objc_msgSend結構體中指向父類的指針找到父類,並在父類的方法表裏查找方法的selector。
- 依次會一直找到NSObject。
- 一旦找到selector,就會獲取到方法實現IMP。
- 傳入相應的參數來執行方法的具體實現。
- 若是最終沒有定位到selector,就會走消息轉發流程。
以 [receiver message]的方式調用方法,若是receiver沒法響應message,編譯器會報錯。但若是是以performSelector來調用,則須要等到運行時才能肯定object是否能接收message消息。若是不能,則程序崩潰。
當咱們不能肯定一個對象是否能接收某個消息時,會先調用respondsToSelector:來判斷一下
若是不使用respondsToSelector:來判斷,那麼這就能夠用到「消息轉發」機制。
當對象沒法接收消息,就會啓動消息轉發機制,經過這一機制,告訴對象如何處理未知的消息。
這樣就能夠採起一些措施,讓程序執行特定的邏輯,從而避免崩潰。措施分爲三個步驟。
1. 動態方法解析
對象接收到未知的消息時,首先會調用所屬類的類方法+resolveInstanceMethod:(實例方法)或 者+resolveClassMethod:(類方法)。
在這個方法中,咱們有機會爲該未知消息新增一個」處理方法」。使用該「處理方法」的前提是已經實現,只須要在運行時經過class_addMethod函數,動態的添加到類裏面就能夠了。代碼以下。
2. 備用接收者
若是在上一步沒法處理消息,則Runtime會繼續調下面的方法。
若是這個方法返回一個對象,則這個對象會做爲消息的新接收者。注意這個對象不能是self自身,不然就是出現無限循環。若是沒有指定對象來處理aSelector,則應該 return [super forwardingTargetForSelector:aSelector]。
可是咱們只將消息轉發到另外一個能處理該消息的對象上,沒法對消息進行處理,例如操做消息的參數和返回值。
3. 完整消息轉發
若是在上一步仍是不能處理未知消息,則惟一能作的就是啓用完整的消息轉發機制。此時會調用如下方法:
這是最後一次機會將消息轉發給其它對象。建立一個表示消息的NSInvocation對象,把與消息的有關所有細節封裝在anInvocation中,包括selector,目標(target)和參數。在forwardInvocation 方法中將消息轉發給其它對象。
forwardInvocation:方法的實現有兩個任務:
a. 定位能夠響應封裝在anInvocation中的消息的對象。 b. 使用anInvocation做爲參數,將消息發送到選中的對象。anInvocation將會保留調用結果,runtime會提取這一結果併發送到消息的原始發送者。
在這個方法中咱們能夠實現一些更復雜的功能,咱們能夠對消息的內容進行修改。另外,若發現消息不該由本類處理,則應調用父類的同名方法,以便繼承體系中的每一個類都有機會處理。
另外,必須重寫下面的方法:
消息轉發機制從這個方法中獲取信息來建立NSInvocation對象。完整的示例以下:
NSObject的forwardInvocation方法只是調用了
doesNotRecognizeSelector方法,它不會轉發任何消息。若是不在以上所述的三個步驟中處理未知消息,則會引起異常。 forwardInvocation就像一個未知消息的分發中心,將這些未知的消息轉發給其它對象。或者也能夠像一個運輸站同樣將全部未知消息都發送給同一個接收對象,取決於具體的實現。
消息的轉發機制能夠用下圖來幫助理解。
代碼以下圖,運用class_copyPropertyList方法來得到屬性列表,遍歷把屬性加入數組中,最終返回此數組。其中[selfdictionaryWithProperty:properties[i]] 方法是用來拿到屬性的描述,例如copy,readonly,NSString等信息。Demo
代碼以下圖,運用class_copyIvarList方法來得到變量列表,經過遍歷把變量加入到數組中,最終返回此數組。其中[[selfclass]decodeType:ivar_getTypeEncoding(ivars[i])]方法是用來拿到變量的類型,例如char,int,unsigned long等信息。Demo
代碼以下圖,經過runtime的class_copyMethodList方法來獲取方法列表,經過遍歷把方法加入到數組中,最終返回此數組。Demo
代碼以下,運用class_copyProtocolList方法來得到協議列表。Demo
下面就是runtime的重頭戲了,被稱做黑魔法的方法交換Swizzling。交換方法是在method_exchangeImplementations裏發生的。Demo
使用Swizzling的過程當中要注意兩個問題:
Swizzling要在+load方法中執行 運行時會自動調用每一個類的兩個方法,+load與+initialize。 +load會在main函數以前調用,而且必定會調用。 +initialize是在第一次調用類方法或實例方法以前被調用,有可能一直不被調用。 通常使用Swizzling是爲了影響全局,因此爲了方法交換必定成功,Swizzling要放在+load中執行。
Swizzling要在dispatch_once中執行 Swzzling是爲了影響全局,因此只讓它執行一次就能夠了,因此要放在dispatch_once中。
方法交換的代碼以下圖。
方法交換有很多應用場景,好比記錄頁面被點開的次數:只要在UIViewController的分類的+load中交換viewDidAppear方法,在交換的方法中添加記錄代碼就能夠了。
我這裏舉一個例子,Swizzling的實際應用:
代碼以下圖,結合代碼理解。 當網絡加載不到圖片時,自動添加佔位圖片,而且不改變圖片的原始調用方法。 在UIimage分類的+load方法中用dispatch_once_t來進行方法的交換,把系統的imageNamed與本身寫的wh_imageNamed進行交換,本身寫的wh_imageNamed中已經進行了佔位圖片的處理。 在別的地方使用imageNamed來拿圖片,實際上已經調用了wh_imageNamed,而且在圖片不存在的時候自動放上一張佔位圖。 注意!本身寫的交換方法中要調用[self wh_imageNamed:@"test」],須要這樣寫,不會形成死循環。
代碼以下,運用runtime的class_addMethod來添加一個方法。Demo
添加方法的運用這裏說一下兩種狀況:
前提:接收到未知的消息時,首先會調用所屬類的類方法+resolveInstanceMethod:(實例方法)或+resolveClassMethod:(類方法)。 第一種狀況是,根據已知的方法名動態的添加一個方法。 第二種狀況是,直接添加一個方法。 代碼以下圖
因爲消息機制,runtime能夠經過objc_msgSend來幫咱們調用一些私有方法。Demo
使用objc_msgSend須要注意兩個問題:
須要導入頭文件#import <objc/message.h> 按照下圖在Build Settings裏設置
在分類中屬性不會自動生成實例變量和存取方法,可是能夠運用runtime的關聯對象(Associated Object)來解決這個問題。Demo
使用 objc_getAssociatedObject 和 objc_setAssociatedObject 來作到存取方法,使用關聯對象模擬實例變量。下面是兩個方法的原型:
方法中的的*@selector(categoryProperty)*就是參數key,使用 @selector(categoryProperty) 做爲 key 傳入,能夠確保 key 的惟一性。
OBJC_ASSOCIATION_COPY_NONATOMIC 是屬性修飾符。
以上就是與runtime有關的一些總結,文章若是有什麼不許確的地方,歡迎指出,共同進步。謝謝!
本文所述的源碼在這裏: WHRuntimeDemo