runtime進行曲,objc_msgSend的前世今生(一)

runtime小序曲一文中舉出了runtime的三種應用方式:

  • Objective-C源代碼,以objc_msgSend方法舉例。
  • NSObject的方法。
  • Runtime的函數。

本文將就第一種方式中的objc_msgSend方法展開講解。
學習進度:

一、objc_msgSend的兩種姿勢

很容易想到,OC中的兩種方法調用的姿勢:實例方法和類方法。下面我們通過一個簡單樣例的clang處理后的代碼看一下。

// OC代碼
// main.m
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
- (void)b;
+ (void)c;
@end
@implementation A
- (void)b {
    NSLog(@"b");
}
+ (void)c {
    NSLog(@"c");
}
@end
int main(int argc, char * argv[]) {
    A *aObject = [[A alloc] init];
    // 實例方法調用
    [aObject b];
    // 類方法調用
    [A c];
}

通過xcrun -sdk iphonesimulator9.3 clang -rewrite-objc main.m將其轉換為編譯后的偽代碼,即運行時執行的代碼的偽代碼(代碼比較多,只列出我們想要的)。

// A類定義
typedef struct objc_object A;
// objc_getClass方法
struct objc_class *objc_getClass(const char *);
// sel_registerName方法
SEL sel_registerName(const char *str) __attribute__((availability(ios,introduced=2.0)));

// 對應上述main函數
int main(int argc, char * argv[]) {
    A *aObject = ((A *(*)(id, SEL))(void *)objc_msgSend)((id)((A *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("A"), sel_registerName("alloc")), sel_registerName("init"));
    // 實例方法調用
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)aObject, sel_registerName("b"));
    // 類方法調用
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("A"), sel_registerName("c"));
}

將上述實例方法調用和類方法調用的強制轉換干掉之后看下。

實例方法

objc_msgSend(aObject, sel_registerName("b"));

可知,aObject是一個objc_object結構體,sel_registerName返回值為SEL。所以,這句話的意思大致是:向一個objc_object發送一個SEL。當然,具體怎么發送的這里先不談。

類方法

objc_msgSend(objc_getClass("A"), sel_registerName("c"));

可知,objc_getClass返回值為objc_class,sel_registerName返回值為SEL。所以,這句話的意思大致是:向一個objc_class發送一個SEL。當然,具體怎么發送的這里先不談。下文會說。

本節過后,大概有兩個疑問:
1、objc_object、objc_class、SEL分別是什么東西?
2、objc_msgSend到底如何工作?

二、objc_object和objc_class

上一節留了兩個問題,這里先回答第一個問題的一部分,即objc_object、objc_class是什么?可以參照runtime源碼。在objc.h和runtime.h里面分別可以找到objc_object和objc_class的實現。

objc_object

// 在objc.h中找到相關代碼
typedef struct objc_class *Class;
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

動態類型id其實就是一個objc_object。

可以看出一個類的實例,即一個對象,其實在runtime時刻是一個objc_class結構體,而結構體里面只有一個指向objc_class的指針isa。哎吆,objc_class好像在哪里見過?對,就是上面提到的調用類方法使用的結構體,下面看一下它的結構。

objc_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的結構體的東西就比較多了,因為現在討論的是調用類的方法,所以我們只關注三個東西isa、super_class和objc_method_list。
isa指向一個objc_class對象,而super_class比較容易理解,即指向這個類的父類。而objc_method_list會在下一節和SEL一起講解。

本節并沒有講解一些東西,只是說了下兩個結構體的實現,讓大家對isa、super_class有一個概念。本節過后,大家的疑問應該變多了,結合一中疑問,大概以下幾個:
1、objc_method_list、SEL。
2、isa、super_class有什么用?怎么用?
3、objc_msgSend到底如何工作?

三、objc_method、SEL和IMP

二中第一個疑問是objc_method_list、SEL,而引入objc_method_list是objc_method的列表,所以我們不研究objc_method_list,改為研究objc_method和SEL,而objc_method結構體中還有一個IMP,與本節內容息息相關,所以以objc_method、SEL和IMP為中心講解。

objc_method

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

可知,OC中每一個方法都是如上的結構體,里面的兩個關鍵字段method_name和method_imp共同為方法的查找和使用服務。

