Runtime 又叫運行時,是一套底層的 C 語言 API,其爲 iOS 內部的核心之一,咱們平時編寫的 OC 代碼,底層都是基於它來實現的。好比:css
[receiver message]; // 底層運行時會被編譯器轉化爲: objc_msgSend(receiver, selector) // 若是其還有參數好比: [receiver message:(id)arg...]; // 底層運行時會被編譯器轉化爲: objc_msgSend(receiver, selector, arg1, arg2, ...)
以上你可能看不出它的價值,可是咱們須要瞭解的是 Objective-C 是一門動態語言,它會將一些工做放在代碼運行時才處理而並不是編譯時。也就是說,有不少類和成員變量在咱們編譯的時是不知道的,而在運行時,咱們所編寫的代碼會轉換成完整的肯定的代碼運行。html
所以,編譯器是不夠的,咱們還須要一個運行時系統(Runtime system)來處理編譯後的代碼。算法
Runtime 基本是用 C 和彙編寫的,因而可知蘋果爲了動態系統的高效而作出的努力。蘋果和 GNU 各自維護一個開源的 Runtime 版本,這兩個版本之間都在努力保持一致。編程
點擊這裏下載蘋果維護的開源代碼。數組
Objc 在三種層面上與 Runtime 系統進行交互:緩存
多數狀況咱們只須要編寫 OC 代碼便可,Runtime 系統自動在幕後搞定一切,還記得簡介中若是咱們調用方法,編譯器會將 OC 代碼轉換成運行時代碼,在運行時肯定數據結構和函數。數據結構
Cocoa 程序中絕大部分類都是 NSObject 類的子類,因此都繼承了 NSObject 的行爲。(NSProxy 類時個例外,它是個抽象超類)app
一些狀況下,NSObject 類僅僅定義了完成某件事情的模板,並無提供所須要的代碼。例如 -description
方法,該方法返回類內容的字符串表示,該方法主要用來調試程序。NSObject 類並不知道子類的內容,因此它只是返回類的名字和對象的地址,NSObject 的子類能夠從新實現。框架
還有一些 NSObject 的方法能夠從 Runtime 系統中獲取信息,容許對象進行自我檢查。例如:ide
-class
方法返回對象的類;-isKindOfClass:
和 -isMemberOfClass:
方法檢查對象是否存在於指定的類的繼承體系中(是不是其子類或者父類或者當前類的成員變量);-respondsToSelector:
檢查對象可否響應指定的消息;-conformsToProtocol:
檢查對象是否實現了指定協議類的方法;-methodForSelector:
返回指定方法實現的地址。Runtime 系統是具備公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下,這意味着咱們使用時只須要引入objc/Runtime.h
頭文件便可。
許多函數可讓你使用純 C 代碼來實現 Objc 中一樣的功能。除非是寫一些 Objc 與其餘語言的橋接或是底層的 debug 工做,你在寫 Objc 代碼時通常不會用到這些 C 語言函數。對於公共接口都有哪些,後面會講到。我將會參考蘋果官方的 API 文檔。
要想全面瞭解 Runtime 機制,咱們必須先了解 Runtime 的一些術語,他們都對應着數據結構。
它是selector
在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選擇器,其實做用就和名字同樣,平常生活中,咱們經過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法。selector 對方法名進行包裝,以便找到對應的方法實現。它的數據結構是:
typedef struct objc_selector *SEL;
咱們能夠看出它是個映射到方法的 C 字符串,你能夠經過 Objc 編譯器器命令@selector()
或者 Runtime 系統的 sel_registerName
函數來獲取一個 SEL
類型的方法選擇器。
注意:
不一樣類中相同名字的方法所對應的 selector 是相同的,因爲變量的類型不一樣,因此不會致使它們調用方法實現混亂。
id 是一個參數類型,它是指向某個類的實例的指針。定義以下:
typedef struct objc_object *id; struct objc_object { Class isa; };
以上定義,看到 objc_object
結構體包含一個 isa 指針,根據 isa 指針就能夠找到對象所屬的類。
注意:
isa 指針在代碼運行時並不總指向實例對象所屬的類型,因此不能依靠它來肯定類型,要想肯定類型仍是須要用對象的-class
方法。
PS:KVO 的實現機理就是將被觀察對象的 isa 指針指向一箇中間類而不是真實類型,詳見:KVO章節。
typedef struct objc_class *Class;
Class
實際上是指向 objc_class
結構體的指針。objc_class
的數據結構以下:
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;
從 objc_class
能夠看到,一個運行時類中關聯了它的父類指針、類名、成員變量、方法、緩存以及附屬的協議。
其中 objc_ivar_list
和 objc_method_list
分別是成員變量列表和方法列表:
// 成員變量列表 struct objc_ivar_list { int ivar_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; // 方法列表 struct objc_method_list { struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; }
因而可知,咱們能夠動態修改 *methodList
的值來添加成員方法,這也是 Category 實現的原理,一樣解釋了 Category 不能添加屬性的緣由。這裏能夠參考下美團技術團隊的文章:深刻理解 Objective-C: Category。
objc_ivar_list
結構體用來存儲成員變量的列表,而 objc_ivar
則是存儲了單個成員變量的信息;同理,objc_method_list
結構體存儲着方法數組的列表,而單個方法的信息則由 objc_method
結構體存儲。
值得注意的時,objc_class
中也有一個 isa 指針,這說明 Objc 類自己也是一個對象。爲了處理類和對象的關係,Runtime 庫建立了一種叫作 Meta Class(元類) 的東西,類對象所屬的類就叫作元類。Meta Class 表述了類對象自己所具有的元數據。
咱們所熟悉的類方法,就源自於 Meta Class。咱們能夠理解爲類方法就是類對象的實例方法。每一個類僅有一個類對象,而每一個類對象僅有一個與之相關的元類。
當你發出一個相似 [NSObject alloc](類方法)
的消息時,實際上,這個消息被髮送給了一個類對象(Class Object),這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類(Root Meta Class)的實例。全部元類的 isa 指針最終都指向根元類。
因此當 [NSObject alloc]
這條消息發送給類對象的時候,運行時代碼 objc_msgSend()
會去它元類中查找可以響應消息的方法實現,若是找到了,就會對這個類對象執行方法調用。
上圖實現是 super_class
指針,虛線時 isa
指針。而根元類的父類是 NSObject
,isa
指向了本身。而 NSObject
沒有父類。
最後 objc_class
中還有一個 objc_cache
,緩存,它的做用很重要,後面會提到。
Method 表明類中某個方法的類型
typedef struct objc_method *Method; struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
objc_method
存儲了方法名,方法類型和方法實現:
SEL
method_types
是個 char 指針,存儲方法的參數類型和返回值類型method_imp
指向了方法的實現,本質是一個函數指針Ivar
是表示成員變量的類型。
typedef struct objc_ivar *Ivar; struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
其中 ivar_offset
是基地址偏移字節
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息以後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP
這個函數指針就指向了這個方法的實現。
若是獲得了執行某個實例某個方法的入口,咱們就能夠繞開消息傳遞階段,直接執行方法,這在後面 Cache
中會提到。
你會發現 IMP
指向的方法與 objc_msgSend
函數類型相同,參數都包含 id
和 SEL
類型。每一個方法名都對應一個 SEL
類型的方法選擇器,而每一個實例對象中的 SEL
對應的方法實現確定是惟一的,經過一組 id
和 SEL
參數就能肯定惟一的方法實現地址。
而一個肯定的方法也只有惟一的一組 id
和 SEL
參數。
Cache 定義以下:
typedef struct objc_cache *Cache struct objc_cache { unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method buckets[1] OBJC2_UNAVAILABLE; };
Cache 爲方法調用的性能進行優化,每當實例對象接收到一個消息時,它不會直接在 isa 指針指向的類的方法列表中遍歷查找可以響應的方法,由於每次都要查找效率過低了,而是優先在 Cache 中查找。
Runtime 系統會把被調用的方法存到 Cache 中,若是一個方法被調用,那麼它有可能從此還會被調用,下次查找的時候就會效率更高。就像計算機組成原理中 CPU 繞過主存先訪問 Cache 同樣。
typedef struct objc_property *Property; typedef struct objc_property *objc_property_t;//這個更經常使用
能夠經過class_copyPropertyList
和 protocol_copyPropertyList
方法獲取類和協議中的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
注意:
返回的是屬性列表,列表中每一個元素都是一個objc_property_t
指針
#import <Foundation/Foundation.h> @interface Person : NSObject /** 姓名 */ @property (strong, nonatomic) NSString *name; /** age */ @property (assign, nonatomic) int age; /** weight */ @property (assign, nonatomic) double weight; @end
以上是一個 Person 類,有3個屬性。讓咱們用上述方法獲取類的運行時屬性。
unsigned int outCount = 0; objc_property_t *properties = class_copyPropertyList([Person class], &outCount); NSLog(@"%d", outCount); for (NSInteger i = 0; i < outCount; i++) { NSString *name = @(property_getName(properties[i])); NSString *attributes = @(property_getAttributes(properties[i])); NSLog(@"%@--------%@", name, attributes); }
打印結果以下:
2014-11-10 11:27:28.473 test[2321:451525] 3 2014-11-10 11:27:28.473 test[2321:451525] name--------T@"NSString",&,N,V_name 2014-11-10 11:27:28.473 test[2321:451525] age--------Ti,N,V_age 2014-11-10 11:27:28.474 test[2321:451525] weight--------Td,N,V_weight
property_getName
用來查找屬性的名稱,返回 c 字符串。property_getAttributes
函數挖掘屬性的真實名稱和 @encode
類型,返回 c 字符串。
objc_property_t class_getProperty(Class cls, const char *name) objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
class_getProperty
和 protocol_getProperty
經過給出屬性名在類和協議中得到屬性的引用。
一些 Runtime 術語講完了,接下來就要說到消息了。體會蘋果官方文檔中的 messages aren’t bound to method implementations until Runtime。消息直到運行時纔會與方法實現進行綁定。
這裏要清楚一點,objc_msgSend
方法看清來好像返回了數據,其實objc_msgSend
從不返回數據,而是你的方法在運行時實現被調用後纔會返回數據。下面詳細敘述消息發送的步驟(以下圖):
selector
是否是要忽略。好比 Mac OS X 開發,有了垃圾回收就不理會 retain,release 這些函數。selector
的 target 是否是 nil
,Objc 容許咱們對一個 nil 對象執行任何方法不會 Crash,由於運行時會被忽略掉。IMP
,先從 cache 裏查找,若是找到了就運行對應的函數去執行相應的代碼。在消息的傳遞中,編譯器會根據狀況在 objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, objc_msgSendSuper_stret
這四個方法中選擇一個調用。若是消息是傳遞給父類,那麼會調用名字帶有 Super 的函數,若是消息返回值是數據結構而不是簡單值時,會調用名字帶有 stret 的函數。
疑問:
咱們常常用到關鍵字self
,可是self
是如何獲取當前方法的對象呢?
其實,這也是 Runtime 系統的做用,self
實在方法運行時被動態傳入的。
當 objc_msgSend
找到方法對應實現時,它將直接調用該方法實現,並將消息中全部參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏參數:
self
所指向的內容,當前方法的對象指針)_cmd
指向的內容,當前方法的 SEL 指針)由於在源代碼方法的定義中,咱們並無發現這兩個參數的聲明。它們時在代碼被編譯時被插入方法實現中的。儘管這些參數沒有被明確聲明,在源代碼中咱們仍然能夠引用它們。
這兩個參數中, self
更實用。它是在方法實現中訪問消息接收者對象的實例變量的途徑。
這時咱們可能會想到另外一個關鍵字 super
,實際上 super
關鍵字接收到消息時,編譯器會建立一個 objc_super
結構體:
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
的類型。所以你會發現:
// 這句話並不能獲取父類的類型,只能獲取當前類的類型名 NSLog(@"%@", NSStringFromClass([super class]));
NSObject
類中有一個實例方法:methodForSelector
,你能夠用它來獲取某個方法選擇器對應的 IMP
,舉個例子:
void (*setter)(id, SEL, BOOL); int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES);
當方法被當作函數調用時,兩個隱藏參數也必須明確給出,上面的例子調用了1000次函數,你也能夠嘗試給 target
發送1000次 setFilled:
消息會花多久。
雖然能夠更高效的調用方法,可是這種作法不多用,除非時須要持續大量重複調用某個方法的狀況,纔會選擇使用以避免消息發送氾濫。
注意:
methodForSelector:
方法是由 Runtime 系統提供的,而不是 Objc 自身的特性
你能夠動態提供一個方法實現。若是咱們使用關鍵字 @dynamic
在類的實現文件中修飾一個屬性,代表咱們會爲這個屬性動態提供存取方法,編譯器不會再默認爲咱們生成這個屬性的 setter 和 getter 方法了,須要咱們本身提供。
@dynamic propertyName;
這時,咱們能夠經過分別重載 resolveInstanceMethod:
和 resolveClassMethod:
方法添加實例方法實現和類方法實現。
當 Runtime 系統在 Cache 和類的方法列表(包括父類)中找不到要執行的方法時,Runtime 會調用 resolveInstanceMethod:
或 resolveClassMethod:
來給咱們一次動態添加方法實現的機會。咱們須要用 class_addMethod
函數完成向特定類添加特定方法實現的操做:
void dynamicMethodIMP(id self, SEL _cmd) { // implementation .... } @implementation MyClass + (BOOL)resolveInstanceMethod:(SEL)aSEL { if (aSEL == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSEL]; } @end
上面的例子爲 resolveThisMethodDynamically
方法添加了實現內容,就是 dynamicMethodIMP
方法中的代碼。其中 "v@:"
表示返回值和參數,這個符號表示的含義見:Type Encoding
注意:
動態方法解析會在消息轉發機制侵入前執行,動態方法解析器將會首先給予提供該方法選擇器對應的IMP
的機會。若是你想讓該方法選擇器被傳送到轉發機制,就讓resolveInstanceMethod:
方法返回NO
。
消息轉發機制執行前,Runtime 系統容許咱們替換消息的接收者爲其餘對象。經過 - (id)forwardingTargetForSelector:(SEL)aSelector
方法。
- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(mysteriousMethod:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector]; }
若是此方法返回 nil
或者 self
,則會計入消息轉發機制(forwardInvocation:
),不然將向返回的對象從新發送消息。
當動態方法解析不作處理返回 NO
時,則會觸發消息轉發機制。這時 forwardInvocation:
方法會被執行,咱們能夠重寫這個方法來自定義咱們的轉發邏輯:
- (void)forwardInvocation:(NSInvocation *)anInvocation { if ([someOtherObject respondsToSelector: [anInvocation selector]]) [anInvocation invokeWithTarget:someOtherObject]; else [super forwardInvocation:anInvocation]; }
惟一參數是個 NSInvocation
類型的對象,該對象封裝了原始的消息和消息的參數。咱們能夠實現 forwardInvocation:
方法來對不能處理的消息作一些處理。也能夠將消息轉發給其餘對象處理,而不拋出錯誤。
注意:參數
anInvocation 是從哪來的?
在forwardInvocation:
消息發送前,Runtime 系統會向對象發送methodSignatureForSelector:
消息,並取到返回的方法簽名用於生成 NSInvocation 對象。因此重寫forwardInvocation:
的同時也要重寫methodSignatureForSelector:
方法,不然會拋異常。
當一個對象因爲沒有相應的方法實現而沒法相應某消息時,運行時系統將經過 forwardInvocation:
消息通知該對象。每一個對象都繼承了 forwardInvocation:
方法。可是, NSObject
中的方法實現只是簡單的調用了 doesNotRecognizeSelector:
。經過實現本身的 forwardInvocation:
方法,咱們能夠將消息轉發給其餘對象。
forwardInvocation:
方法就是一個不能識別消息的分發中心,將這些不能識別的消息轉發給不一樣的接收對象,或者轉發給同一個對象,再或者將消息翻譯成另外的消息,亦或者簡單的「吃掉」某些消息,所以沒有響應也不會報錯。這一切都取決於方法的具體實現。
注意:
forwardInvocation:
方法只有在消息接收對象中沒法正常響應消息時纔會被調用。因此,若是咱們嚮往一個對象將一個消息轉發給其餘對象時,要確保這個對象不能有該消息的所對應的方法。不然,forwardInvocation:
將不可能被調用。
轉發和繼承類似,可用於爲 Objc 編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發出去,就好像它把另外一個對象中的方法接過來或者「繼承」過來同樣。
這使得在不一樣繼承體系分支下的兩個類能夠實現「繼承」對方的方法,在上圖中 Warrior
和 Diplomat
沒有繼承關係,可是 Warrior
將 negotiate
消息轉發給了 Diplomat
後,就好似 Diplomat
是 Warrior
的超類同樣。
消息轉發彌補了 Objc 不支持多繼承的性質,也避免了由於多繼承致使單個類變得臃腫複雜。
雖然轉發能夠實現繼承的功能,可是 NSObject
仍是必須表面上很嚴謹,像 respondsToSelector:
和 isKindOfClass:
這類方法只會考慮繼承體系,不會考慮轉發鏈。
若是上圖中的 Warrior
對象被問到是否能響應 negotiate
消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] ) ...
回答固然是 NO
, 儘管它能接受 negotiate
消息而不報錯,由於它靠轉發消息給 Diplomat
類響應消息。
若是你就是想要讓別人覺得 Warrior
繼承到了 Diplomat
的 negotiate
方法,你得從新實現 respondsToSelector:
和 isKindOfClass:
來加入你的轉發算法:
- (BOOL)respondsToSelector:(SEL)aSelector { if ( [super respondsToSelector:aSelector] ) return YES; else { /* Here, test whether the aSelector message can * * be forwarded to another object and whether that * * object can respond to it. Return YES if it can. */ } return NO; }
除了 respondsToSelector:
和 isKindOfClass:
以外,instancesRespondToSelector:
中也應該寫一份轉發算法。若是使用了協議,conformsToProtocol:
一樣也要加入到這一行列中。
若是一個對象想要轉發它接受的任何遠程消息,它得給出一個方法標籤來返回準確的方法描述 methodSignatureForSelector:
,這個方法會最終響應被轉發的消息。從而生成一個肯定的 NSInvocation
對象描述消息和消息參數。這個方法最終響應被轉發的消息。它須要像下面這樣實現:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { NSMethodSignature* signature = [super methodSignatureForSelector:selector]; if (!signature) { signature = [surrogate methodSignatureForSelector:selector]; } return signature; }
在 Runtime 的現行版本中,最大的特色就是健壯的實例變量了。當一個類被編譯時,實例變量的內存佈局就造成了,它代表訪問類的實例變量的位置。實例變量一次根據本身所佔空間而產生位移:
上圖左是 NSObject
類的實例變量佈局。右邊是咱們寫的類的佈局。這樣子有一個很大的缺陷,就是缺少拓展性。哪天蘋果更新了 NSObject
類的話,就會出現問題:
咱們自定義的類的區域和父類的區域重疊了。只有蘋果將父類改成之前的佈局才能拯救咱們,但這樣致使它們不能再拓展它們的框架了,由於成員變量佈局被固定住了。在脆弱的實例變量(Fragile ivar)環境下,須要咱們從新編譯繼承自 Apple 的類來恢復兼容。若是是健壯的實例變量的話,以下圖:
在健壯的實例變量下,編譯器生成的實例變量佈局跟之前同樣,可是當 Runtime 系統檢測到與父類有部分重疊時它會調整你新添加的實例變量的位移,那樣你再子類中新添加的成員變量就被保護起來了。
注意:
在健壯的實例變量下,不要使用siof(SomeClass)
,而是用class_getInstanceSize([SomeClass class])
代替;也不要使用offsetof(SomeClass, SomeIvar)
,而要使用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))
來代替。
咱們讓本身的類繼承自 NSObject
不只僅是由於基類有不少複雜的內存分配問題,更是由於這使得咱們能夠享受到 Runtime 系統帶來的便利。
雖然平時咱們不多會考慮一句簡單的調用方法,發送消息底層所作的複雜的操做,但深刻理解 Runtime 系統的細節使得咱們能夠利用消息機制寫出功能更強大的代碼。
我的以爲其實日常開發用不到Runtime,用Runtime純屬爲了裝一把,可是用戶是看不到你的代碼的!
若是學會Runtime不是爲了裝逼,那將毫無心義!
另外.....
個人願望是.......
世界和平.........