本篇文章講的是super的實際運做原理,若有同窗對super與self的區分還有疑惑的,請參考ChenYilong大神的《招聘一個靠譜的iOS》面試題參考答案(上)。html
打開蘋果API文檔,搜索objc_msgSendSuper
(對該函數陌生的先去補補rumtime)。c++
裏面明確提到了使用super
關鍵字發送消息會被編譯器轉化爲調用objc_msgSendSuper
以及相關函數(由返回值決定)。git
再讓咱們看看該函數的定義(這是文檔中的定義):github
id objc_msgSendSuper(struct objc_super *super, SEL op, ...);複製代碼
這裏的super
已經再也不是咱們調用時寫的[super init]
的super
了,這裏指代的是struct objc_super
結構體指針。文檔中明確指出,該結構體須要包含接收消息的實例以及一開始尋找方法實現的父類:面試
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained Class super_class;
/* super_class is the first class to search */
};複製代碼
既然知道了super
是如何調用的,那麼咱們來嘗試本身實現一個super
。objective-c
讓咱們先定義兩個類:shell
這是父類:Father類緩存
// Father.h
@interface Father : NSObject
- (void)eat;
@end
// Father.m
@implementation Father
- (void)eat {
NSLog(@"Father eat");
}
@end複製代碼
這是子類:Son類架構
// Son.h
@interface Son : Father
- (void)eat;
@end
// Son.m
@implementation Son
- (void)eat {
[super eat];
}
@end複製代碼
在這裏,咱們的Son類重寫了父類的eat
方法,裏面只作一件事,就是調用父類的eat
方法。函數
讓咱們在main中開始進行測試:
int main(int argc, char * argv[]) {
Son *son = [Son new];
[son eat];
}
// 輸出:
2017-05-14 22:44:00.208931+0800 TestSuper[7407:3788932] Father eat複製代碼
到這裏沒毛病,一個Son對象調用了eat
方法(內部調用父類的eat
),輸出告終果。
super
的效果:改寫Son.m:
// Son.m
- (void)eat {
// [super eat];
struct objc_super superReceiver = {
self,
[self superclass]
};
objc_msgSendSuper(&superReceiver, _cmd);
}複製代碼
運行咱們的main函數:
//輸出
2017-05-14 22:47:00.109379+0800 TestSuper[7417:3790621] Father eat複製代碼
沒毛病,咱們但是根據官方文檔來實現super
的效果。
難道super
真的就是如此?
讓咱們持懷疑的態度看看下面這個例子:
在這裏,咱們又有個Son的子類出現了:Grandson類
// Grandson.h
@interface Grandson : Son
@end
// Grandson.m
@implementation Grandson
@end複製代碼
該類啥什麼都沒實現,純粹繼承自Son。
而後讓咱們改寫main函數:
int main(int argc, char * argv[]) {
Grandson *grandson = [Grandson new];
[grandson eat];
}複製代碼
運行起來,過一會就crash了,如圖:
再看看相關線程中的方法調用:
這是一個死循環,因此係統讓該段代碼強制中止了。可爲何這裏會構成死循環呢?讓咱們好好分析分析:
superReceiver
結構體,內部包含了self
以及[self superclass]
。在調用過程當中,self指代的應是Grandson實例,也就是grandson這個變量,那麼[self superclass]
方法返回值也就是Son這個類。superReceiver
中的父類就是開始尋找方法實現的那個父類,咱們能夠得出,此時的objc_msgSendSuper(&superReceiver, _cmd)
函數調用的方法實現便是Son類中的eat
方法的實現。即,構成了遞歸。既然這裏不能使用superclass
方法,那麼咱們要如何本身實現super
的做用呢?
咱們是這段代碼的做者,因此,咱們能夠這樣:
// 咱們修改了Son.m
- (void)eat {
// [super eat];
struct objc_super superReceiver = {
self,
objc_getClass("Father")
};
objc_msgSendSuper(&superReceiver, _cmd);
}
// 輸出
2017-05-14 23:16:49.232375+0800 TestSuper[7440:3798009] Father eat複製代碼
咱們直接指明superReceiver
中要尋找方法實現的父類:Father。這裏一定有人會問:這樣子豈不是每一個調用[super xxxx]
的地方都須要直接指明父類?
「直接指明」的意思是,代碼中直接寫出這個類,好比直接寫:
[Father class]
或者objc_getClass("Father")
,這裏面的Father與"Father"就是咱們在代碼裏寫死的。
先不談這個疑問,咱們來分析這段代碼:
superReceiver
結構體,內部包含了self
以及Father
這個類。objc_msgSendSuper
函數直接去Father類中尋找eat
方法的實現,並執行(輸出)。如今這段代碼是以正常邏輯執行的。
[super xxxx]
真的要直接指明父類?咱們使用clang的rewrite指令重寫Son.m:
clang -rewrite-objc Son.m複製代碼
生成的Son.cpp文件:
static void _I_Son_eat(Son * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("eat"));
}複製代碼
這一行到底的代碼可讀性太差,讓咱們稍稍分解下(因爲語法問題咱們做了少許語法修改以經過編譯,實際做用與原cpp中一致):
static void _I_Son_eat(Son * self, SEL _cmd) {
__rw_objc_super superReceiver = (__rw_objc_super){
(__bridge struct objc_object *)(id)self,
(__bridge struct objc_object *)(id)class_getSuperclass(objc_getClass("Son"))};
typedef void *Func(__rw_objc_super *, SEL);
Func *func = (void *)objc_msgSendSuper;
func(&superReceiver, sel_registerName("eat"));
}複製代碼
先修改Son.m運行起來:
// Son.m
- (void)eat {
// [super eat];
//_I_Son_eat即爲重寫的函數
_I_Son_eat(self, _cmd);
}
// 輸出
2017-05-15 00:08:37.782519+0800 TestSuper[7460:3810248] Father eat複製代碼
沒有毛病。
重寫的代碼裏構建了一個__rw_objc_super
的結構體,定義以下:
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
// cpp裏的語法,忽略便可
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};複製代碼
該結構體與struct objc_super
一致。以後咱們將objc_msgSendSuper
函數轉換爲指定參數的函數func
進行調用。這裏請注意__rw_objc_super superReceiver
中的第二個值:class_getSuperclass(objc_getClass("Son"))
。
該代碼直接指明的類是本類:Son類。可是__rw_objc_super
結構體中的superClass
並非本類,而是經過runtime查找出的父類。這與咱們本身實現的 「直接指明Father爲objc_super
結構體的super_class
值」 最後達到的效果是同樣的。
因此,[super xxxx]
確定要經過指明一個類,能夠是父類,也能夠是本類,來達到正確調用父類方法的目的!只不過「直接指明」這件事,編譯器會幫咱們搞定,咱們只管寫super
便可。
clang的rewrite功能所提供的重寫後的代碼並不是編譯器(LLVM)轉換後的代碼,現在的編譯器在Xcode開啓bitcode功能後會生成一種中間代碼:LLVM Intermediate Representation(LLVM IR)。該代碼向上可統一大部分高級語言,向下可支持多種不一樣架構的CPU,具體可查看LLVM文檔。因此咱們的目標是從IR代碼求證super
究竟在作什麼事!
終端裏cd到Son.m文件所在目錄,執行:
clang -emit-llvm Son.m -S -o son.ll複製代碼
生成的IR代碼比較多,咱們挑重點進行查看:
%0 = type opaque
// Son的eat方法
define internal void @"\01-[Son eat]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8 // 分配一個指針的內存,8字節對齊(聲明一個指針變量)
%4 = alloca i8*, align 8 // 分配一個char *的內存(聲明一個char *指針變量)
%5 = alloca %struct._objc_super, align 8 // 給_objc_super分配內存(聲明一個struct._objc_super變量)
store %0* %0, %0** %3, align 8 // 將第一個參數,id self 寫入%3分配的內存中去
store i8* %1, i8** %4, align 8 // 將_cmd寫入%4分配的內存中區
%6 = load %0*, %0** %3, align 8 // 讀出%3內存中的數據到%6這個臨時變量(%3中存的是self)
%7 = bitcast %0* %6 to i8* // 將%6變量的類型轉換爲char *指針類型,指向的仍是self
%8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0 // 取struct._objc_super變量(%5)中的第0個元素,聲明爲%8
store i8* %7, i8** %8, align 8 // 將%7存入%8這個變量中,即把i8* 類型的 self存入告終構體第0個元素中
%9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8 // 聲明%9臨時變量爲struct._class_t*類型,內容爲@"OBJC_CLASSLIST_SUP_REFS_$_"
%10 = bitcast %struct._class_t* %9 to i8* // 將%9的變量強轉爲char *類型
%11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1 // 取struct._objc_super變量(%5)中的第1個元素,聲明爲%11
store i8* %10, i8** %11, align 8 // 將%9的變量,即@"OBJC_CLASSLIST_SUP_REFS_$_"存入結構體第1個元素中
%12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !7 // 將@selector(eat)的引用放入char *類型的%12變量中
// 函數調用,傳入參數爲上述生成的struct._objc_super結構體和 @selector(eat),調用函數objc_msgSendSuper2
call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12)
ret void
}
@"OBJC_CLASS_$_Son" = global %struct._class_t {
%struct._class_t* @"OBJC_METACLASS_$_Son",
%struct._class_t* @"OBJC_CLASS_$_Father",
%struct._objc_cache* @_objc_empty_cache,
i8* (i8*, i8*)** null,
%struct._class_ro_t* @"\01l_OBJC_CLASS_RO_$_Son"
}, section "__DATA, __objc_data", align 8
// 直接存放進入struct._objc_super的變量, 內容爲@"OBJC_CLASS_$_Son"
@"OBJC_CLASSLIST_SUP_REFS_$_" = private global %struct._class_t* @"OBJC_CLASS_$_Son", section "__DATA, __objc_superrefs, regular, no_dead_strip", align 8複製代碼
IR的語法其實不難記,仍是比較好懂的。這裏咱們只要對照着看便可:
代碼中既有彙編的趕腳,又有高級語言的味道。基本上註釋都補全了,代碼中的邏輯和上文中咱們本身實現的/clang重寫的代碼基本類似。可是這裏注意@"OBJC_CLASSLIST_SUP_REFS_$_"
這個變量。
@"OBJC_CLASSLIST_SUP_REFS_$_"
其實就是對應到struct objc_super
結構中的第二個元素:super_class
。在IR代碼的%11以及後面那一行就是體現。
而@"OBJC_CLASSLIST_SUP_REFS_$_"
的定義就是@"OBJC_CLASS_$_Son"
這個全局變量。@"OBJC_CLASS_$_Son"
全局變量就是Son這個類對象,裏面包含了元類:@"OBJC_METACLASS_$_Son"
,以及父類:@"OBJC_CLASS_$_Father"
,以及其餘的一些數據。然而,看到這裏,咱們發現這和咱們本身實現的super
,以及clang重寫的super
都不同:這裏是直接將[Son class]
做爲struct objc_super
的super_class
,可是並無任何調用class_getSuperclass
的地方...
可是,這裏惟一的一個函數@objc_msgSendSuper2
貌似不同凡響,與咱們以前看到的objc_msgSendSuper
相比多了個2,難道是這個函數在做鬼?那就讓咱們到官方的objc4-709源碼裏查詢下這個函數(位於objc-msg-arm64.s
文件中):
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START
ldp x0, x16, [x0] // x0 = real receiver, x16 = class
ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
CacheLookup NORMAL
END_ENTRY _objc_msgSendSuper2複製代碼
這是一段彙編代碼,沒錯,蘋果爲了提升運行效率,發送消息相關的函數是直接用匯編實現的。
這裏咱們來簡單分析下這個函數:
ldp x0, x16, [x0]
:從x0出讀取兩個字數據到x0與x16中,根據註釋,讀取的數據應該是對應的self
與[Son class]
。ldr x16, [x16, #SUPERCLASS]
:將x16的數值+SUPERCLASS值的偏移做爲地址,取出該地址的數值保存在x16中。這裏的SUPERCLASS
定義是#define SUPERCLASS 8
,也就是偏移8位,那麼取到的應該就是@"OBJC_CLASS_$_Father"
這個父類[Father class]
到x16中。CacheLookup
函數,參數爲NORMAL。讓咱們看看CacheLookup
的定義:
/******************************************************************** * * CacheLookup NORMAL|GETIMP|LOOKUP * * Locate the implementation for a selector in a class method cache. * * Takes: * x1 = selector * x16 = class to be searched * * Kills: * x9,x10,x11,x12, x17 * * On exit: (found) calls or returns IMP * with x16 = class, x17 = IMP * (not found) jumps to LCacheMiss * ********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro複製代碼
具體的CacheLookup
咱們這裏就再也不展開了,咱們只關心這裏是從哪裏查找方法的。在註釋中,明確說到這是一個「去類的方法緩存中尋找方法實現」的函數,參入的參數是x1中的selector,x16中的class(class to be searched 就是說從這個類中開始查找),而這時候的x16,偏偏是咱們剛纔在_objc_msgSendSuper2
存入的父類[Father class]
,所以,方法會從這個類中開始查找。
從手動實現->查看clang重寫->查看IR碼->查看彙編源碼這幾個過程分析下來,咱們總算是把這條真實的super
調用鏈路搞搞清楚了:
struct._objc_super
結構體, 結構體中self
爲接收對象,直接指明自身的類爲結構體第二個class類型的值。_objc_msgSendSuper2
函數,傳入上述struct._objc_super
結構體。_objc_msgSendSuper2
函數中直接經過偏移量直接查找父類。CacheLookup
函數去父類中查找指定方法。因此,從真實的IR代碼中,super
關鍵字實際上是直接指明本類Son,再結合_objc_msgSendSuper2
函數直接獲取父類去查找方法的,而並不是像clang重寫的那樣,指明本類,再經過runtime查找父類。
其實先指明本類,再經過runtime查找父類,也是沒有問題的,這還能夠避免一些運行時「更改父類」的狀況。可是LLVM的作法應該是有他的道理的,多是出於性能考慮?