SEL
從SEL類型的成員為method_name可以知道,SEL大概代表一個方法的名字,可以通過以下方式檢驗。

// OC代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
- (void)b;
+ (void)c;
@end
@implementation A
- (void)b {
}
+ (void)c {
    NSLog(@"%s", @selector(b));
}
@end
int main(int argc, char * argv[]) {
    [A c];
}

// 輸出
2016-12-29 15:23:03.850 block[57622:533311] b

IMP
SEL的作用很容易理解,即想找到想要的objc_method結構體,需要通過SEL來遍歷,那么,IMP是什么呢?一般來說,通過SEL找到想要的objc_method,下一步就是調用方法的實現了,objc_method結構體中唯一有可能和方法實現相關的就是IMP了。所以,IMP是一個函數指針,指向objc_method對應方法的實現部分。

objc_method中的method_types指的是方法的返回值和參數。以返回值開始,依次把參數拼在后面,比如"i@",即為返回值為int類型,參數為一個對象,參照對照表

簡單樣例

// 考慮到只用文字描述有點空洞,這里通過class_addMethod方法的使用做個樣例
//先看下class_addMethod定義,幾個參數容易理解。cls為需要增加方法的類,后三個參數對應objc_method結構體中的幾個成員。
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

// OC代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
- (void)b;
@end
@implementation A
- (void)b {
}
@end
int c(NSString *str) {
    NSLog(@"c");
    return 0;
}
int main(int argc, char * argv[]) {
    class_addMethod([A class], @selector(c:), (IMP)c, "i@");
    A *aObject = [[A alloc] init];
    [aObject performSelector:@selector(c:)];
}

// 輸出,可見完成了方法c的調用。
2016-12-29 15:51:17.931 block[60624:567679] c

本節內容大致到這,SEL、IMP你應該搞懂了,現在只剩下兩個問題:
1、isa、super_class有什么用?怎么用?
2、objc_msgSend到底如何工作?

四、objc_msgSend工作原理和實例方法調用

1、objc_msgSend工作原理

偽代碼

// 首先看一下objc_msgSend的方法實現的偽代碼
id objc_msgSend(id self, SEL op, ...) {
   if (!self) return nil;
   // 關鍵代碼(a)
   IMP imp = class_getMethodImplementation(self->isa, SEL op);
   imp(self, op, ...); // 調用這個函數,偽代碼...
}
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) {
      ... // 執行動態綁定
    }
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; // 這個是用于消息轉發的
    return imp;
}
// 遍歷繼承鏈,查找IMP
IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }
    Class curClass = cls;
    IMP imp = nil;
    do { // 先查緩存,緩存沒有時重建,仍舊沒有則向父類查詢
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass); // 關鍵代碼(b)
    return imp;
}

分析
首先在Class中的緩存查找imp(沒緩存則初始化緩存),如果沒找到,則向父類的Class查找。如果一直查找到根類仍舊沒有實現,則用_objc_msgForward函數指針代替imp。最后,執行這個imp。

2、實例方法調用

代碼

// 實例方法調用,參照一
objc_msgSend(aObject, sel_registerName("b"));

// 參照二中實例對象的結構
typedef struct objc_class *Class;
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

// 順便給出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;

分析
結合關鍵代碼(a)、關鍵代碼(b)和上述結構,大概可以猜到實例方法的調用順序:
1、將一個objc_object結構體的isa指針(即其對應的objc_class結構體)和一個方法名SEL傳入class_getMethodImplementation。
2、在class_getMethodImplementation方法中,使用lookUpImpOrNil遍歷繼承鏈,若返回nil,則消息轉發(消息轉發下一章在講)。
3、在lookUpImpOrNil遍歷繼承鏈,即先在當前objc_class的cache中查找SEL,若沒有,則在methodLists中查找,若存在,則將其放入該objc_class的cache中,然后返回IMP。若不存在,則通過super_class指針找到該class的父class,繼續該步驟直到NSObject停止(NSObject的super_class指向nil)。
4、執行得到的IMP。

調用順序的第3步也解釋了objc_class 結構體中cache的存在意義。

圖示

實例對象的isa和繼承鏈

3、結論與問題

