Objective-C 是一門動態(tài)語言,相比C語言來說,增加了面向?qū)ο蟮奶匦院拖鬟f機制斑举。消息傳遞機制的基礎就是runtime,也就是常說的運行時機制赐纱。
網(wǎng)上關(guān)于runtime的介紹已經(jīng)非常多了脊奋,因此本文對runtime不對做過多的介紹熬北,而是結(jié)合我個人在項目中遇到的實際問題,介紹一些runtime在項目中的實際應用诚隙。
runtime介紹
在開始說runtime在項目中的實際運用之前讶隐,還是先簡單介紹下runtime。
runtime的核心是消息傳遞久又。對比Objective-C和C/C++, 調(diào)用一個方法/函數(shù)在Objective-C中被稱之為發(fā)送消息巫延。如 [A testMethod],可以翻譯成向A對象發(fā)送了 testMethod的消息地消。為何在Objective-C中炉峰,函數(shù)調(diào)用被稱之為發(fā)消息?以及Objective-C中的發(fā)消息和C/C++ 中的函數(shù)調(diào)用有什么區(qū)別脉执?
我們知道疼阔,C語言是 "靜態(tài)語言", 所謂靜態(tài)語言,指的是一個方法/函數(shù)和內(nèi)存中的一段代碼綁定在一起半夷,程序執(zhí)行時竿开,調(diào)用一個方法,實際上就是直接執(zhí)行對應內(nèi)存中的代碼段玻熙。而且否彩,函數(shù)名和代碼段綁定這個過程在編譯階段就已經(jīng)確定好了。而在Objective-C中嗦随,[object testMethod] 不會立即執(zhí)行 testMethod方法列荔,而是會向 object 發(fā)送一個 testMethod的消息,最終消息的接收的者枚尼,可能是 object對象贴浙,也可能不是object對象,這個過程是在運行時發(fā)生的署恍。也就是說崎溃,在編譯階段,[object testMethod] 程序是不知道 testMethod 消息的最終接收者是誰的盯质,消息的接收者袁串,在運行時才能夠確定。
消息傳遞
上面也提到了呼巷,[object testMethod] 會向object發(fā)送一個testMethod的消息囱修,但是消息最終的接收者不一定是object對象,尋找消息最終接收者的過程實際上就是一個消息傳遞的過程王悍。首先看一下Objective-C中object 和 class 的定義破镰。
在objc.h中可以看到object的定義,如下:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
在runtime.h中可以看到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;
消息傳遞的關(guān)鍵是isa指針以及方法列表,也就是objc_method_list鲜漩。消息傳遞的過程大致如下:
- 首先根據(jù)object對象的isa指針獲取到該對象所屬的類源譬,然后在類對象的方法列表中尋找,是否有對應的方法孕似。如果有瓶佳,則找到,object即為消息最終的接收者鳞青;如果沒找到霸饲,進行第2步。
- 第上一步獲取到的類對象臂拓,根據(jù)super_class指針厚脉,可以獲取到該類的父類,在父類的方法列表中尋找是否有對應的方法胶惰。如果有傻工,則找到返回;如果沒有孵滞,則重復該操作中捆。
- 倘若一直找到基類(NSObject)都沒有找到對應的方法,也就是沒有對象能夠接收該消息坊饶,此時會發(fā)生 unrecognize selector 的崩潰泄伪。在發(fā)生崩潰之前,還有三次機會來挽救:
(1)resolveInstanceMethod: 該方法返回一個bool值匿级,如果返回YES蟋滴,則重新啟動一次消息傳遞的過程,如果返回NO痘绎,進入下一步操作津函。在項目開發(fā)中,如果有未識別的消息孤页,可以在該方法中添加對應的消息尔苦,然后返回YES即可避免崩潰。示例代碼如下:
+(BOOL)resolveInstanceMethod:(SEL)sel
{
BOOL isInstanced = [self resolveInstanceMethod:sel];
if(!isInstanced){
class_addMethod([self class],sel,(IMP)emptyMethod,"v@:");
return YES;
}
return isInstanced;
}
在emptyMethod中不做任何操作即可避免崩潰行施。
(2)forwardingTargetForSelector: 該方法返回一個對象允坚,提供了將消息轉(zhuǎn)發(fā)給其他對象的機會。只要該方法返回的不是nil和self悲龟,消息傳遞的過程會重新啟動屋讶。示例代碼如下:
-(id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(testSelector:)){
return testObject;
}
return [super forwardingTargetForSelector:aSelector];
}
返回testObject之后冰寻,相當于 [testObject testSelector]须教,消息傳遞會重新啟動。
(3) methodSignatureForSelector 和 forwardingInvocation 方法。methodSignatureForSelector 會返回一個方法簽名類轻腺,如果返回的是nil,則直接拋出 unrecognized selector的異常乐疆;否則runtime會根據(jù)返回的方法簽名類創(chuàng)建一個 NSInvocation對象,之后調(diào)用forwardingInvocation贬养。forwardingInvocation所做的工作和forwardingTargetForSelector 類似挤土,也是將消息轉(zhuǎn)發(fā)給其他對象。
至此误算,消息傳遞的過程結(jié)束仰美。下面介紹在項目中使用到runtime的地方。
runtime的實際運用
避免nsnull崩潰
問題背景:在項目開發(fā)中儿礼,由于各種各樣的原因咖杂,服務器接口常常會返回null,而在Objective-C中蚊夫,向null 發(fā)送消息會引發(fā) unrecognized selector的異常诉字,引起app崩潰,產(chǎn)品體驗非常不好知纷。
解決方案:一種解決方案是獲得服務器接口返回的數(shù)據(jù)后壤圃,進行判斷,如果是null琅轧,則賦值一個空字符串伍绳,或者進行相應的處理,避免崩潰乍桂。但是一個完整項目里面使用的接口是非常多的墨叛,而且一個接口返回的字段也是非常多的,倘若對每個接口的每個字段都進行判斷模蜡,一方面會寫大量的重復代碼漠趁,另一方面也破壞了代碼的美觀,且沒有從根本上解決問題忍疾,屬于治標不治本闯传。
另一種方法是使用runtime解決。在Objective-C中卤妒,向nil發(fā)送消息是不會發(fā)生崩潰的甥绿。因此如果向null 發(fā)送了消息,可以通過消息轉(zhuǎn)發(fā)则披,把消息最終的接收者設置為nil即可避免崩潰共缕。通過上面的介紹,可以在最后一步士复,也就是 forwardingInvocation 中將消息的接收者設置為nil图谷,不過 forwardingInvocation 需要配合 methodSignatureForSelector 方法使用翩活。在 methodSignatureForSelector 方法中返回方法簽名類,然后在 forwardingInvocation 進行消息轉(zhuǎn)發(fā)即可便贵。示例代碼如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
@synchronized([self class])
{
//look up method signature
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if (!signature)
{
//not supported by NSNull, search other classes
// 在這里構(gòu)造NSMethodSignature
}
return signature;
}
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
invocation.target = nil;
[invocation invoke];
}
避免數(shù)組越界崩潰
日常開發(fā)中菠镇,經(jīng)常會碰到數(shù)組越界的情況,不幸的是承璃,數(shù)組越界同樣會直接引發(fā)崩潰利耍,造成非常不好的體驗。解決數(shù)組越界的方法很多盔粹,可以在使用的時候提前判斷隘梨,比如說 在使用 [array objectAtIndex: i] 之前,先進行下面類似的判斷
id object = nil;
if(array.count > i - 1)
object = array[i];
可以預見的是舷嗡,項目中用到數(shù)組的地方會非常多出嘹,若每次使用之前都判斷一下,會寫大量重復冗余的代碼咬崔。
另一種方法是寫NSArray税稼、NSMutableArray 的 category,在分類方法中判斷一次就可以。這種方法的一個缺點是需要在項目中所有的調(diào)用都需要調(diào)用分類中的方法垮斯,這點需要和項目中的同事約定好郎仆,否則仍然避免不了數(shù)組越界引起崩潰的問題。
使用 Method Swizzling 可以解決數(shù)組越界的問題兜蠕。Objective-C是動態(tài)語言扰肌,支持在運行時交換兩個方法的實現(xiàn)。利用這一特性熊杨,可以寫一個新的方法曙旭,如 hookObjectAtIndex 替換 objectAtIndex 方法,在hookObjectAtIndex 方法中判斷數(shù)組越界的情況晶府。這樣桂躏,在項目中還是使用NSArray 的 objectAtIndex,但是實際執(zhí)行的是 hookObjectAtIndex方法川陆。示例代碼如下:
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/* 數(shù)組有內(nèi)容obj類型才是__NSArrayI ,NSArray在runtime中對應的是__NSArrayI */
NSArray* obj = [[NSArray alloc] initWithObjects:@0, @1, nil];
[obj swizzleInstanceMethod:@selector(objectAtIndex:) withMethod:@selector(hookObjectAtIndex:)];
[obj release];
});
}
- (id) hookObjectAtIndex:(NSUInteger)index {
if (index < self.count) {
return [self hookObjectAtIndex:index];
}
return nil;
}
兩個注意點:
- Method Swizzling應該寫在 + load 方法中剂习,原因:+ load 方法在類加載時就會被執(zhí)行;
- Method Swizzling 應該總是在 dispatch_once 中執(zhí)行较沪,原因: Method Swizzling 的修改是影響全局的鳞绕,因此只需要執(zhí)行一次就可以了,dispatch_once 可以保證這一點尸曼。
PS:Method Swizzling 的功能非常強大们何,解決NSArray 越界崩潰只是一方面應用。
字典轉(zhuǎn)模型
在項目開發(fā)中控轿,服務器會返回json類型數(shù)據(jù)冤竹,這就需要我們在程序中將字典轉(zhuǎn)為模型以方便使用拂封。字典轉(zhuǎn)模型通常有兩種方法:
- 自己寫字典轉(zhuǎn)模型的過程。示例代碼如下:
- (instancetype)initWithDict:(NSDictionary *)dict
{
if(self = [super init]){
self.url = dict[@"url"];
self.width = dict[@"width"];
self.height = dict[@"height"];
}
return self;
}
這種方法的缺點是:倘若屬性比較多贴见,需要寫較多的代碼烘苹,我們項目中有接口返回將近20個字段躲株,解析起來非常麻煩片部;如果模型增加了新的屬性,需要增加相應的代碼霜定。
- 另一種方法是使用一些第三方庫档悠,如YYModel、MJExtension等望浩,可以幫助我們自動的將字典轉(zhuǎn)為模型辖所,省去了我們自己將屬性和字典值對應的過程,以及增加屬性時磨德,不需要寫新的代碼缘回。這些第三方庫的實現(xiàn)原理使用到了runtime的知識。
通過上面的介紹知道典挑,在Class的數(shù)據(jù)結(jié)構(gòu)中有變量列表(struct objc_ivar_list *ivars)酥宴,可以通過runtime獲取到對象所屬類的變量列表,然后將屬性和字典中的值對應即可轉(zhuǎn)成模型您觉。示例代碼如下:
-(instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)獲取類的屬性及屬性對應的類型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通過property_getName函數(shù)獲得屬性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通過property_getAttributes函數(shù)可以獲得屬性的名字和@encode編碼
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即釋放properties指向的內(nèi)存
free(properties);
//(2)根據(jù)類型給屬性賦值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
KVO的實現(xiàn)
KVO是Objective-C對觀察者模式的一種實現(xiàn)拙寡,通過KVO,當被觀察者對象的某個屬性發(fā)生改變時琳水,觀察者就會收到通知肆糕,并做相應的處理。KVO的實現(xiàn)依賴于runtime在孝。
蘋果通過 isa-swizzling 的方式實現(xiàn)KVO诚啃。通過上面的介紹,我們知道在Object 對象的定義中有一個 isa 指針私沮,指向的是該對象所屬的類绍申,KVO就是通過修改 isa 指針的指向?qū)崿F(xiàn)的。具體過程:
當觀察一個對象A時顾彰,假設該對象所屬的類是TestA极阅,KVO機制動態(tài)的創(chuàng)建了一個新的類,名稱為NSKVONotifying_TestA涨享,在新的類中重寫了所觀察屬性的 setter 方法筋搏。新的setter方法會在調(diào)用原setter方法之前和之后通知觀察者,觀察者會做出相應的處理厕隧。在新建完NSKVONotifying_TestA類后奔脐,KVO機制會修改對象的 isa 指針俄周,指向 NSKVONotifying_TestA 類,這樣在調(diào)用 setter方法時髓迎,會調(diào)用 NSKVONotifying_TestA 的setter方法峦朗。新setter方法的實現(xiàn)大致如下:
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"];
[super setValue:aDate forKey:@"now"];
[self didChangeValueForKey:@"now"];
}
新建類以及修改對象的 isa 指針這些過程都是在運行時實現(xiàn)的,在應用層面上我們完全感知不到排龄。假設我們在項目中手動創(chuàng)建類NSKVONotifying_TestA,程序運行到注冊KVO的代碼時就會崩潰波势,這也能夠間接的證實,KVO機制確實創(chuàng)建了新的 NSKVONotifying_TestA 類橄维。
總結(jié)
SmallTalk 語言的創(chuàng)始人Alan Kay 曾說過尺铣,面向?qū)ο蟛皇?SmallTalk 的核心,消息傳遞才是争舞,The big idea is "messaging"凛忿。runtime在消息傳遞中扮演著非常重要的角色,理解和掌握runtime非常的有必要竞川。而且店溢,在項目的實際開發(fā)中,利用runtime也確實可以幫助解決很多問題委乌。本文是結(jié)合我自己在項目中遇到的問題所做的總結(jié)床牧,文中如有錯誤的地方,歡迎大家指正~如果大家在項目中使用runtime解決了其他問題福澡,也歡迎在評論區(qū)交流叠赦。
參考文章
http://tech.glowing.com/cn/objective-c-runtime/
http://blog.ibireme.com/2013/11/26/objective-c-messaging/