細說objc中的Runtime

前言

這篇文章講解了一些runtime的基本知識,需要有一定的objc基礎及開發經驗。為了更好的閱讀體驗,推薦戳我的博客閱讀。

從理解發送消息講起

為什么objc叫消息發送?為什么不叫函數調用?在objc中,發送消息僅僅表示一種行為 ,不能理解為像C語言中那樣的函數調用。原因就是在發送消息的背后,runtime幫我們做了非常多的事情。這樣是objc能真正成為一門動態語言的真正原因。


要學習runtime所要掌握的幾個基本概念

在開始學習runtime之前,有幾個基本的概念是必須要了解的:

SEL

SELselectorobjc中的表示類型,selector是方法選擇器,可以理解為方法的ID。而這個ID的數據結構是SEL

typedef struct objc_selector *SEL

其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。

id

objc_msgSend第一個參數類型為id,大家對它都不陌生,它是一個指向類實例的指針:

typedef struct objc_object *id

那么objc_object又是啥呢:

struct objc_object {
    Class isa;
};

objc_object結構體包含一個isa指針,根據isa指針就可以順藤摸瓜找到對象所屬的類。

Class

之所以說isa是指針是因為Class其實是一個指向objc_class結構體的指針:

typedef struct objc_class *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中的對象與類博客:

    1. 類對象的isa指針指向的是類對象的元類,每個類對象都有自己的元類。
    2. 類對象里存放時的實例對象的對象方法,屬性,協議列表等信息。注意objc_cache *cache這個東西,存的是匹配信息,比如消息來到時該對象能不能處理此消息,方法對應的實現等都收納在此中。
    3. 只有類對象才有super_class這個指針。
    4. NSObjectsuper_class指針指向nil
  • 關于元類的概念:

    1. 元類是類對象的類對象,類對象的對象方法(即實例對象的類方法)列表就存在此處。
    2. 所有元類的isa指針指向NSObject的元類,即根元類。super_class指針指向NSObject

如下圖:


對象,類,元類

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指向了方法的實現,本質上是一個函數指針,后面會詳細講到。

IMP

IMP在objc.h中的定義是:

typedef id (*IMP)(id, SEL, ...);

  • 它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在后面會提到。

  • 你會發現IMP指向的方法與objc_msgSend函數類型相同,參數都包含idSEL類型。每個方法名都對應一個SEL類型的方法選擇器,而每個實例對象中的SEL對應的方法實現肯定是唯一的,通過一組idSEL參數就能確定唯一的方法實現地址;反之亦然。

runtime做的那些事

首先需要明確的是,在objc中,直到運行時才將消息與方法實現綁定。而這些工作都是runtime為我們做的。

runtime之消息轉發

其實[receiver message]會被編譯器轉化為:

objc_msgSend(receiver, selector)

如果消息含有參數,則為:

objc_msgSend(receiver, selector, arg1, arg2, ...)

在平時的調用中,看起來像是objc_msgSend返回了數據,其實objc_msgSend從不返回數據而是你的方法被調用后返回了數據。下面詳細敘述下消息發送步驟:

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函數了。
  2. 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數去執行。
  4. 如果 cache 找不到就找一下方法分發表(即class里的method_list表)。
  5. 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
  6. 如果還找不到就要開始進入動態方法解析了,后面會提到。

當然,objc_msgSend函數只是一般情況下的調用,還有會有例如給父類發送消息,返回值是結構體而不是數值等情況,會采用其他的例如objc_msgSendSuper函數等,在此不再敘述。

消息轉發的第一步: 動態方法解析

所謂動態方法解析是發生在objc_msgSend函數查找完所有類對象or元類方法列表后仍未找到方法實現第一個調用的方法,動態方法解析發生在消息轉發之前:

+ (BOOL)resolveClassMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相應的類方法調用
+ (BOOL)resolveInstanceMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相應的對象方法調用

在平時的開發中,最常用的情況就是聲明某個屬性為@dynamic后,需要我們自己提供settergetter方法時。如:

@dynamic propertyName;

添加如上關鍵字修飾屬性表示告訴編譯器我們會動態的提供存取方法,此時動態方法解析就是個不錯的選擇:

    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

第二步: 重定向

動態方法解析雖然強大,可以動態地為一個類添加方法,但它也有個很大的弊端:只能為當前類添加方法。如果我們想在消息無法解讀時調用其他類的方法呢?此時就要用到重定向了。

