runtime執行時 isa指標 SEL方法選擇器 IMP函式指標 Method方法 runtime訊息機制 runtime的使用

wuhao丶發表於2017-09-20

目的

本文主要跟大家分享iOS攻城獅比較感興趣的知識點runtime。示例程式碼在這裡:WHRuntimeDemo
讀完並理解這篇文章之後,你將掌握下面這幾個問題的答案。

  1. 什麼是runtime執行時
  2. 什麼是isa指標
  3. 什麼是SEL,什麼是IMP, 什麼是Method
  4. 什麼是訊息機制
  5. runtime執行時的8種使用場景

What a beautiful day!
What a beautiful day!

概述

runtime:Objective-C是動態語言,它將很多靜態語言在編譯和連結時做的事放到了執行時,這個執行時系統就是runtime。
runtime其實就是一個庫,它基本上是用C和彙編寫的一套API,這個庫使C語言有了物件導向的能力。
靜態語言:在編譯的時候會決定呼叫哪個函式。
動態語言(OC):在執行的時候根據函式的名稱找到對應的函式來呼叫。

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的使用:獲取屬性列表,獲取成員變數列表,獲得方法列表,獲取協議列表,方法交換(黑魔法),動態的新增方法,呼叫私有方法,為分類新增屬性。

一、什麼是runtime執行時

概述中已經說了,runtime其實就是一個庫,這個庫主要做了兩件事情:

  1. 封裝:runtime把物件用C語言的結構體來表示,方法用C語言的函式來表示。這些結構體和函式被runtime封裝後,我們就可以在程式執行的時候,對類/物件/方法進行操作。
  2. 尋找方法的最終執行:當執行[receiver message]的時候,相當於向receiver傳送一條訊息message。runtime會根據reveiver能否處理這條message,從而做出不同的反應。

在OC中,類是用Class來表示的,而Class實際上是一個指向objc_class結構體的指標。

Class
Class

來看一下objc_class的定義

objc_class的定義
objc_class的定義

在這裡只說一下cache

Cache用於快取最近使用的方法。一個類只有一部分方法是常用的,每次呼叫一個方法之後,這個方法就被快取到cache中,下次呼叫時runtime會先在cache中查詢,如果cache中沒有,才會去methodList中查詢。有了cache,經常用到的方法的呼叫效率就提高了!

你只要記住,runtime其實就是一個庫,它是一套API,這個庫使C語言有了物件導向的能力。我們可以運用runtime這個庫裡面的各種方法,在程式執行的時候對類/例項物件/變數/屬性/方法等進行各種操作。

二、什麼是isa指標

在解釋isa之前,你需要知道,在Objective-C中,所有的類自身也是一個物件,我們可以向這個物件傳送訊息(呼叫類方法)。

先來看一下runtime中例項物件的結構體objc_object。

objc_object
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。

isa指標
isa指標

三、什麼是SEL,IMP,Method

SEL

SEL又叫選擇器,是方法的selector的指標。

SEL
SEL

方法的selector用於表示執行時方法的名字。Objective-C在編譯時,會依據每一個方法的名字、引數序列,生成一個唯一的整型標識(Int型別的地址),這個標識就是SEL。

兩個類之間,無論它們是父子關係,還是沒有關係,只要方法名相同,那麼方法的SEL就是一樣的,每一個方法都對應著一個SEL,所以在 Objective-C同一個類中,不能存在2個同名的方法,即使引數型別不同也不行。像下面這種情況就會報錯。

報錯
報錯

SEL是一個指向方法的指標,是根據方法名hash化了的一個字串,而對於字串的比較僅僅需要比較他們的地址就可以了,所以速度上非常優秀,它的存在只是為了加快方法的查詢速度。

不同的類可以擁有相同的selector,不同類的例項物件執行相同的selector時,會在各自的方法列表中去根據selector尋找對應的IMP。SEL就是為了查詢方法的最終實現IMP。

IMP

IMP實際上是一個函式指標,指向方法實現的首地址。代表了方法的最終實現。

IMP
IMP