本節介紹了objc_msgSend工作原理、objc_object的isa和objc_class的super_class等的使用方式,但是有一個和本章相關的成員沒有提到,即objc_class結構體的isa指針。無疑,它是和類方法的調用直接相關的。

五、類方法調用和元類(metaClass)

上面留下的最后一個問題,objc_class結構體的isa指針問題。無疑是和類方法調用直接相關的。

1、類方法調用與實例方法調用區別

類方法的調用和實例方法顯然不用,后者是需要先創建一個實例,而這個實例在堆中有自己對應的objc_object結構體,是以這個結構體對應的其“私有”的objc_class結構體為基礎進行消息傳遞的。
那么,類方法呢?類方法不需要創建實例,每個類都有一個自己對應的現成的objc_class結構體。而類方法的調用也是以這個“公有”的objc_class結構體做操作的。

公有、私有指的是每個對象實例都有自己的objc_class(私有),實例方法的調用也是通過這個私有的objc_class,而每個類只有一個對應的objc_class(公有),任何地方調用類方法,都是通過這個公有的objc_class。

2、methodLists中的方法

代碼

// OC代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
- (void)b;
+ (void)d;
@end
@implementation A
- (void)b {
    NSLog(@"b");
}
+ (void)d {
    NSLog(@"d");
}
@end
int main(int argc, char * argv[]) {  
    // 在A對應的objc_class結構體的繼承鏈中找到實例方法b
    IMP bIMP = class_getMethodImplementation([A class], @selector(b));
    // 執行IMP
    bIMP();
}

// 輸出
2016-12-29 18:21:39.810 block[74277:689120] b

分析
可知,可以從A的objc_class的methodLists找到方法b。但是,嘗試將

IMP bIMP = class_getMethodImplementation([A class], @selector(b));

改為

IMP bIMP = class_getMethodImplementation([A class], @selector(d));

執行代碼,會crash。可以得出結論,objc_class的methodLists中只存有實例方法,并沒有類方法。那么類方法在哪里?

類方法的位置

// OC代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
- (void)b;
+ (void)d;
@end
@implementation A
- (void)b {
    NSLog(@"b");
}
+ (void)d {
    NSLog(@"d");
}
@end
int main(int argc, char * argv[]) {
    // 獲取A類對應的metaClass
    Class aMeta = objc_getMetaClass(class_getName([A class]));
    // 在metaClass中找類方法d
    IMP dIMP = class_getMethodImplementation(aMeta, @selector(d));
    dIMP();
}

// 輸出
2016-12-29 18:27:19.290 block[74821:695164] d

可知,A的objc_class結構體有一個現成的metaClass,而它存有A類的類方法。這里大概可以想到A類的objc_class結構體里面的isa指針指向這個metaClass。

當然,metaClass里面只存了類方法,沒有實例方法。

3、元類(metaClass)

由代碼

Class aMeta = objc_getMetaClass(class_getName([A class]));

可知,元類也是一個objc_class結構體。而相應的也有isa和super_class成員。super_class比較好理解,參照四中objc_msgSend實現,用于在繼承鏈上方法的查找。而元類的isa均指向NSObject對應的元類,這里不多解釋(NSObject對應元類的isa指向自己,其super_class指向NSObject,也不解釋了)。

4、類方法調用流程

// 類方法調用,參照一
objc_msgSend(objc_getClass("A"), sel_registerName("c"));

// 順便給出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_getClass("A"),即為獲取A的class,結合四中objc_msgSend實現,將其isa指針指向的metaClass傳入class_getMethodImplementation來進行查找,符合我們的預期。其后的步驟與實例方法相同。

5、類調用圖示

類的isa和繼承鏈

六、結論

結合四和五的圖展示,現在,你看懂這個圖了嗎。(可以發現本文在四中留下了兩個疑問,即動態綁定和消息轉發,且等下一篇分享)


runtime經典圖

七、文獻

1、http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
2、http://blog.csdn.net/reylen/article/details/50440450
3、http://cocoasamurai.blogspot.jp/2010/01/understanding-objective-c-runtime.html
4、http://blog.ibireme.com/2013/11/26/objective-c-messaging/
5、https://github.com/opensource-apple/objc4

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

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,225評論 0 7
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 755評論 0 2
  • 本文轉載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 772評論 0 1
  • 本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 816評論 0 4