Runtime 系列 1-- 從一個崩潰談起

本文從一個崩潰問題談起,然后逐步深入,探討下runtime的細節和使用,主要涉及到的知識點如下:

  1. objc_msgSend的實現原理
  1. isa指針
  2. 類和元類
  3. object_getClass(obj)與[obj class]的區別

崩潰代碼

我們先來看看兩段代碼,第一段代碼主要是展示[self class ]和[super class]的區別,第二段代碼會在第一段代碼的基礎上進一步探討他們的區別,然后就為什么會引起崩潰做進一步探討,這會涉及到上面的四個runtime相關的知識點

第一段代碼

#import "father.h"

@interface son : father

@end

#import "son.h"

@implementation son
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        NSLog(@"self class-->%@",[self class]);
        NSLog(@"super class-->%@",[super class]);

    }
    return self;
}
@end

輸出:
2016-08-09 09:42:40.152 test1[33870:252634] self class-->son
2016-08-09 09:42:40.153 test1[33870:252634] super class-->son

分析:

根據其他語言的經驗,我們想當然會認為self class是son,super class是father。但是輸出的卻都是一樣的,都是son。這是因為oc一切方法的本質都是消息的發送和接受,是動態的,不能按照字面意思理解。具體的我們后面再進一步探討。

第二段代碼:

#import <UIKit/UIKit.h>

@interface father : UIViewController
@property(nonatomic,strong) NSString * name;

@end

=====================================

#import "father.h"

@implementation father

-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self =[super initWithCoder:aDecoder]) {
        self.name = @"";
    }
    return self;
}


-(void)setName:(NSString *)name{
    NSLog(@"%s,%@", __PRETTY_FUNCTION__, @"不會調用這個方法");
    _name = name;
}
@end

#import "father.h"

@interface son : father

@end

===================================
#import "son.h"

@implementation son
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        NSLog(@"self class-->%@",[self class]);
        NSLog(@"super class-->%@",[super class]);

    }
    return self;
}

-(void)setName:(NSString *)name{
    NSLog(@"%s,%@", __PRETTY_FUNCTION__, @"會調用這個方法");
    if ([name isEqualToString:@""]){
        [NSException raise:NSInvalidArgumentException format:@"姓名不能為空"];
    }
    

}

@end

輸出:

2016-08-09 10:00:22.203 test1[34027:265079] -[son setName:],會調用這個方法
2016-08-09 10:00:26.316 test1[34027:265079] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '姓名不能為空'

分析:

我們在父類father的initWitheCoder方法中設置self.name = @"",我們只想初始化一下name的值為空。然后我們在子類son中重寫了setName方法,設置不讓name變量為空,否則拋出異常。

按道理說,我們在父類使用self.name方法應該調用father的setName方法,在子類son中使用self.name方法也應該調用sone的setName方法。但是實際上我們看到在父類中使用self.name調用的確實子類son的setName方法,從而導致了崩潰,這是為什么呢?

暫且按下不表,我們先來看看runtime相關的一些知識,只有理解了這些知識,我們才能真正理解上面出現的問題。


objc_msgSend的實現原理

我們平時使用方法調用都是如下的模式:

[target MethodName:var1];

但是卻很少去深究這句代碼為什么能執行,怎么執行。下面我們就來看看,

首先這句代碼會被編譯為如下樣式:

objc_msgSend(target,@selector(MethodName:),var1);

而objc_msgSend函數就是我們runtime里面的一個非常重要的函數,所有的消息轉發都和這個函數息息相關。

ObjC 是一種面向runtime(運行時)的語言,也就是說,它會盡可能地把代碼執行的決策從編譯和鏈接的時候,推遲到運行時。這給程序員寫代碼帶來很大的靈活性,比如說你可以把消息轉發給你想要的對象,或者隨意交換一個方法的實現之類的。這就要求 runtime 能檢測一個對象是否能對一個方法進行響應,然后再把這個方法分發到對應的對象去。我們拿 C 來跟 ObjC 對比一下。在 C 語言里面,一切從 main 函數開始,程序員寫代碼的時候是自上而下地,一個 C 的結構體或者說類吧,是不能把方法調用轉發給其他對象的。但是在oc中,我們可以在運行時把上面的target換成其他對象,非常靈活。

objc_msgSend函數的原型如下:

id objc_msgSend ( id self, SEL op, ... );

上面的函數里面有兩個參數id和SEL,我們分別看看。

id

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

typedef struct objc_object *id;

那objc_object又是啥呢:

struct objc_object { Class isa; };

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

PS:

isa指針不總是指向實例對象所屬的類,不能依靠它來確定類型,而是應該用class方法來確定實例對象的類。因為KVO的實現機理就是將被觀察對象的isa指針指向一個中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術。

