本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具有了靈活的動態特性,使這門古老的語言煥發生機。主要內容以下:html
引言
曾經以爲Objc特別方便上手,面對着 Cocoa 中大量 API,只知道簡單的查文檔和調用。還記得初學 Objective-C 時把 [receiver message]
當成簡單的方法調用,而無視了「發送消息」這句話的深入含義。其實 [receiver message]
會被編譯器轉化爲:node
1 |
objc_msgSend(receiver, selector) |
若是消息含有參數,則爲:ios
1 |
objc_msgSend(receiver, selector, arg1, arg2, ...) |
若是消息的接收者可以找到對應的 selector
,那麼就至關於直接執行了接收者這個對象的特定方法;不然,消息要麼被轉發,或是臨時向接收者動態添加這個 selector
對應的實現內容,要麼就乾脆玩完崩潰掉。git
如今能夠看出 [receiver message]
真的不是一個簡簡單單的方法調用。由於這只是在編譯階段肯定了要向接收者發送 message
這條消息,而 receive
將要如何響應這條消息,那就要看運行時發生的狀況來決定了。程序員
Objective-C 的 Runtime 鑄就了它動態語言的特性,這些深層次的知識雖然平時寫代碼用的少一些,可是倒是每一個 Objc 程序員須要瞭解的。github
簡介
由於Objc是一門動態語言,因此它老是想辦法把一些決定工做從編譯鏈接推遲到運行時。也就是說只有編譯器是不夠的,還須要一個運行時系統 (runtime system) 來執行編譯後的代碼。這就是 Objective-C Runtime 系統存在的意義,它是整個 Objc 運行框架的一塊基石。objective-c
Runtime其實有兩個版本: 「modern」 和 「legacy」。咱們如今用的 Objective-C 2.0 採用的是現行 (Modern) 版的 Runtime 系統,只能運行在 iOS 和 macOS 10.5 以後的 64 位程序中。而 maxOS 較老的32位程序仍採用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在於當你更改一個類的實例變量的佈局時,在早期版本中你須要從新編譯它的子類,而現行版就不須要。算法
Runtime 基本是用 C 和彙編寫的,可見蘋果爲了動態系統的高效而做出的努力。你能夠在這裏下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。編程
與 Runtime 交互
Objc 從三種不一樣的層級上與 Runtime 系統進行交互,分別是經過 Objective-C 源代碼,經過 Foundation 框架的NSObject
類定義的方法,經過對 runtime 函數的直接調用。數組
Objective-C 源代碼
大部分狀況下你就只管寫你的Objc代碼就行,runtime 系統自動在幕後辛勤勞做着。
還記得引言中舉的例子吧,消息的執行會使用到一些編譯器爲實現動態語言特性而建立的數據結構和函數,Objc中的類、方法和協議等在 runtime 中都由一些數據結構來定義,這些內容在後面會講到。(好比 objc_msgSend
函數及其參數列表中的 id
和 SEL
都是啥)
NSObject 的方法
Cocoa 中大多數類都繼承於 NSObject
類,也就天然繼承了它的方法。最特殊的例外是 NSProxy
,它是個抽象超類,它實現了一些消息轉發有關的方法,能夠經過繼承它來實現一個其餘類的替身類或是虛擬出一個不存在的類,說白了就是領導把本身展示給你們風光無限,可是把活兒都交給幕後小弟去幹。
有的NSObject
中的方法起到了抽象接口的做用,好比description
方法須要你重載它併爲你定義的類提供描述內容。NSObject
還有些方法能在運行時得到類的信息,並檢查一些特性,好比class
返回對象的類;isKindOfClass:
和isMemberOfClass:
則檢查對象是否在指定的類繼承體系中;respondsToSelector:
檢查對象可否響應指定的消息;conformsToProtocol:
檢查對象是否實現了指定協議類的方法;methodForSelector:
則返回指定方法實現的地址。
Runtime 的函數
Runtime 系統是一個由一系列函數和數據結構組成,具備公共接口的動態共享庫。頭文件存放於/usr/include/objc
目錄下。許多函數容許你用純C代碼來重複實現 Objc 中一樣的功能。雖然有一些方法構成了NSObject
類的基礎,可是你在寫 Objc 代碼時通常不會直接用到這些函數的,除非是寫一些 Objc 與其餘語言的橋接或是底層的debug工做。在 Objective-C Runtime Reference 中有對 Runtime 函數的詳細文檔。
Runtime 基礎數據結構
還記得引言中的objc_msgSend:
方法吧,它的真身是這樣的:
1 |
id objc_msgSend ( id self, SEL op, ... ); |
下面將會逐漸展開介紹一些術語,其實它們都對應着數據結構。熟悉 Objective-C 類的內存模型或看過相關源碼的能夠直接跳過。
SEL
objc_msgSend
函數第二個參數類型爲SEL
,它是selector
在Objc中的表示類型(Swift中是Selector
類)。selector
是方法選擇器,能夠理解爲區分方法的 ID,而這個 ID 的數據結構是SEL
:
1 |
typedef struct objc_selector *SEL; |
其實它就是個映射到方法的C字符串,你能夠用 Objc 編譯器命令 @selector()
或者 Runtime 系統的 sel_registerName
函數來得到一個 SEL
類型的方法選擇器。
不一樣類中相同名字的方法所對應的方法選擇器是相同的,即便方法名字相同而變量類型不一樣也會致使它們具備相同的方法選擇器,因而 Objc 中方法命名有時會帶上參數類型(NSNumber
一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。
id
objc_msgSend
第一個參數類型爲id
,你們對它都不陌生,它是一個指向類實例的指針:
1 |
typedef struct objc_object *id; |
那objc_object
又是啥呢,參考 objc-private.h 文件部分源碼:
1 |
struct objc_object { |
objc_object
結構體包含一個 isa
指針,類型爲 isa_t
聯合體。根據 isa
就能夠順藤摸瓜找到對象所屬的類。isa
這裏還涉及到 tagged pointer 等概念。由於 isa_t
使用 union
實現,因此可能表示多種形態,既能夠當成是指針,也能夠存儲標誌位。有關 isa_t
聯合體的更多內容能夠查看 Objective-C 引用計數原理。
PS: isa
指針不老是指向實例對象所屬的類,不能依靠它來肯定類型,而是應該用 class
方法來肯定實例對象的類。由於KVO的實現機理就是將被觀察對象的 isa
指針指向一箇中間類而不是真實的類,這是一種叫作 isa-swizzling 的技術,詳見官方文檔
Class
Class
實際上是一個指向 objc_class
結構體的指針:
1 |
typedef struct objc_class *Class; |
而 objc_class
包含不少方法,主要都爲圍繞它的幾個成員作文章:
1 |
struct objc_class : objc_object { |
objc_class
繼承於 objc_object
,也就是說一個 ObjC 類自己同時也是一個對象,爲了處理類和對象的關係,runtime 庫建立了一種叫作元類 (Meta Class) 的東西,類對象所屬類型就叫作元類,它用來表述類對象自己所具有的元數據。類方法就定義於此處,由於這些方法能夠理解成類對象的實例方法。每一個類僅有一個類對象,而每一個類對象僅有一個與之相關的元類。當你發出一個相似 [NSObject alloc]
的消息時,你事實上是把這個消息發給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。全部的元類最終都指向根元類爲其超類。全部的元類的方法列表都有可以響應消息的類方法。因此當 [NSObject alloc]
這條消息發給類對象的時候,objc_msgSend()
會去它的元類裏面去查找可以響應消息的方法,若是找到了,而後對這個類對象執行方法調用。
上圖實線是 superclass
指針,虛線是isa
指針。 有趣的是根元類的超類是 NSObject
,而 isa
指向了本身,而 NSObject
的超類爲 nil
,也就是它沒有超類。
能夠看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。
cache_t
1 |
struct cache_t { |
_buckets
存儲 IMP
,_mask
和 _occupied
對應 vtable
。
cache
爲方法調用的性能進行優化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa
指向的類的方法列表中遍歷查找可以響應消息的方法,由於這樣效率過低了,而是優先在 cache
中查找。Runtime 系統會把被調用的方法存到 cache
中(理論上講一個方法若是被調用,那麼它有可能從此還會被調用),下次查找的時候效率更高。
bucket_t
中存儲了指針與 IMP 的鍵值對:
1 |
struct bucket_t { |
有關緩存的實現細節,能夠查看 objc-cache.mm 文件。
class_data_bits_t
objc_class
中最複雜的是 bits
,class_data_bits_t
結構體所包含的信息太多了,主要包含 class_rw_t
, retain/release/autorelease/retainCount
和 alloc
等信息,不少存取方法也是圍繞它展開。查看 objc-runtime-new.h 源碼以下:
1 |
struct class_data_bits_t { |
注意 objc_class
的 data
方法直接將 class_data_bits_t
的data
方法返回,最終是返回 class_rw_t
,保了好幾層。
能夠看到 class_data_bits_t
裏又包了一個 bits
,這個指針跟不一樣的 FAST_
前綴的 flag 掩碼作按位與操做,就能夠獲取不一樣的數據。bits
在內存中每一個位的含義有三種排列順序:
32 位:
0 | 1 | 2 - 31 |
---|---|---|
FAST_IS_SWIFT | FAST_HAS_DEFAULT_RR | FAST_DATA_MASK |
64 位兼容版:
0 | 1 | 2 | 3 - 46 | 47 - 63 |
---|---|---|---|---|
FAST_IS_SWIFT | FAST_HAS_DEFAULT_RR | FAST_REQUIRES_RAW_ISA | FAST_DATA_MASK | 空閒 |
64 位不兼容版:
0 | 1 | 2 | 3 - 46 | 47 |
---|---|---|---|---|
FAST_IS_SWIFT | FAST_REQUIRES_RAW_ISA | FAST_HAS_CXX_DTOR | FAST_DATA_MASK | FAST_HAS_CXX_CTOR |
48 | 49 | 50 | 51 | 52 - 63 |
FAST_HAS_DEFAULT_AWZ | FAST_HAS_DEFAULT_RR | FAST_ALLOC | FAST_SHIFTED_SIZE_SHIFT | 空閒 |
其中 64 位不兼容版每一個宏對應的含義以下:
1 |
// class is a Swift class |
這裏面除了 FAST_DATA_MASK
是用一段空間存儲數據外,其餘宏都是隻用 1 bit 存儲 bool 值。class_data_bits_t
提供了三個方法用於位操做:getBit
,setBits
和 clearBits
,對應到存儲 bool 值的掩碼也有封裝函數,好比:
1 |
bool isSwift() { |
重頭戲在於最大的那塊存儲區域–FAST_DATA_MASK
,它其實就存儲了指向 class_rw_t
的指針:
1 |
class_rw_t* data() { |
對這片內存讀寫處於併發環境,但並不須要加鎖,由於會經過對一些狀態(realization or construction)判斷來決定是否可讀寫。
class_data_bits_t
甚至還包含了一些對 class_rw_t
中 flags
成員存取的封裝函數。
class_ro_t
objc_class
包含了 class_data_bits_t
,class_data_bits_t
存儲了 class_rw_t
的指針,而 class_rw_t
結構體又包含 class_ro_t
的指針。
class_ro_t
中的 method_list_t
, ivar_list_t
, property_list_t
結構體都繼承自 entsize_list_tt<Element, List, FlagMask>
。結構爲 xxx_list_t
的列表元素結構爲 xxx_t
,命名很工整。protocol_list_t
與前三個不一樣,它存儲的是 protocol_t *
指針列表,實現比較簡單。
entsize_list_tt
實現了 non-fragile 特性的數組結構。假如蘋果在新版本的 SDK 中向 NSObject
類增長了一些內容,NSObject
的佔據的內存區域會擴大,開發者之前編譯出的二進制中的子類就會與新的 NSObject
內存有重疊部分。因而在編譯期會給 instanceStart
和 instanceSize
賦值,肯定好編譯時每一個類的所佔內存區域起始偏移量和大小,這樣只需將子類與基類的這兩個變量做對比便可知道子類是否與基類有重疊,若是有,也可知道子類須要挪多少偏移量。更多細節能夠參考後面的章節 Non Fragile ivars。
1 |
struct class_ro_t { |
class_ro_t->flags
存儲了不少在編譯時期就肯定的類的信息,也是 ABI 的一部分。下面這些 RO_
前綴的宏標記了 flags
一些位置的含義。其中後三個並不須要被編譯器賦值,是預留給運行時加載和初始化類的標誌位,涉及到與 class_rw_t
的類型強轉。運行時會用到它作判斷,後面會講解。
1 |
|
class_rw_t
class_rw_t
提供了運行時對類拓展的能力,而 class_ro_t
存儲的大可能是類在編譯時就已經肯定的信息。兩者都存有類的方法、屬性(成員變量)、協議等信息,不過存儲它們的列表實現方式不一樣。
class_rw_t
中使用的 method_array_t
, property_array_t
, protocol_array_t
都繼承自 list_array_tt<Element, List>
, 它能夠不斷擴張,由於它能夠存儲 list 指針,內容有三種:
- 空
- 一個
entsize_list_tt
指針 entsize_list_tt
指針數組
class_rw_t
的內容是能夠在運行時被動態修改的,能夠說運行時對類的拓展大都是存儲在這裏的。
1 |
struct class_rw_t { |
class_rw_t->flags
存儲的值並非編輯器設置的,其中有些值可能未來會做爲 ABI 的一部分。下面這些 RW_
前綴的宏標記了 flags
一些位置的含義。這些 bool 值標記了類的一些狀態,涉及到聲明週期和內存管理。有些位目前甚至還空着。
1 |
|
demangledName
是計算機語言用於解決實體名稱惟一性的一種方法,作法是向名稱中添加一些類型信息,用於從編譯器中向連接器傳遞更多語義信息。
realizeClass
在某個類初始化以前,objc_class->data()
返回的指針指向的實際上是個 class_ro_t
結構體。等到 static Class realizeClass(Class cls)
靜態方法在類第一次初始化時被調用,它會開闢 class_rw_t
的空間,並將 class_ro_t
指針賦值給 class_rw_t->ro
。這種偷天換日的行爲是靠 RO_FUTURE
標誌位來記錄的:
1 |
ro = (const class_ro_t *)cls->data(); |
注意以前 RO 和 RW flags 宏標記的一個細節:
1 |
|
也就是說 ro = (const class_ro_t *)cls->data();
這種強轉對於接下來的 ro->flags & RO_FUTURE
操做徹底是 OK 的,兩種結構體第一個成員都是 flags
,RO_FUTURE
與 RW_FUTURE
值同樣的。
通過 realizeClass
函數處理的類纔是『真正的』類,調用它時不能對類作寫操做。
Category
Category
爲現有的類提供了拓展性,它是 category_t
結構體的指針。
1 |
typedef struct category_t *Category; |
category_t
存儲了類別中能夠拓展的實例方法、類方法、協議、實例屬性和類屬性。類屬性是 Objective-C 2016 年新增的特性,沾 Swift 的光。因此 category_t
中有些成員變量是爲了兼容 Swift 的特性,Objective-C 暫沒提供接口,僅作了底層數據結構上的兼容。
1 |
struct category_t { |
在 App 啓動加載鏡像文件時,會在 _read_images
函數間接調用到 attachCategories
函數,完成向類中添加 Category
的工做。原理就是向 class_rw_t
中的 method_array_t
, property_array_t
, protocol_array_t
數組中分別添加 method_list_t
, property_list_t
, protocol_list_t
指針。以前講過 xxx_array_t
能夠存儲對應 xxx_list_t
的指針數組。
在調用 attachCategories
函數以前,會先使用 unattachedCategoriesForClass
函數獲取類中還未添加的類別列表。這個列表類型爲 locstamped_category_list_t
,它封裝了 category_t
以及對應的 header_info
。header_info
存儲了實體在鏡像中的加載和初始化狀態,以及一些偏移量,在加載 Mach-O 文件相關函數中常常用到。
1 |
struct locstamped_category_t { |
因此更具體來講 attachCategories
作的就是將 locstamped_category_list_t.list
列表中每一個 locstamped_category_t.cat
中的那方法、協議和屬性分別添加到類的 class_rw_t
對應列表中。header_info
中的信息決定了是不是元類,從而選擇應該是添加實例方法仍是類方法、實例屬性仍是類屬性等。源碼在 objc-runtime-new.mm 文件中,很好理解。
Method
Method
是一種表明類中的某個方法的類型。
1 |
typedef struct method_t *Method; |
而 objc_method
在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現:
1 |
struct method_t { |
- 方法名類型爲
SEL
,前面提到過相同名字的方法即便在不一樣類中定義,它們的方法選擇器也相同。 - 方法類型
types
是個char
指針,其實存儲着方法的參數類型和返回值類型。 imp
指向了方法的實現,本質上是一個函數指針,後面會詳細講到。
Ivar
Ivar
是一種表明類中實例變量的類型。
1 |
typedef struct ivar_t *Ivar; |
而 ivar_t
在上面的成員變量列表中也提到過:
1 |
struct ivar_t { |
能夠根據實例查找其在類中的名字,也就是「反射」:
1 |
-(NSString *)nameWithInstance:(id)instance { |
class_copyIvarList
函數獲取的不只有實例變量,還有屬性。但會在本來的屬性名前加上一個下劃線。
objc_property_t
@property
標記了類中的屬性,這個沒必要多說你們都很熟悉,它是一個指向objc_property
結構體的指針:
1 |
typedef struct property_t *objc_property_t; |
能夠經過 class_copyPropertyList
和 protocol_copyPropertyList
方法來獲取類和協議中的屬性:
1 |
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) |
返回類型爲指向指針的指針,哈哈,由於屬性列表是個數組,每一個元素內容都是一個 objc_property_t
指針,而這兩個函數返回的值是指向這個數組的指針。
舉個栗子,先聲明一個類:
1 |
@interface Lender : NSObject { |
你能夠用下面的代碼獲取屬性列表:
1 |
id LenderClass = objc_getClass("Lender"); |
你能夠用 property_getName
函數來查找屬性名稱:
1 |
const char *property_getName(objc_property_t property) |
你能夠用class_getProperty
和 protocol_getProperty
經過給出的名稱來在類和協議中獲取屬性的引用:
1 |
objc_property_t class_getProperty(Class cls, const char *name) |
你能夠用property_getAttributes
函數來發掘屬性的名稱和@encode
類型字符串:
1 |
const char *property_getAttributes(objc_property_t property) |
把上面的代碼放一塊兒,你就能從一個類中獲取它的屬性啦:
1 |
id LenderClass = objc_getClass("Lender"); |
對比下 class_copyIvarList
函數,使用 class_copyPropertyList
函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。
protocol_t
雖然 Objective-C 的 Category
和 protocol
拓展能力有限,但也得爲了將就 Swift 的感覺,充個胖子。
flags
32 位指針最後兩位是給加載 Mach-O 的 fix-up 階段使用的,前 16 位預留給 Swift 用的。
protocol
主要內容實際上是(可選)方法,其次就是繼承其餘 protocol
。Swift 還支持 protocol
多繼承,因此須要 protocols
數組來作兼容。
1 |
struct protocol_t : objc_object { |
IMP
IMP
在objc.h
中的定義是:
1 |
typedef void (*IMP)(void /* id, SEL, ... */ ); |
它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息以後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP
這個函數指針就指向了這個方法的實現。既然獲得了執行某個實例某個方法的入口,咱們就能夠繞開消息傳遞階段,直接執行方法,這在後面會提到。
你會發現 IMP
指向的方法與 objc_msgSend
函數類型相同,參數都包含 id
和 SEL
類型。每一個方法名都對應一個 SEL
類型的方法選擇器,而每一個實例對象中的 SEL
對應的方法實現確定是惟一的,經過一組 id
和 SEL
參數就能肯定惟一的方法實現地址;反之亦然。
消息
前面作了這麼多鋪墊,如今終於說到了消息了。Objc 中發送消息是用中括號([]
)把接收者和消息括起來,而直到運行時纔會把消息與方法實現綁定。
有關消息發送和消息轉發機制的原理,能夠查看這篇文章。
objc_msgSend 函數
在引言中已經對objc_msgSend
進行了一點介紹,看起來像是objc_msgSend
返回了數據,其實objc_msgSend
從不返回數據而是你的方法被調用後返回了數據。下面詳細敘述下消息發送步驟:
- 檢測這個
selector
是否是要忽略的。好比 Mac OS X 開發,有了垃圾回收就不理會retain
,release
這些函數了。 - 檢測這個 target 是否是
nil
對象。ObjC 的特性是容許對一個nil
對象執行任何一個方法不會 Crash,由於會被忽略掉。 - 若是上面兩個都過了,那就開始查找這個類的
IMP
,先從cache
裏面找,完了找獲得就跳到對應的函數去執行。 - 若是
cache
找不到就找一下方法分發表。 - 若是分發表找不到就到超類的分發表去找,一直找,直到找到
NSObject
類爲止。 - 若是還找不到就要開始進入動態方法解析了,後面會提到。
PS:這裏說的分發表其實就是Class
中的方法列表,它將方法選擇器和方法實現地址聯繫起來。
其實編譯器會根據狀況在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四個方法中選擇一個來調用。若是消息是傳遞給超類,那麼會調用名字帶有」Super」的函數;若是消息返回值是數據結構而不是簡單值時,那麼會調用名字帶有」stret」的函數。排列組合正好四個方法。
值得一提的是在 i386 平臺處理返回類型爲浮點數的消息時,須要用到objc_msgSend_fpret
函數來進行處理,這是由於返回類型爲浮點數的函數對應的 ABI(Application Binary Interface) 與返回整型的函數的 ABI 不兼容。此時objc_msgSend
再也不適用,因而objc_msgSend_fpret
被派上用場,它會對浮點數寄存器作特殊處理。不過在 PPC 或 PPC64 平臺是不須要麻煩它的。
PS:有木有發現這些函數的命名規律哦?帶「Super」的是消息傳遞給超類;「stret」可分爲「st」+「ret」兩部分,分別表明「struct」和「return」;「fpret」就是「fp」+「ret」,分別表明「floating-point」和「return」。
方法中的隱藏參數
咱們常常在方法中使用self
關鍵字來引用實例自己,但從沒有想過爲何self
就能取到調用當前方法的對象吧。其實self
的內容是在方法運行時被偷偷的動態傳入的。
當objc_msgSend
找到方法對應的實現時,它將直接調用該方法實現,並將消息中全部的參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的參數:
- 接收消息的對象(也就是
self
指向的內容) - 方法選擇器(
_cmd
指向的內容)
之因此說它們是隱藏的是由於在源代碼方法的定義中並無聲明這兩個參數。它們是在代碼被編譯時被插入實現中的。儘管這些參數沒有被明確聲明,在源代碼中咱們仍然能夠引用它們。在下面的例子中,self
引用了接收者對象,而_cmd
引用了方法自己的選擇器:
1 |
- strange |
在這兩個參數中,self
更有用。實際上,它是在方法實現中訪問消息接收者對象的實例變量的途徑。
而當方法中的super
關鍵字接收到消息時,編譯器會建立一個objc_super
結構體:
1 |
struct objc_super { id receiver; Class class; }; |
這個結構體指明瞭消息應該被傳遞給特定超類的定義。但receiver
仍然是self
自己,這點須要注意,由於當咱們想經過[super class]
獲取超類時,編譯器只是將指向self
的id
指針和class
的SEL傳遞給了objc_msgSendSuper
函數,由於只有在NSObject
類才能找到class
方法,而後class
方法調用object_getClass()
,接着調用objc_msgSend(objc_super->receiver, @selector(class))
,傳入的第一個參數是指向self
的id
指針,與調用[self class]
相同,因此咱們獲得的永遠都是self
的類型。
獲取方法地址
在IMP
那節提到過能夠避開消息綁定而直接獲取方法的地址並調用方法。這種作法不多用,除非是須要持續大量重複調用某方法的極端狀況,避開消息發送氾濫而直接調用該方法會更高效。
NSObject
類中有個methodForSelector:
實例方法,你能夠用它來獲取某個方法選擇器對應的IMP
,舉個栗子:
1 |
void (*setter)(id, SEL, BOOL); |
當方法被當作函數調用時,上節提到的兩個隱藏參數就須要咱們明確給出了。上面的例子調用了1000次函數,你能夠試試直接給target
發送1000次setFilled:
消息會花多久。
PS:methodForSelector:
方法是由 Cocoa 的 Runtime 系統提供的,而不是 Objc 自身的特性。
動態方法解析
你能夠動態地提供一個方法的實現。例如咱們能夠用@dynamic
關鍵字在類的實現文件中修飾一個屬性:
1 |
@dynamic propertyName; |
這代表咱們會爲這個屬性動態提供存取方法,也就是說編譯器不會再默認爲咱們生成setPropertyName:
和propertyName
方法,而須要咱們動態提供。咱們能夠經過分別重載resolveInstanceMethod:
和resolveClassMethod:
方法分別添加實例方法實現和類方法實現。由於當 Runtime 系統在Cache
和方法分發表中(包括超類)找不到要執行的方法時,Runtime會調用resolveInstanceMethod:
或resolveClassMethod:
來給程序員一次動態添加方法實現的機會。咱們須要用class_addMethod
函數完成向特定類添加特定方法實現的操做:
1 |
void dynamicMethodIMP(id self, SEL _cmd) { |
上面的例子爲resolveThisMethodDynamically
方法添加了實現內容,也就是dynamicMethodIMP
方法中的代碼。其中 「v@:
」 表示返回值和參數,這個符號涉及 Type Encoding
PS:動態方法解析會在消息轉發機制浸入前執行。若是 respondsToSelector:
或 instancesRespondToSelector:
方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP
的機會。若是你想讓該方法選擇器被傳送到轉發機制,那麼就讓resolveInstanceMethod:
返回NO
。
評論區有人問如何用 resolveClassMethod:
解析類方法,我將他貼出有問題的代碼作了糾正和優化後以下,能夠順便將實例方法和類方法的動態方法解析對比下:
頭文件:
1 |
|
m 文件:
1 |
|
須要深入理解 [self class]
與 object_getClass(self)
甚至 object_getClass([self class])
的關係,其實並不難,重點在於 self
的類型:
- 當
self
爲實例對象時,[self class]
與object_getClass(self)
等價,由於前者會調用後者。object_getClass([self class])
獲得元類。 - 當
self
爲類對象時,[self class]
返回值爲自身,仍是self
。object_getClass(self)
與object_getClass([self class])
等價。
凡是涉及到類方法時,必定要弄清楚元類、selector、IMP 等概念,這樣才能作到觸類旁通,隨機應變。
消息轉發
重定向
在消息轉發機制執行前,Runtime 系統會再給咱們一次偷樑換柱的機會,即經過重載- (id)forwardingTargetForSelector:(SEL)aSelector
方法替換消息的接受者爲其餘對象:
1 |
- (id)forwardingTargetForSelector:(SEL)aSelector |
畢竟消息轉發要耗費更多時間,抓住此次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回 若是此方法返回nil或self,則會進入消息轉發機制(self
,由於那樣會死循環。forwardInvocation:
);不然將向返回的對象從新發送消息。
若是想替換類方法的接受者,須要覆寫 + (id)forwardingTargetForSelector:(SEL)aSelector
方法,並返回類對象:
1 |
+ (id)forwardingTargetForSelector:(SEL)aSelector { |
轉發
當動態方法解析不做處理返回NO
時,消息轉發機制會被觸發。在這時forwardInvocation:
方法會被執行,咱們能夠重寫這個方法來定義咱們的轉發邏輯:
1 |
- (void)forwardInvocation:(NSInvocation *)anInvocation |
該消息的惟一參數是個NSInvocation
類型的對象——該對象封裝了原始的消息和消息的參數。咱們能夠實現forwardInvocation:
方法來對不能處理的消息作一些默認的處理,也能夠將消息轉發給其餘對象來處理,而不拋出錯誤。
這裏須要注意的是參數anInvocation
是從哪的來的呢?其實在forwardInvocation:
消息發送前,Runtime系統會向對象發送methodSignatureForSelector:
消息,並取到返回的方法簽名用於生成NSInvocation
對象。因此咱們在重寫forwardInvocation:
的同時也要重寫methodSignatureForSelector:
方法,不然會拋異常。
當一個對象因爲沒有相應的方法實現而沒法響應某消息時,運行時系統將經過forwardInvocation:
消息通知該對象。每一個對象都從NSObject
類中繼承了forwardInvocation:
方法。然而,NSObject
中的方法實現只是簡單地調用了doesNotRecognizeSelector:
。經過實現咱們本身的forwardInvocation:
方法,咱們能夠在該方法實現中將消息轉發給其它對象。
forwardInvocation:
方法就像一個不能識別的消息的分發中心,將這些消息轉發給不一樣接收對象。或者它也能夠象一個運輸站將全部的消息都發送給同一個接收對象。它能夠將一個消息翻譯成另一個消息,或者簡單的」吃掉「某些消息,所以沒有響應也沒有錯誤。forwardInvocation:
方法也能夠對不一樣的消息提供一樣的響應,這一切都取決於方法的具體實現。該方法所提供是將不一樣的對象連接到消息鏈的能力。
注意: forwardInvocation:
方法只有在消息接收對象中沒法正常響應消息時纔會被調用。 因此,若是咱們但願一個對象將negotiate
消息轉發給其它對象,則這個對象不能有negotiate
方法。不然,forwardInvocation:
將不可能會被調用。
轉發和多繼承
轉發和繼承類似,能夠用於爲Objc編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發出去,就好似它把另外一個對象中的方法借過來或是「繼承」過來同樣。
這使得不一樣繼承體系分支下的兩個類能夠「繼承」對方的方法,在上圖中Warrior
和Diplomat
沒有繼承關係,可是Warrior
將negotiate
消息轉發給了Diplomat
後,就好似Diplomat
是Warrior
的超類同樣。
消息轉發彌補了 Objc 不支持多繼承的性質,也避免了由於多繼承致使單個類變得臃腫複雜。它將問題分解得很細,只針對想要借鑑的方法才轉發,並且轉發機制是透明的。
替代者對象(Surrogate Objects)
轉發不只能模擬多繼承,也能使輕量級對象表明重量級對象。弱小的女人背後是強大的男人,畢竟女人遇到難題都把它們轉發給男人來作了。這裏有一些適用案例,能夠參看官方文檔。
轉發與繼承
儘管轉發很像繼承,可是NSObject
類不會將二者混淆。像respondsToSelector:
和 isKindOfClass:
這類方法只會考慮繼承體系,不會考慮轉發鏈。好比上圖中一個Warrior
對象若是被問到是否能響應negotiate
消息:
1 |
if ( [aWarrior respondsToSelector: |
結果是NO
,儘管它可以接受negotiate
消息而不報錯,由於它靠轉發消息給Diplomat
類來響應消息。
若是你爲了某些意圖偏要「弄虛做假」讓別人覺得Warrior
繼承到了Diplomat
的negotiate
方法,你得從新實現 respondsToSelector:
和 isKindOfClass:
來加入你的轉發算法:
1 |
- (BOOL)respondsToSelector:(SEL)aSelector |
除了respondsToSelector:
和 isKindOfClass:
以外,instancesRespondToSelector:
中也應該寫一份轉發算法。若是使用了協議,conformsToProtocol:
一樣也要加入到這一行列中。相似地,若是一個對象轉發它接受的任何遠程消息,它得給出一個methodSignatureForSelector:
來返回準確的方法描述,這個方法會最終響應被轉發的消息。好比一個對象能給它的替代者對象轉發消息,它須要像下面這樣實現methodSignatureForSelector:
:
1 |
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector |
健壯的實例變量 (Non Fragile ivars)
在 Runtime 的現行版本中,最大的特色就是健壯的實例變量。當一個類被編譯時,實例變量的佈局也就造成了,它代表訪問類的實例變量的位置。從對象頭部開始,實例變量依次根據本身所佔空間而產生位移:
上圖左邊是NSObject
類的實例變量佈局,右邊是咱們寫的類的佈局,也就是在超類後面加上咱們本身類的實例變量,看起來不錯。但試想若是哪天蘋果更新了NSObject
類,發佈新版本的系統的話,那就悲劇了:
咱們自定義的類被劃了兩道線,那是由於那塊區域跟超類重疊了。惟有蘋果將超類改成之前的佈局才能拯救咱們,但這樣也致使它們不能再拓展它們的框架了,由於成員變量佈局被死死地固定了。在脆弱的實例變量(Fragile ivars) 環境下咱們須要從新編譯繼承自 Apple 的類來恢復兼容性。那麼在健壯的實例變量下會發生什麼呢?
在健壯的實例變量下編譯器生成的實例變量佈局跟之前同樣,可是當 runtime 系統檢測到與超類有部分重疊時它會調整你新添加的實例變量的位移,那樣你在子類中新添加的成員就被保護起來了。
須要注意的是在健壯的實例變量下,不要使用sizeof(SomeClass)
,而是用class_getInstanceSize([SomeClass class])
代替;也不要使用offsetof(SomeClass, SomeIvar)
,而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))
來代替。
優化 App 的啓動時間 講過加載 Mach-O 文件時有個步驟是經過 fix-up 修改偏移量來解決 fragile base class。
Objective-C Associated Objects
在 OS X 10.6 以後,Runtime系統讓Objc支持向對象動態添加變量。涉及到的函數有如下三個:
1 |
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy ); |
這些方法以鍵值對的形式動態地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量:
1 |
enum { |
這些常量對應着引用關聯值的政策,也就是 Objc 內存管理的引用計數機制。有關 Objective-C 引用計數機制的原理,能夠查看這篇文章。
Method Swizzling
以前所說的消息轉發雖然功能強大,但須要咱們瞭解而且能更改對應類的源代碼,由於咱們須要實現本身的轉發邏輯。當咱們沒法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現時,該怎麼辦呢?可能繼承類並重寫方法是一種想法,可是有時沒法達到目的。這裏介紹的是 Method Swizzling ,它經過從新映射方法對應的實現來達到「偷天換日」的目的。跟消息轉發相比,Method Swizzling 的作法更爲隱蔽,甚至有些冒險,也增大了debug的難度。
PS: 對於熟練使用 Method Swizzling 的開發者,能夠跳過此章節,看看我另外一篇『稍微深刻』一點的文章 Objective-C Method Swizzling。
這裏摘抄一個 NSHipster 的例子:
1 |
|
上面的代碼經過添加一個Tracking
類別到UIViewController
類中,將UIViewController
類的viewWillAppear:
方法和Tracking
類別中xxx_viewWillAppear:
方法的實現相互調換。Swizzling 應該在+load
方法中實現,由於+load
是在一個類最開始加載時調用。dispatch_once
是GCD中的一個方法,它保證了代碼塊只執行一次,並讓其爲一個原子操做,線程安全是很重要的。
若是類中不存在要替換的方法,那就先用class_addMethod
和class_replaceMethod
函數添加和替換兩個方法的實現;若是類中已經有了想要替換的方法,那麼就調用method_exchangeImplementations
函數交換了兩個方法的 IMP
,這是蘋果提供給咱們用於實現 Method Swizzling 的便捷方法。
可能有人注意到了這行:
1 |
// When swizzling a class method, use the following: |
object_getClass((id)self)
與 [self class]
返回的結果類型都是 Class
,但前者爲元類,後者爲其自己,由於此時 self
爲 Class
而不是實例.注意 [NSObject class]
與 [object class]
的區別:
1 |
+ (Class)class { |
PS:若是類中沒有想被替換實現的原方法時,class_replaceMethod
至關於直接調用class_addMethod
向類中添加該方法的實現;不然調用method_setImplementation
方法,types
參數會被忽略。method_exchangeImplementations
方法作的事情與以下的原子操做等價:
1 |
IMP imp1 = method_getImplementation(m1); |
最後xxx_viewWillAppear:
方法的定義看似是遞歸調用引起死循環,其實不會的。由於[self xxx_viewWillAppear:animated]
消息會動態找到xxx_viewWillAppear:
方法的實現,而它的實現已經被咱們與viewWillAppear:
方法實現進行了互換,因此這段代碼不只不會死循環,若是你把[self xxx_viewWillAppear:animated]
換成[self viewWillAppear:animated]
反而會引起死循環。
看到有人說+load
方法自己就是線程安全的,由於它在程序剛開始就被調用,不多會碰到併發問題,因而 stackoverflow 上也有大神給出了另外一個 Method Swizzling 的實現:
1 |
- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 { |
上面的代碼一樣要添加在某個類的類別中,相比第一個種實現,只是去掉了dispatch_once
部分。
Method Swizzling 的確是一個值得深刻研究的話題,找了幾篇不錯的資源推薦給你們:
- Objective-C的hook方案(一): Method Swizzling
- Method Swizzling
- How do I implement method swizzling?
- What are the Dangers of Method Swizzling in Objective C?
- JRSwizzle
在用 SpriteKit 寫遊戲的時候,由於 API 自己有一些缺陷(增刪節點時不考慮父節點是否存在啊,很容易崩潰啊有木有!),我在 Swift 上使用 Method Swizzling彌補這個缺陷:
1 |
extension SKNode { |
而後其餘地方調用那兩個類方法:
1 |
SKNode.yxy_swizzleAddChild() |
由於 Swift 中的 extension 的特殊性,最好在某個類的load()
方法中調用上面的兩個方法.我是在AppDelegate 中調用的,因而保證了應用啓動時可以執行上面兩個方法.
總結
咱們之因此讓本身的類繼承 NSObject
不只僅由於蘋果幫咱們完成了複雜的內存分配問題,更是由於這使得咱們可以用上 Runtime 系統帶來的便利。可能咱們平時寫代碼時可能不多會考慮一句簡單的 [receiver message]
背後發生了什麼,而只是當作方法或函數調用。深刻理解 Runtime 系統的細節更有利於咱們利用消息機制寫出功能更強大的代碼,好比 Method Swizzling 等。
Update 20170820: 使用 objc4-709 源碼重寫部分章節,更新至 Swift 4 代碼示例。
參考連接: