Runtime運行時機(jī)制

想深入理解Objective-C這門動態(tài)語言就不得不深入理解下它的“動態(tài)”是如何實現(xiàn)的。早先拜讀過《Effective Objective-C 2.0》就讓我更深入的窺探到OC運行時特別之處,本文當(dāng)中也有部分內(nèi)容借鑒自這本經(jīng)典著作键耕。第四屆互聯(lián)網(wǎng)大會的項目也完成了,年底閑來無事整理寫些總結(jié)纺阔。

動態(tài)語言是相對于靜態(tài)語言如C語言區(qū)別而言的玻靡。C語言在編譯期就能決定了運行時應(yīng)該調(diào)用的函數(shù)绣否,函數(shù)地址實際上是硬編碼在指令之中的。而OC在編譯期甚至不知道對象的類型奴紧,需要在運行時處理唐含,當(dāng)然它的底層也都是轉(zhuǎn)化為C函數(shù)調(diào)用。運行時實際上決定了OC最終的編程實現(xiàn)沫浆,即什么類的對象執(zhí)行什么函數(shù)捷枯,而且這個執(zhí)行調(diào)用是可以修改的,這也是運行時吸引人的地方专执。

運行時的調(diào)用有3種方式
* 第一種是系統(tǒng)底層封裝實現(xiàn)的淮捆,所有OC的代碼就會調(diào)用,那就是消息傳遞機(jī)制本股。

id value = [someObj methodName:parameter];
// 編譯期OC轉(zhuǎn)化為標(biāo)準(zhǔn)C函數(shù)
id value = objc_msgSend(someObj,@selector(methodName:),parameter);

objc_msgSend消息傳遞機(jī)制中的核心函數(shù)(實際上是四種objc_msgSend 攀痊, objc_msgSend_stretobjc_msgSendSuper拄显, objc_msgSendSuper_stret苟径,其他三種在處理一些“邊界情況”的時候會用到,可查閱《Effective Objective-C 2.0》第45頁躬审,這篇文章也有提及 )棘街,它會根據(jù)對象即someObj和它的方法名來調(diào)用合適的方法完成完整的函數(shù)調(diào)用實現(xiàn)蟆盐。在查詢方法名時,它會首先在someObj的“方法列表”中查找蹬碧,找不到就沿著它的繼承體系向上找舱禽,如果都沒有那就會看到調(diào)試時控制臺提示的錯誤包含一句[__ClassName methodName] unrecognized selector sent to instance xxxxsomeObj所屬的__ClassName類找不到methodName這個對象方法恩沽,否則就可以正常運行了誊稚。如此看來,方法調(diào)用似乎每次都需要查表效率很低罗心,其實不然里伯,objc_msgSend會將匹配結(jié)果緩存到“快速映射表”(fast map)里,每個類都有這樣一塊緩存渤闷,下次再調(diào)用方法就直接可在映射表里找了疾瓮。

  • 第二種是NSObjec這個基類特有的幾個調(diào)用方法,能做類型判斷或者查看是否有響應(yīng)函數(shù)的這些方法都是運行時機(jī)制的方法飒箭。
-class方法返回對象的類狼电;
-isKindOfClass: 和 -isMemberOfClass: 方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當(dāng)前類的成員變量);
-respondsToSelector: 檢查對象能否響應(yīng)指定的消息弦蹂;
-conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法肩碟;
-methodForSelector: 返回指定方法實現(xiàn)的地址。
  • 第三種就是直接調(diào)用Runtime函數(shù)庫了凸椿,稍后在實際應(yīng)用中會介紹到削祈。

runtime可以做什么

    1. 動態(tài)方法添加

如上所述,在開發(fā)中偶爾會有在消息轉(zhuǎn)發(fā)過程中找不到調(diào)用方法而導(dǎo)致程序閃退脑漫,為了用戶體驗髓抑,閃退是不能允許的,所以我們需要利用運行時來杜絕因這個問題而導(dǎo)致的閃退优幸,而轉(zhuǎn)化為彈出其他報錯提示吨拍,并把日志記錄到后臺中方便我們做進(jìn)一步的程序完善。

[__ClassName methodName] unrecognized selector sent to instance xxxx這段異常信息是由NSObjectdoesNotRecognizeSelector:方法所拋出的网杆。但并不是攔截這個方法做處理防止閃退密末,因為這個方法只是幫助打印提示信息的。

消息轉(zhuǎn)發(fā)分為兩個階段跛璧,第一階段是沿著繼承體系查找是否能動態(tài)添加方法,以處理當(dāng)前這個未知的方法新啼,叫“動態(tài)方法解析”追城,第二階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”,如果第一階段運行完燥撞,那方法接收者(如上邊例子中的someObj)就無法再動態(tài)添加方法來響應(yīng)這個找不到的方法了座柱。此時運行時系統(tǒng)會請求接收者用其他手段來處理與消息有關(guān)的方法調(diào)用迷帜,這里又細(xì)分為2小步。首先請接收者看看有沒有其他對象能處理這條消息色洞,如果有戏锹,那么一切如常。若沒有火诸,則會啟動完整的消息轉(zhuǎn)發(fā)機(jī)制锦针,運行時系統(tǒng)會把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對象中(NSInvocation的使用),再給接收者最后一次機(jī)會置蜀,讓它來設(shè)法解決這條消息奈搜。