SEL

objc_msgSend函數第二個參數類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的數據結構是SEL:

typedef struct objc_selector *SEL;

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

可以根據SEL(方法編號)去類方法列表找到對應的實例方法的實現,或者去元類方法列表找到對應的類方法的實現.

消息轉發步驟

結合上面的知識點,我們現在就可以理解了objc_msgSend的實現原理。

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函數了。
  2. 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數去執行。
  4. 如果 cache 找不到就找一下方法分發表。
  5. 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
  6. 如果還找不到就要開始進入動態方法解析了,這個我在另外一篇文章《runtime消息轉發機制的理解和使用》中會詳細描述

PS:這里說的分發表其實就是Class中的方法列表,它將方法選擇器和方法實現地址聯系起來。

一圖以蔽之:

image

isa指針

上面的objc_msgSend實現原理里面提到了isa指針、類。也是我們平常經常解除的兩個概念,但是他們內部具體如何實現,卻很少深究。

我們知道所有的對象都是由其對應的類實例化而來,在Objective-C中,我們用到的幾乎所有類都是NSObject類的子類,NSObject類定義格式如下(忽略其方法聲明):

@interface NSObject <NSObject> {
Class isa;
}

這個Class為何物?在objc.h中我們發現其僅僅是一個結構(struct)指針的typedef定義:

typedef struct objc_class *Class;

同樣的,objc_class又是什么呢?在Objective-C2.0中,objc_class的定義如下:

struct objc_class {
Class isa;
}

寫到這里大家可能就暈了,怎么又有一個isa?

我們知道isa指針指向的是該對象所屬的類,對于實例對象的isa指針我們知道是指向其所屬的類,但是實例對象所屬的類的isa指針又指向誰呢?

這里我們先記住一點:類本身也是對象!!

那么既然類本身也是對象,那么他所屬的類是誰?

答案就是:元類!!

所以實例對象所屬的類的isa指針指向的是元類。

1.類對象的實質

類對象是由編譯器創建的,即在編譯時所謂的類,就是指類對象(官方文檔中是這樣說的: The class object is the compiled version of the class)。

任何直接或間接繼承了NSObject的類,它的實例對象(instance objec)中都有一個isa指針,指向它的類對象(class object)。這個類對象(class object)中存儲了關于這個實例對象(instace object)所屬的類的定義的一切:包括變量,方法,遵守的協議等等。

因此,類對象能訪問所有關于這個類的信息,利用這些信息可以產生一個新的實例,但是類對象不能訪問任何實例對象的內容。當你調用一個 “類方法” 例如 [NSObject alloc],你事實上是發送了一個消息給他的類對象。

2.類對象和實例對象的區別

盡管類對象保留了一個類實例的原型,但它并不是實例本身。它沒有自己的實例變量,也不能執行那些類的實例的方法(只有實例對象才可以執行實例方法)。然而,類的定義能包含那些特意為類對象準備的方法–類方法( 而不是的實例方法)。類對象從父類那里繼承類方法,就像實例從父類那里繼承實例方法一樣。

類對象是一個功能完整的對象,所以也能被動態識別(dynamically typed),接收消息,從其他類繼承方法。特殊之處在于它們是由編譯器創建的,缺少它們自己的數據結構(實例變量),只是在運行時產生實例的代理。

元類

實際上,類對象是元類對象的一個實例!!

元類描述了 一個類對象,就像類對象描述了普通對象一樣。不同的是元類的方法列表是類方法的集合,由類對象的選擇器來響應。當向一個類發送消息時,objc_msgSend會通過類對象的isa指針定位到元類,并檢查元類的方法列表(包括父類)來決定調用哪個方法。元類代替了類對象描述了類方法,就像類對象代替了實例對象描述了實例化方法。

很顯然,元類也是對象,也應該是其他類的實例,實際上元類是根元類(root class’s metaclass)的實例,而根元類是其自身的實例,即根元類的isa指針指向自身。

類的super_class指向其父類,而元類的super_class則指向父類的元類。元類的super class鏈與類的super class鏈平行,所以類方法的繼承與實例方法的繼承也是并行的。而根元類(root class’s metaclass)的super_class指向根類(root class),這樣,整個指針鏈就鏈接起來了!!

記住,當一個消息發送給任何一個對象, 方法的檢查 從對象的 isa 指針開始,然后是父類。實例方法在類中定義, 類方法在元類和根類中定義。(根類的元類就是根類自己)。

總結

綜上所述,類對象(class object)中包含了類的實例變量,實例方法的定義,而元類對象(metaclass object)中包括了類的類方法(也就是C++中的靜態方法)的定義。

類對象和元類對象中當然還會包含一些其它的東西,蘋果以后也可能添加其它的內容,但對于我們只需要記住:類對象存的是關于實例對象的信息(變量,實例方法等),而元類對象(metaclass object)中存儲的是關于類的信息(類的版本,名字,類方法等)。

要注意的是,類對象(class object)和元類對象(metaclass object)的定義都是objc_class結構,其不同僅僅是在用途上,比如其中的方法列表在類對象(instance object)中保存的是實例方法(instance method),而在元類對象(metaclass object)中則保存的是類方法(class method)

一圖以蔽之

image

object_getClass(obj)與[obj class]的區別

  1. object_getClass(obj)返回的是obj中的isa指針;

  2. 而[obj class]則分兩種情況:

    • 當obj為實例對象時,[obj class]調用的是實例方法:-(Class)class,返回的obj對象中的isa指針;

    • 當obj為類對象(包括元類和根類以及根元類)時,調用的是類方法:+ (Class)class,返回的結果為其本身。

  3. -(Class)class的實現如下:

    - (Class)class {
     return object_getClass(self);
     }
    

第一段代碼解析

回頭我們再看看第一段代碼為什么[self class]和[super class]都輸出的是son。

[self class]

根據上面的知識,我們知道[self class]最終會轉換為如下形式:

id objc_msgSend(son的實例對象self, @selector(class), ...)

消息的接受者是son的實例對象self,然后調用他的class方法,它自己沒有實現該方法,最終在NSObject中找到該方法的實現,然后返self的isa指針,此時self是son類的實例對象,那么isa指針也就是指向son類,所以[self class]返回的son。

[super class]

而當使用 [super setName] 調用時,會使用 objc_msgSendSuper 函數.
看下 objc_msgSendSuper 的函數定義:

id objc_msgSendSuper(struct objc_super *super,  @selector(class), ...)

第一個參數是個objc_super的結構體,第二個參數還是類似上面的類方法的selector,先看下objc_super這個結構體是什么東西:

struct objc_super {
   id receiver;
   Class superClass;
};

在此處上面的結構體轉換為如下樣式:

struct objc_super {
   son的實例對象self;
   father;
};

那么調用[super class]后的內部流程如下:

  1. 當使用 [super class] 時,這時要轉換成 objc_msgSendSuper 的方法。
  2. 先構造 objc_super 的結構體,第一個成員變量就是 self,第二個成員變量是 father,然后要找 class 這個 selector,先去 superClass 也就是father中去找,沒有,然后去father的父類中去找,結果還是在 NSObject 中找到了。
  3. 然后內部使用函數 objc_msgSend(objc_super->receiver, @selector(class)) 去調用,此時已經和我們用 [self class] 調用時相同了,因為此時的 receiver 還是 son的實例對象self,所以這里返回的也是 son。

總結

很多人會想當然的認為“ super 和 self 類似,應該是指向父類的指針吧!”。這是很普遍的一個誤區。

其實 super 是一個 Magic Keyword, 它本質是一個編譯器標示符,和 self 是指向的同一個消息接受者!他們兩個的不同點在于:super 會告訴編譯器,調用 class 這個方法時,要去父類的方法,而不是本類里的。

上面的例子不管調用[self class]還是[super class],接受消息的對象都是當前 Son *xxx 這個對象。

當使用 self 調用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;而當使用 super 時,則從父類的方法列表中開始找。然后調用父類的這個方法。


第二段代碼解析

第二段代碼崩潰的原因是因為在father里面使用self.name = @""調用的是子類的setName方法,從而導致了崩潰。

我們來看看為什么沒有調用自己的setName方法,反而是調用了子類son的setName方法。

其實結合第一段代碼解析就知道,在父類father里面調用[self setName]方法,消息的接受者依然是son的實例對象,然后去son的類方法列表去找setName方法,找到了,就執行。

所以你會看到明明在父類里面調用的自己的setName方法,但是真正被執行的確實子類son的setName方法。

所以我們要注意,如果子類重寫了父類的方法,那么不管在子類還是父類調用該方法,最終被執行的方法是子類的方法。


總結

本文從一個崩潰問題談起,然后開始逐步深入,探討了一些runtime的特性和機制,由此可見runtime的一些本質,但也只是管中窺豹,做拋磚引玉之用,大家有更好的想法,歡迎探討。

后續我會繼續對runtime其他特性進行介紹,歡迎一起探討。

這是runtime的源碼,有興趣的同學可以自行閱讀,可以加深理解

http://opensource.apple.com//source/objc4/objc4-208/runtime/objc-runtime.m

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

推薦閱讀更多精彩內容