第一個引數是指向self的指標(如果是例項方法,則是類例項的記憶體地址;如果是類方法,則是指向元類的指標),第二個引數是方法選擇器(selector),省略號是方法的引數。

每個方法對應唯一的SEL,通過SEL快速準確地獲得對應的 IMP,取得IMP後,就獲得了執行這個方法程式碼了。

Method

Method是用於表示類的方法。

Method
Method

Method結構體中包含一個SEL和IMP,實際上相當於在SEL和IMP之間作了一個對映。有了SEL,我們便可以找到對應的IMP,從而呼叫方法的實現程式碼。

四、什麼是訊息機制

當執行了[receiver message]的時候,相當於向receiver傳送一條訊息message。runtime會根據reveiver能否處理這條message,從而做出不同的反應。

方法(訊息機制)的呼叫流程

訊息直到執行時才繫結到方法的實現上。編譯器會將訊息表示式[receiver message]轉化為一個訊息函式,即objc_msgSend(receiver, selector)。

objc_msgSend
objc_msgSend

objc_msgSend做了如下事情:

  1. 通過物件的isa指標獲取類的結構體。
  2. 在結構體的方法表裡查詢方法的selector。
  3. 如果沒有找到selector,則通過objc_msgSend結構體中指向父類的指標找到父類,並在父類的方法表裡查詢方法的selector。
  4. 依次會一直找到NSObject。
  5. 一旦找到selector,就會獲取到方法實現IMP。
  6. 傳入相應的引數來執行方法的具體實現。
  7. 如果最終沒有定位到selector,就會走訊息轉發流程。

訊息轉發機制

以 [receiver message]的方式呼叫方法,如果receiver無法響應message,編譯器會報錯。但如果是以performSelector來呼叫,則需要等到執行時才能確定object是否能接收message訊息。如果不能,則程式崩潰。

當我們不能確定一個物件是否能接收某個訊息時,會先呼叫respondsToSelector:來判斷一下

respondsToSelector
respondsToSelector

如果不使用respondsToSelector:來判斷,那麼這就可以用到“訊息轉發”機制。

當物件無法接收訊息,就會啟動訊息轉發機制,通過這一機制,告訴物件如何處理未知的訊息。

這樣就可以採取一些措施,讓程式執行特定的邏輯,從而避免崩潰。措施分為三個步驟。

1. 動態方法解析

物件接收到未知的訊息時,首先會呼叫所屬類的類方法+resolveInstanceMethod:(例項方法)或 者+resolveClassMethod:(類方法)。

在這個方法中,我們有機會為該未知訊息新增一個”處理方法”。使用該“處理方法”的前提是已經實現,只需要在執行時通過class_addMethod函式,動態的新增到類裡面就可以了。程式碼如下。

class_addMethod
class_addMethod

2. 備用接收者

如果在上一步無法處理訊息,則Runtime會繼續調下面的方法。

forwardingTargetForSelector
forwardingTargetForSelector

如果這個方法返回一個物件,則這個物件會作為訊息的新接收者。注意這個物件不能是self自身,否則就是出現無限迴圈。如果沒有指定物件來處理aSelector,則應該 return [super forwardingTargetForSelector:aSelector]。

但是我們只將訊息轉發到另一個能處理該訊息的物件上,無法對訊息進行處理,例如操作訊息的引數和返回值。

forwardingTargetForSelector
forwardingTargetForSelector

3. 完整訊息轉發

如果在上一步還是不能處理未知訊息,則唯一能做的就是啟用完整的訊息轉發機制。此時會呼叫以下方法:

forwardInvocation
forwardInvocation

這是最後一次機會將訊息轉發給其它物件。建立一個表示訊息的NSInvocation物件,把與訊息的有關全部細節封裝在anInvocation中,包括selector,目標(target)和引數。在forwardInvocation 方法中將訊息轉發給其它物件。

forwardInvocation:方法的實現有兩個任務:

a. 定位可以響應封裝在anInvocation中的訊息的物件。
b. 使用anInvocation作為引數,將訊息傳送到選中的物件。anInvocation將會保留呼叫結果,runtime會提取這一結果併傳送到訊息的原始傳送者。

