本文從一個崩潰問題談起,然后逐步深入,探討下runtime的細節和使用,主要涉及到的知識點如下:
- objc_msgSend的實現原理
- isa指針
- 類和元類
- 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的實現原理。
- 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函數了。
- 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉。
- 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數去執行。
- 如果 cache 找不到就找一下方法分發表。
- 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
- 如果還找不到就要開始進入動態方法解析了,這個我在另外一篇文章《runtime消息轉發機制的理解和使用》中會詳細描述
PS:這里說的分發表其實就是Class中的方法列表,它將方法選擇器和方法實現地址聯系起來。
一圖以蔽之:
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)
一圖以蔽之
object_getClass(obj)與[obj class]的區別
object_getClass(obj)返回的是obj中的isa指針;
-
而[obj class]則分兩種情況:
當obj為實例對象時,[obj class]調用的是實例方法:-(Class)class,返回的obj對象中的isa指針;
當obj為類對象(包括元類和根類以及根元類)時,調用的是類方法:+ (Class)class,返回的結果為其本身。
-
-(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]后的內部流程如下:
- 當使用 [super class] 時,這時要轉換成 objc_msgSendSuper 的方法。
- 先構造 objc_super 的結構體,第一個成員變量就是 self,第二個成員變量是 father,然后要找 class 這個 selector,先去 superClass 也就是father中去找,沒有,然后去father的父類中去找,結果還是在 NSObject 中找到了。
- 然后內部使用函數 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