runtime介紹以及在項目中的實際運用

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鲜漩。消息傳遞的過程大致如下:

  1. 首先根據(jù)object對象的isa指針獲取到該對象所屬的類源譬,然后在類對象的方法列表中尋找,是否有對應的方法孕似。如果有瓶佳,則找到,object即為消息最終的接收者鳞青;如果沒找到霸饲,進行第2步。
  2. 第上一步獲取到的類對象臂拓,根據(jù)super_class指針厚脉,可以獲取到該類的父類,在父類的方法列表中尋找是否有對應的方法胶惰。如果有傻工,則找到返回;如果沒有孵滞,則重復該操作中捆。
  3. 倘若一直找到基類(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;
}

兩個注意點:

  1. Method Swizzling應該寫在 + load 方法中剂习,原因:+ load 方法在類加載時就會被執(zhí)行;
  2. Method Swizzling 應該總是在 dispatch_once 中執(zhí)行较沪,原因: Method Swizzling 的修改是影響全局的鳞绕,因此只需要執(zhí)行一次就可以了,dispatch_once 可以保證這一點尸曼。
    PS:Method Swizzling 的功能非常強大们何,解決NSArray 越界崩潰只是一方面應用。

字典轉(zhuǎn)模型

在項目開發(fā)中控轿,服務器會返回json類型數(shù)據(jù)冤竹,這就需要我們在程序中將字典轉(zhuǎn)為模型以方便使用拂封。字典轉(zhuǎn)模型通常有兩種方法:

  1. 自己寫字典轉(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個字段躲株,解析起來非常麻煩片部;如果模型增加了新的屬性,需要增加相應的代碼霜定。

  1. 另一種方法是使用一些第三方庫档悠,如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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市革砸,隨后出現(xiàn)的幾起案子除秀,更是在濱河造成了極大的恐慌,老刑警劉巖算利,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件册踩,死亡現(xiàn)場離奇詭異,居然都是意外死亡效拭,警方通過查閱死者的電腦和手機暂吉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缎患,“玉大人慕的,你說我怎么就攤上這事〖酚妫” “怎么了肮街?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長判导。 經(jīng)常有香客問我嫉父,道長沛硅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任绕辖,我火速辦了婚禮摇肌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仪际。我一直安慰自己围小,他們只是感情好,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布弟头。 她就那樣靜靜地躺著吩抓,像睡著了一般涉茧。 火紅的嫁衣襯著肌膚如雪赴恨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天伴栓,我揣著相機與錄音伦连,去河邊找鬼。 笑死钳垮,一個胖子當著我的面吹牛惑淳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播饺窿,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼歧焦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肚医?” 一聲冷哼從身側(cè)響起绢馍,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肠套,沒想到半個月后舰涌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡你稚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年瓷耙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刁赖。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡搁痛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宇弛,到底是詐尸還是另有隱情鸡典,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布涯肩,位于F島的核電站轿钠,受9級特大地震影響巢钓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疗垛,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一症汹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贷腕,春花似錦背镇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至涮总,卻和暖如春胸囱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瀑梗。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工烹笔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抛丽。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓谤职,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亿鲜。 傳聞我的和親對象是個殘疾皇子允蜈,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評論 0 9
  • 本文詳細整理了 Cocoa 的 Runtime 系統(tǒng)的知識蒿柳,它使得 Objective-C 如虎添翼饶套,具備了靈活的...
    lylaut閱讀 800評論 0 4
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,554評論 33 466
  • 上一篇地址:Kingfisher源碼閱讀(一) 開始下載任務 上次說到了downloadAndCacheImage...
    Sheepy閱讀 1,962評論 1 5
  • 太可怕了,從今天的眼光去看未來的世界其馏,太恐怖了凤跑,可人類卻不可避免的要往這個方向發(fā)展,不以哪一個人的意志而轉(zhuǎn)移叛复,這是...
    穆諾閱讀 101評論 0 0