Objective-C語言總是盡可能地將工作從編譯鏈接時推遲到運行時。只要有可能,Objective-C總是使用動態的方式來解決問題。這意味著Objective-C語言不僅需要一個編譯器,同時也需要一個運行時系統來執行編譯好的代碼。Runtime扮演的角色類似于Objective-C語言的操作系統,Objective-C基于該系統來工作。
1、與Runtime的交互
Objective-C程序有三種途徑和運行時系統交互:
- 通過Objective-C源代碼;
- 通過Foundation框架中NSObject的方法;
- 通過直接調用Runtime的函數。
1.1、通過Objective-C源代碼
大部分情況下,你只需編寫和編譯Objective-C源代碼,運行時系統在后臺自動運行。
當編譯Objective-C類和方法時,編譯器為實現語言動態特性將自動創建一些數據結構和函數。這些數據結構包含類定義和協議定義的信息(如objc_msgSend函數)。
1.2、通過NSObject的方法
Cocoa中絕大部分類都是NSObject的子類,都繼承了NSObject的方法。
NSObject的某些方法可以從運行時系統中獲取信息,對對象進行一定程度的自我檢查,如class
返回對象的類;isKindOfClass:
和isMemberOfClass:
檢查對象是否在指定的類繼承體系中;respondsToSelector:
檢查對象能否響應指定的消息;conformsToProtocol:
檢查對象是否實現了指定協議類的方法;methodForSelector:
返回指定方法實現的地址。
1.3、通過Runtime的函數
Runtime 系統是一個由一系列函數和數據結構組成,具有公共接口的動態庫,頭文件存放在/usr/include/objc中。這些函數支持用純C的函數來實現 Objective-C 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是我們在寫 Objc 代碼時一般不會直接用到這些函數的。在Objective-C Runtime Reference中有對 Runtime 函數的詳細文檔。
2、Runtime術語
-
SEL
SEL
是映射到方法的C字符串,它不同于C語言中的函數指針,函數指針直接保存了方法的地址,但SEL
只是方法編號。它的數據結構是這樣的:
typedef struct objc_selector *SEL;
可以用 Objc 編譯器命令@selector()
或者 Runtime 系統的sel_registerName
函數來獲得一個SEL
類型的方法選擇器。不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器。
-
id
概括來說,id
是一個指向實例的指針,定義如下:
typedef struct objc_object *id;
objc_object
的定義是:
struct objc_object { Class isa; };
struct objc_class {
Class isa;
#if !__OBJC2__
......
#endif
};
可以看到,id 是指向 objc_object 結構體的指針,而 objc_object 包含一個 Class 的結構體指針 isa。
不過isa
指針不總是指向實例對象所屬的類,不能依靠它來確定類型,而應該用class
方法來確定實例對象的類。因為KVO的實現原理就是將被觀察對象的isa
指針指向一個動態創建的中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文檔。
-
Class
Class
是一個指向objc_class
結構體的指針:
typedef struct objc_class *Class;
在 runtime.h 中可以看到
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
結構體就是 Objective-C 的對象系統的基石,其中第一個字段isa
是objc_class
結構體指針,指向該對象所屬的類型對象。實際上,在 Objective-C 中,類本身也是一個對象,而一個類的 isa 指針指向它的元類(Meta Class)。元類是一個類對象的類,元類中存儲著類方法。
向一個對象發送消息時,runtime會在這個對象所屬的那個類的方法列表中查找。而向一個類發送消息時,runtime會在這個類的元類的方法列表中查找。每個類都會有一個單獨的元類,因為每個類的類方法基本不可能完全相同。
那么元類是什么?
和類一樣,元類也是一個對象,它也有一個 isa 指針指向其所屬的類。所有的元類都使用 NSObject 的元類作為它們的所屬類。NSObject 的元類是它自己。
與類一樣,元類也有自己的父類。meta class 的 super class 是 super class 的 meta class。直到基類的 meta class,它的 super class 指向基類自身。關系如下:
-
Method
Method
是一種代表類中某個方法的類型。
typedef struct objc_method *Method;
objc_method
儲了方法名,方法類型和方法實現:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
方法名類型為SEL
,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
方法類型method_types
是個 char 指針,其實存儲著方法的參數類型和返回值類型。
method_imp
指向了方法的實現,本質上是一個函數指針,后面會詳細講到。
-
Ivar
Ivar
是一種代表類中實例變量的類型。
typedef struct objc_ivar *Ivar;
可以根據實例查找其在類中的名字,也就是“反射”:
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
class_copyIvarList
函數獲取的不僅有實例變量,還有屬性。但會在原本的屬性名前加上一個下劃線。
-
IMP
IMP
是一個函數指針,它的定義是:
typedef id (*IMP)(id, SEL, ...);
當你發起一個 ObjC 消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在后面會提到。
-
Cache
在 runtime.h 中Cache
的定義如下:
typedef struct objc_cache *Cache;
objc_class
結構體中有一個struct objc_cache *cache
,它到底是緩存啥的呢?
Cache
為方法調用的性能進行優化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa
指向的類的方法列表中遍歷查找能夠響應消息的方法,因為這樣效率太低了,而是優先在Cache
中查找。Runtime 系統會把被調用的方法存到Cache
中(理論上講一個方法如果被調用,那么它有可能今后還會被調用),下次查找的時候效率更高。
-
Property
@property
標記了類中的屬性,它是一個指向objc_property
結構體的指針:
typedef struct objc_property *Property;
可以通過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
指針,而這兩個函數返回的值是指向這個數組的指針。
相對于class_copyIvarList
函數,使用class_copyPropertyList
函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。
你可以用property_getAttributes
函數來發掘屬性的名稱和@encode類型字符串:
- property_getAttributes 返回的字符串以字母 T 開始,接著是@encode 編碼和逗號。
- 如果屬性有 readonly 修飾,則字符串中含有 R 和逗號。
- 如果屬性有 copy 或者 retain 修飾,則字符串分別含有 C 或者&,然后是逗號。
- 如果屬性定義有定制的 getter 和 setter 方法,則字符串中有 G 或者 S 跟著相應的方法名以及逗號(例如,GcustomGetter,ScustomSetter:,,)。
- 如果屬性是只讀的,且有定制的 get 訪問方法,則描述到此為止。
- 字符串以 V 然后是屬性的名字結束。
3、消息
本段主要描述如何將發消息轉換為objc_msgSend函數調用,如何通過名字來指定一個方法,以及如何使用objc_msgSend函數。
3.1、獲得方法地址
使用NSObject類中的methodForSelector:
方法,可以獲得一個指向方法實現的指針,通過該指針可以直接調用方法實現。例:
void (*setter) (id, SEL, BOOL);
// methodForSelector:方法會返回一個函數指針
setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];
for (int i = 0; i < 1000; i ++) {
// 使用函數指針直接調函數,參數一:函數對象,參數二:函數簽名,參數三:函數參數
setter(self, @selector(setFilled:), NO);
}
注意,methodForSelector:
是Runtime提供的功能,不是Objective-C語言本身的功能。
3.2、objc_msgSend函數
Objective-C中,消息是在運行的時候才和方法實現綁定的。[receiver message]
會被編譯器轉換成對objc_msgSend函數的調用。該函數有兩個主要參數:receiver
和selector
,再加上參數的話就是objc_msgSend(receiver, selector, arg1, arg2, ...)
。
消息發送的詳細步驟:
- 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain ,release 這些函數了。
- 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉。
- 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數去執行。
- 如果 cache 找不到就找一下方法分發表。
- 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
- 找到方法實現之后,然后將消息接收者對象及方法中指定的參數傳給找到的方法實現,最后,將方法實現的返回值作為該函數的返回值返回。
- 如果還找不到就要開始進入動態方法解析了,后面會提到。
消息機制的關鍵在于編譯器為類和對象生成的結構,每個類的結構中至少包含兩個基本元素:isa
指針和方法列表。
當對象被創建時,它會被分配內存,并初始化實例變量。對象的第一個實例變量是一個指向該對象的類結構體的指針,即isa
。通過isa
指針可以訪問它對應的類及相應的父類。方法列表存放方法名字和對應的實現地址。
對象接收消息時,objc_msgSend
先根據該對象的isa
指針找到該對象對應的類的方法表,從表中尋找對應的方法。如果找不到,objc_msgSend
將繼續在父類中尋找,直到NSObject類。一旦找到對應方法,objc_msgSend
會以消息接收者對象為參數調用該方法。
為了加快消息的處理過程,運行時系統通常會將使用過的方法放入緩存中。每個類都有一個獨立的緩存,同時包括繼承的方法和在該類中定義的方法。objc_msgSend
在尋找方法時,會優先在緩存中尋找。如果緩存中已經有了需要的方法,則消息僅僅比函數調用慢一點點。
3.3、使用隱藏的參數
我們經常使用self
來表示當前方法的對象,但是為什么它能表示當前方法對象呢?實際上它是在代碼編譯時插入方法中的。
當objc_msgSend找到方法對應的實現時,它會直接調用該方法,并將消息中的參數傳遞給方法實現,同時,它還傳遞兩個隱藏的參數:接收消息的對象(self)和方法選擇器(_cmd)。
在方法中可以通過self
來引用消息接收者對象,通過_cmd
來引用方法本身。
3.4、動態方法解析
我們可以通過resolveInstanceMethod:
和resolveClassMethod:
方法動態地添加實例方法和類方法的實現。當Runtime系統在緩存和方法列表中找不到要執行的方法時,會調用resolveInstanceMethod:
和resolveClassMethod:
方法來給我們一次動態添加方法實現的機會。例如,有如下的函數:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
可以通過resolveInstanceMethod:
將它作為類方法 resolveThisMethodDynamically
的實現:
@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
其中 “v@:” 表示返回值和參數,這個符號涉及 Type Encoding。
動態方法解析會在消息轉發之前前執行。如果 respondsToSelector:
或 instancesRespondToSelector:
方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。如果你實現了resolveInstanceMethod:
方法但是仍然希望正常進行消息轉發,只需要返回NO就可以了。
4、消息轉發
通常,給一個對象發送它不能處理的消息會得到出錯提示,不過,運行時系統在拋出錯誤之前,還有三次機會拯救程序:
- Method Resolution
- Fast Forwarding
- Normal Forwarding
Method Resolution(動態方法解析)
Runtime系統在運行時會先調用resolveInstanceMethod:
或resolveClassMethod:
方法,讓我們添加方法的實現。如果添加方法并返回YES,那系統就會重新啟動一次消息發送的過程。如果沒有實現或返回NO,會執行 Fast Forwarding 操作。
Fast Forwarding(快速轉發)
如果目標對象實現forwardingTargetForSelector:
方法,并且這個方法返回的不是nil或self,也會重啟消息發送的過程,把這消息轉發給指定對象來處理。否則,就會繼續 Normal Fowarding。
Normal Forwarding(“慢速”轉發)
如果沒有使用 Fast Forwarding 來轉發消息,最后只能使用 Normal Forwarding 來進行消息轉發。它會調用methodSignatureForSelector:
方法來獲取函數的參數和返回值,如果返回為nil,程序會Crash掉,并拋出 unrecognized selector sent to instance 異常信息。如果返回一個函數簽名,系統就會創建一個NSInvocation
對象并調用forwardInvocation:
方法進行消息轉發。
4.1、轉發
如果一個對象收到一條無法處理的消息(如動態方法解析返回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:
消息通知該對象。每個對象都從 NSObject 類中繼承了forwardInvocation:
方法。然而NSObject 中的方法實現只是簡單地調用了 doesNotRecognizeSelector:
。通過實現自己的 forwardInvocation:
方法,你可以在該方法實現中將消息轉發給其它對象。
forwardInvocation:
方法就像一個不能識別的消息的分發中心,將這些消息轉發給不同接收對象。或者它也可以象一個運輸站將所有的消息都發送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的“吃掉”某些消息,因此沒有響應也沒有錯誤。forwardInvocation:
方法也可以對不同的消息提供同樣的響應,這一切都取決于方法的具體實現。該方法所提供是將不同的對象鏈接到消息鏈的能力。
注意: forwardInvocation:
方法只有在消息接收對象中無法正常響應消息時才會被調用。 所以,如果你希望你的對象將一個消息轉發給其它對象,你的對象就不能有這個方法。否則,forwardInvocation:
將不會被調用。
4.2、轉發和多重繼承
消息轉發很像繼承,并且可以用來在Objective-C程序中模擬多重繼承。如下圖所示,一個對象通過轉發來響應消息,看起來就像該對象從別的類那借來了或者”繼承“了方法實現一樣。
在上圖中,Warrior 類的一個對象實例將 negotiate 消息轉發給 Diplomat 類的一個實例。看起來,Warrior 類似乎和 Diplomat 類一樣, 響應 negotiate 消息,并且行為和 Diplomat 一樣,實際上是 Diplomat 類響應了該消息。
消息轉發彌補了Objective-C不支持多重繼承的性質,也避免了因為多繼承導致單個類變得臃腫復雜。
4.3、轉發和類繼承
盡管消息轉發很像繼承,但它不是繼承。例如在 NSObject 類中,方法respondsToSelector:
和isKindOfClass:
只會出現在繼承鏈中,而不是消息轉發鏈中。例如,如果向一個 Warrior 類的對象詢問它能否響應 negotiate 消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
返回NO,盡管該對象能夠接收和響應negotiate
方法。
如果你想要讓它看起來真的像是繼承了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:
來返回準確的方法描述,這個方法會最終響應被轉發的消息。比如一個對象能給它的替代者對象轉發消息,它需要像下面這樣實現methodSignatureForSelector:
:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
5、動態屬性關聯
在 OS X 10.6 之后,Runtime系統讓Objc支持向對象動態添加變量。涉及到的函數有以下三個:
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );
這些方法以鍵值對的形式動態地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
這些常量對應著引用關聯值的政策,也就是 Objc 內存管理的引用計數機制。你會發現這里邊沒有 weak 屬性,關于如何關聯 weak 屬性,請參考《如何使用 Runtime 給現有的類添加 weak 屬性》。
6、Method Swizzling
Method Swizzling 就是方法交換,主要有兩種使用場景:hook和面向切面編程。
hook一般在+load
方法中使用:
- (void)replacementReceiveMessage:(id)arg1 {
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}
APP需要進行數據埋點時,就需要面向切面編程了。假如需要統計按鈕點擊的情況,就可以把按鈕點擊的方法進行交換,這樣就可以最大限度地減少代碼修改和入侵。
要注意的是,在+load
中使用 Method Swizzling 是一件很危險的事情,因為它會影響工程中所有相同類的代碼,可能會出現意想不到的Bug。
關于 Method Swizzling 有一個輕量級的庫Aspects很值得閱讀。