動態(tài)方法解析:
在對象收到無法解讀的消息后,首先將調(diào)用其所屬類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
該方法的參數(shù)就是那個未知的方法盯荤,其返回值Boolean類型馋吗,表示這個類是否能新增一個實例方法類處理這個方法。如果未知的方法不是對象方法而是類方法秋秤,那么調(diào)用的就是+ (BOOL)resolveClassMethod:(SEL)sel這個方法了宏粤。
例如:
someObj調(diào)用了未實現(xiàn)的實例方法callMethod,此時我們可以通過重載+ (BOOL)resolveInstanceMethod:(SEL)sel來處理這個未知方法。

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == NSSelectorFromString(@"callMethod")) {
        /*
         * IMP 是編譯期生成的函數(shù)指針
         * class_addMethod 函數(shù)完成向特定類添加特定方法實現(xiàn)的操作
        */
        class_addMethod(self,sel,(IMP)callMethodTest,"chart");
        return YES;
    }

    return [super resolveClassMethod:sel];
}

void callMethodTest (id self ,SEL _cmd){
    NSLog(@"---callMethodTest----");
}

這種處理方式也常用來處理@dynamic修飾的屬性灼卢,因為使用@dynamic就是告訴編譯器绍哎,不要自動創(chuàng)建實現(xiàn)屬性所用的實例變量,也不要為其創(chuàng)建存取方法芥玉,我們會為這個屬性動態(tài)提供存取方法蛇摸。

注意:我們并不能重載+ (BOOL)resolveInstanceMethod:(SEL)sel使返回值直接為YES,這樣會讓我們不知道哪里出了問題,因為我們不能通過SEL來獲取方法信息灿巧。

  • 2.動態(tài)添加屬性和判斷屬性類型

動態(tài)添加屬性:
一般來說分類(category)中是不支持添加屬性的赶袄,但有時候確實需要添加,那么就可以通過 objc/runtime.h 庫中的一些函數(shù)來實現(xiàn)抠藕。在AFNetworking饿肺、MasonrySDWebImage等常用框架中都大量用到了這種方式盾似。
栗子:

#import <Foundation/Foundation.h>

@interface NSObject (ExchangeMethod)

@property (strong, nonatomic) NSString *name;

@end
#import "NSObject+ExchangeMethod.h"
#import <objc/runtime.h>

#define NameKey @"nameKey"

@implementation NSObject (ExchangeMethod)

- (void)setName:(NSString *)name{
    // 將屬性同對象關(guān)聯(lián)
    objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    // 取出 對應(yīng)Key關(guān)聯(lián)的對象屬性
    return objc_getAssociatedObject(self, NameKey);
}

@end

屬性類型判斷:
類型判斷常見的使用場景就是數(shù)據(jù)解析--字典轉(zhuǎn)模型敬辣。
獲取屬性列表的方式有兩種:

// 第一種
    unsigned int count;
    
    objc_property_t *properties = class_copyPropertyList(self.class, &count);
    
    NSMutableArray *array = [NSMutableArray array];
    
    for (int i =0; i< count ; i++) {
        objc_property_t pro = properties[I];
        const char *name = property_getName(pro);
        const char *attributes = property_getAttributes(pro);
        NSString *property = [[NSString alloc] initWithUTF8String:name];
        [array addObject:property];
         NSLog(@"attributes : %s,  name: %s",attributes,name);
    }
// 第二種
    unsigned int count;
    /*
     *參數(shù)1:類名
     *參數(shù)2:傳入無符號整型的內(nèi)存地址,當(dāng)讀取到成員變量的數(shù)量時零院,會給這個值賦值
     *返回值:Ivar * :是一個指針類型溉跃,相當(dāng)于數(shù)組,里邊裝著Ivar
     */
    
    Ivar *ivars = class_copyIvarList([UIView class],&count);
    
    for (int i=0; i < count; i++) {
        Ivar ivar = ivars[I];
        // 獲取屬性名字告抄,調(diào)用函數(shù)ivar_getName(ivar)獲取
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 獲取屬性類型撰茎,調(diào)用函數(shù)ivar_getTypeEncoding(ivar)獲取
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        NSLog(@"type : %@,  name: %@",type,name);
    }