在這個方法中我們可以實現一些更復雜的功能,我們可以對訊息的內容進行修改。另外,若發現訊息不應由本類處理,則應呼叫父類的同名方法,以便繼承體系中的每個類都有機會處理。

另外,必須重寫下面的方法:

methodSignatureForSelector
methodSignatureForSelector

訊息轉發機制從這個方法中獲取資訊來建立NSInvocation物件。完整的示例如下:

完整訊息轉發
完整訊息轉發

NSObject的forwardInvocation方法只是呼叫了

doesNotRecognizeSelector方法,它不會轉發任何訊息。如果不在以上所述的三個步驟中處理未知訊息,則會引發異常。
forwardInvocation就像一個未知訊息的分發中心,將這些未知的訊息轉發給其它物件。或者也可以像一個運輸站一樣將所有未知訊息都傳送給同一個接收物件,取決於具體的實現。

訊息的轉發機制可以用下圖來幫助理解。

訊息的轉發機制
訊息的轉發機制

五、runtime的使用

1. 獲取屬性列表

程式碼如下圖,運用class_copyPropertyList方法來獲得屬性列表,遍歷把屬性加入陣列中,最終返回此陣列。其中[selfdictionaryWithProperty:properties[i]] 方法是用來拿到屬性的描述,例如copy,readonly,NSString等資訊。Demo

獲取屬性列表
獲取屬性列表

2. 獲取成員變數列表

程式碼如下圖,運用class_copyIvarList方法來獲得變數列表,通過遍歷把變數加入到陣列中,最終返回此陣列。其中[[selfclass]decodeType:ivar_getTypeEncoding(ivars[i])]方法是用來拿到變數的型別,例如char,int,unsigned long等資訊。Demo

獲取成員變數列表
獲取成員變數列表

3. 獲取方法列表

程式碼如下圖,通過runtime的class_copyMethodList方法來獲取方法列表,通過遍歷把方法加入到陣列中,最終返回此陣列。Demo

獲取方法列表
獲取方法列表

4. 獲取協議列表

程式碼如下,運用class_copyProtocolList方法來獲得協議列表。Demo

獲取協議列表
獲取協議列表

5. 方法交換(黑魔法)

下面就是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”],需要這樣寫,不會造成死迴圈。

交換imageNamed方法
交換imageNamed方法

6. 新增方法

程式碼如下,運用runtime的class_addMethod來新增一個方法。Demo

新增方法
新增方法

新增方法的運用這裡說一下兩種情況:

前提:接收到未知的訊息時,首先會呼叫所屬類的類方法+resolveInstanceMethod:(例項方法)或+resolveClassMethod:(類方法)。
第一種情況是,根據已知的方法名動態的新增一個方法。
第二種情況是,直接新增一個方法。
程式碼如下圖

新增方法
新增方法

7. 呼叫私有方法

由於訊息機制,runtime可以通過objc_msgSend來幫我們呼叫一些私有方法。Demo

呼叫私有方法
呼叫私有方法

使用objc_msgSend需要注意兩個問題:

需要匯入標頭檔案#import
按照下圖在Build Settings裡設定

設定使用objc_msgSend
設定使用objc_msgSend

8. 為分類新增屬性

在分類中屬性不會自動生成例項變數和存取方法,但是可以運用runtime的關聯物件(Associated Object)來解決這個問題。Demo

為分類新增屬性
為分類新增屬性

使用 objc_getAssociatedObject 和 objc_setAssociatedObject 來做到存取方法,使用關聯物件模擬例項變數。下面是兩個方法的原型:

關聯屬性
關聯屬性

方法中的的@selector(categoryProperty)就是引數key,使用 @selector(categoryProperty) 作為 key 傳入,可以確保 key 的唯一性。

OBJC_ASSOCIATION_COPY_NONATOMIC 是屬性修飾符。

屬性修飾符
屬性修飾符

後記

以上就是與runtime有關的一些總結,文章如果有什麼不準確的地方,歡迎指出,共同進步。謝謝!
推薦簡單又好用的分類集合:WHKit
本文所述的原始碼在這裡: WHRuntimeDemo

相關文章