重定向是發生在動態方法解析之后,完整的消息轉發之后的。在此處我們就可以動態的將一條消息轉換為其他類的調用:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    if (aSelector == @selector(intstanceNoImpMethod)) {
        
        return [testClass class];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

需要注意的時,此時我們將消息轉發給了testClass這個類,就表示我們希望這個不能被當前類解讀的消息中的sel能被testClass執行。如果testClass類中沒有這個sel的類方法,程序一樣會crash:

+[testClass intstanceNoImpMethod]: unrecognized selector sent to class 0x103122ea8

如果我們在此方法中直接返回self或者nil,都會跳過這個步驟直接進入完整的消息轉發機制。

所以如果你想把這個消息解讀為其他類的對象方法,就要返回這個類的對象,如果想解讀為類方法,就要返回類對象

同理,如果你想轉發一個含有類方法的消息,就應該調用:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(intstanceNoImpMethod)) {
        
        return [testClass new];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

于是你可以大開腦洞:把一個類的類方法or對象方法換成另一個類的對象方法or類方法,怎么組合隨你,只要注意sel的名稱一致即可。有木有感覺很酷??

ps:我在此處混用了類與類對象,其實這倆是一個東西,如果你不能理解,戳這里:深入理解objc中的對象與類

可見,利用重定向是在objc模擬多繼承的一種方法。但是重定向和動態方法解析一樣都有弊端,那就是還是不夠“靈活”。消息中sel不能被我們更換。設想這種情景:如果我們又想更換sel,又想更換消息的接受者,此時我們該怎么做?

第三步:完整的消息轉發機制

上文我們降到了如果動態方法解析失敗,進入重定向,那么重定向也失敗了,就來到了完整的消息轉發機制:

這里遇到了些小困難,接下來在填坑吧


runtime健壯的實例變量

@property大家每天都在用,但也許你不知道,runtime在你為類添加了一個屬性時,它會將這個成員變量(iva)存放在類對象里。這和比如JAVA等語言有著很大的不同:

objc將實例變量當做一種存儲偏移量(offset)所用的特殊變量交由類對象保管。偏移量會運行期間查找,如果類的定義變了,那存儲的偏移量也就變了。這樣的話,無論何時訪問實例變量,都能獲取到正確的值。

基于這個原因,我們才能該動態的給一個類添加屬性,因為屬性列表本來就是動態查找的。

8月21日更新:關于健壯的實例變量,還有這樣的說法:

當一個類定義了某些成員變量后編譯一次后,再次改變該類的成員變量,會導致偏移量發生改變。在有runtime的情況下,它會自動幫你調整偏移量,以保證不用再次編譯文件。


關聯對象

關聯對象指的是動態的為一個對象添加變量,之前有寫過介紹的短文:在分類中給類添加屬性


神奇的Method Swizzling

之前所說的消息轉發雖然功能強大,但需要我們了解并且能更改對應類的源代碼,因為我們需要實現自己的轉發邏輯。當我們無法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現時,該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時無法達到目的。這里介紹的是 Method Swizzling ,它通過重新映射方法對應的實現來達到“偷天換日”的目的。跟消息轉發相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。

上一個??:

+ (void)load{
    
    Class aClass = [self class];
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(cumtomMethod:);
    
    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
    
    // When swizzling a class method, use the following:
    // Class aClass = object_getClass((id)self);
    // ...
    // Method originalMethod = class_getClassMethod(aClass, originalSelector);
    // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
    
    BOOL didAddMethod =
    class_addMethod(aClass,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(aClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    
}

調用:

- (void)cumtomMethod:(BOOL)animted{
    
    [self  cumtomMethod:animted];
    
    NSLog(@"1111");
}

輸出:

2016-08-18 00:39:04.658 總結測試[80016:3763455] 1111

我剛開始看這段代碼也是暈的一塌糊涂,現在回想起來是沒能立即SELIMP,下面是具體的調用過程:

系統調用viewWillAppear:(SEL) ----> 來到了customMethodIMP -----> 我們自己調用customMethod:SEL -----> 系統viewWillAppear:IMP

目前為止小弟也只是知道有這么個東西,還真沒用到過這玩意。真有興趣可以看看這篇:Objective-C的hook方案(一): Method Swizzling

8-21凌晨補充下:最近基友分享了一篇關于Method Swizzling的應用方案,是騰訊一面提到的。有興趣可以看下。


扯扯淡??

這篇文章花了不少心血,也通過撰寫這篇文章徹底重新認識了Runtime這個之前小白時看都不敢看的東西。也觀摩了不少大神的博客,感覺平時應該多注意這些好的資源,有時候比悶頭寫代碼強不少??。
在此放一下我特別喜歡的一位博主的博客:玉令天下的博客,就像引言所說,這篇文章不過是我讀這為大神博客的學習筆記罷了。作為同齡的開發者,很是汗顏啊,共勉吧~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,755評論 0 9
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,229評論 0 7
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,581評論 33 466
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,153評論 0 9
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 758評論 0 2