/// An opaque type that represents an instance variable.
/*
Ivar 是表示成員變量的類型
*/
typedef struct objc_ivar *![Ivar.png](http://upload-images.jianshu.io/upload_images/308319-1ad920412e90db1d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
;

/// An opaque type that represents an Objective-C declared property.
/*
objc_property_t 是表示一個Objective-C聲明的屬性
*/
typedef struct objc_property * objc_property_t;

兩者都可以獲取屬性名稱和類型,信息詳細(xì)程度不一樣打洼。

Ivar.png
objc_property_t.png

objc_property_t打印的屬性的特性字符串說明龄糊,通過property_getAttributes(objc_property_t _Nonnull property)獲取查看

//特性
  typedef struct {
      const char *name;           //特性名稱
      const char *value;          //特性的值
  } objc_property_attribute_t;

特性編碼 具體含義

R readonly
C copy
& retain
N nonatomic
G(name) getter=(name)
S(name) setter=(name)
D @dynamic
W weak
P 用于垃圾回收機(jī)制

詳細(xì)參見
一般獲取屬性信息用第一種逆粹,YYModel和MJExtension 框架中都有用到。

  • 3.方法交換
    OC對象在收到消息后炫惩,究竟調(diào)用哪種方法是在運行時才能解析決定的僻弹。而在運行時我們還可以新增、修改或者交換執(zhí)行方法他嚷,也叫“方法調(diào)配”即method swizzling蹋绽。

類的“方法列表”中會把方法名映射到相關(guān)的方法實現(xiàn)上,通過“動態(tài)消息派發(fā)系統(tǒng)”找到對應(yīng)的調(diào)用方法爸舒。這些方法均已函數(shù)指針的形式來表示蟋字,即IMP。比如:someObj對象可以響應(yīng)makeName扭勉、makeHeight鹊奖、makeSex等方法,這張表中的每個方法都映射到不同的IMP上涂炎,如下圖忠聚。

someObj對象的方法映射表.png

OC在運行時系統(tǒng)提供的幾個API能用來操作這張表。
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)可以用來做方法交換唱捣。

+(void)load{
    Method m1 = class_getInstanceMethod(self, NSSelectorFromString(@"makeName"));
    Method m2 = class_getInstanceMethod(self, @selector(testMakeName));
    
    method_exchangeImplementations(m1, m2);
}

不過两蟀,在實際開發(fā)中,直接交換方法的意義并不大震缭,每一個方法都應(yīng)該對應(yīng)自己的實現(xiàn)赂毯。但是,為既有方法添加新功能是比較實用的拣宰。
栗子:
NSString的獲取小寫字符串方法lowercaseString党涕,我們要打印信息,那我們可以這樣寫

+(void)load{
    Method m1 = class_getInstanceMethod(self, NSSelectorFromString(@"lowercaseString"));
    Method m2 = class_getInstanceMethod(self, @selector(mcLowercaseString));
    
    method_exchangeImplementations(m1, m2);
}

- (NSString *)mcLowercaseString{
    NSString *lowercase = [self mcLowercaseString];
    NSLog(@"%@ => %@",self,lowercase);
    return lowercase;
}

這段代碼看似會死循環(huán)巡社,其實不然膛堤,因為兩個方法名指向了對方的函數(shù)指針I(yè)MP,所以[self mcLowercaseString];實際上是調(diào)用的lowercaseString晌该。通過這種方式肥荔,我們可以為那些系統(tǒng)黑盒方法增加日志打印功能,非常有助于調(diào)試使用朝群。一般很少有人用這個特性永久修改某各類的功能燕耿,而且若濫用的話,反而會讓代碼不易讀難于維護(hù)姜胖。

關(guān)于分類category美團(tuán)技術(shù)博客也有一篇點擊查看缸棵。
喜歡就點個贊唄!
歡迎大家提出更好的改進(jìn)意見和建議,從搬磚到設(shè)計建筑的路上,你我同行堵第!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市隧出,隨后出現(xiàn)的幾起案子踏志,更是在濱河造成了極大的恐慌,老刑警劉巖胀瞪,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件针余,死亡現(xiàn)場離奇詭異,居然都是意外死亡凄诞,警方通過查閱死者的電腦和手機(jī)圆雁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帆谍,“玉大人伪朽,你說我怎么就攤上這事⊙打” “怎么了烈涮?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長窖剑。 經(jīng)常有香客問我坚洽,道長,這世上最難降的妖魔是什么西土? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任讶舰,我火速辦了婚禮,結(jié)果婚禮上需了,老公的妹妹穿的比我還像新娘跳昼。我一直安慰自己,他們只是感情好援所,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布庐舟。 她就那樣靜靜地躺著,像睡著了一般住拭。 火紅的嫁衣襯著肌膚如雪挪略。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天滔岳,我揣著相機(jī)與錄音杠娱,去河邊找鬼。 笑死谱煤,一個胖子當(dāng)著我的面吹牛摊求,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刘离,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼室叉,長吁一口氣:“原來是場噩夢啊……” “哼睹栖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茧痕,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤野来,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后踪旷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體曼氛,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年令野,在試婚紗的時候發(fā)現(xiàn)自己被綠了舀患。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡气破,死狀恐怖聊浅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情堵幽,我是刑警寧澤狗超,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站朴下,受9級特大地震影響努咐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜殴胧,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一渗稍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧团滥,春花似錦竿屹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至力惯,卻和暖如春碗誉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背父晶。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工哮缺, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人甲喝。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓尝苇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子糠溜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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