本篇文章講的是super的實際運作原理,如有同學對super與self的區分還有疑惑的,請參考ChenYilong大神的《招聘一個靠譜的iOS》面試題參考答案(上)。
super究竟在幹什麼?
官方提到的super關鍵字?
開啟蘋果API文件,搜尋objc_msgSendSuper
(對該函式陌生的先去補補rumtime)。
裡面明確提到了使用super
關鍵字傳送訊息會被編譯器轉化為呼叫objc_msgSendSuper
以及相關函式(由返回值決定)。
再讓我們看看該函式的定義(這是文件中的定義):
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
。
手動實現super關鍵字
讓我們先定義兩個類:
這是父類: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
),輸出了結果。
1. 下面,我們來自己實現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了,如圖:
再看看相關執行緒中的方法呼叫:
這是一個死迴圈,所以系統讓該段程式碼強制停止了。可為什麼這裡會構成死迴圈呢?讓我們好好分析分析:
- Grandson中沒有實現eat方法,所以main函式中Grandson的例項執行eat方法是這樣的:根據類繼承關係自下而上尋找,在Grandson的父類Son類中找到了eat方法,進行呼叫。
- 在Son的eat方法的實現中,我們構建了一個
superReceiver
結構體,內部包含了self
以及[self superclass]
。在呼叫過程中,self指代的應是Grandson例項,也就是grandson這個變數,那麼[self superclass]
方法返回值也就是Son這個類。 - 根據第2點的分析,以及我們在文章開頭的文件中,蘋果指出
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"就是我們在程式碼裡寫死的。
先不談這個疑問,我們來分析這段程式碼:
- Grandson中沒有實現eat方法,所以main函式中Grandson的例項執行eat方法是這樣的:根據類繼承關係自下而上尋找,在Grandson的父類Son類中找到了eat方法,進行呼叫。
- 在Son的eat方法的實現中,我們構建了一個
superReceiver
結構體,內部包含了self
以及Father
這個類。 objc_msgSendSuper
函式直接去Father類中尋找eat
方法的實現,並執行(輸出)。
現在這段程式碼是以正常邏輯執行的。
2. [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不可靠
為何clang不可靠
clang的rewrite功能所提供的重寫後的程式碼並非編譯器(LLVM)轉換後的程式碼,如今的編譯器在Xcode開啟bitcode功能後會生成一種中間程式碼:LLVM Intermediate Representation(LLVM IR)。該程式碼向上可統一大部分高階語言,向下可支援多種不同架構的CPU,具體可檢視LLVM文件。所以我們的目標是從IR程式碼求證super
究竟在做什麼事!
檢視IR程式碼
終端裡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的語法其實不難記,還是比較好懂的。這裡我們只要對照著看即可:
- %1,%2,@xxx之類的都是指代變數,理解為變數名就可以了
- i8指8位的int型別,即1個位元組的char型別。i8就是指char 指標
- alloca指分配記憶體,理解為宣告一個變數即可,如alloca i8即為一個char 的變數
- %0在開頭的程式碼裡說明了是一個不透明的型別,所以%0*就指代一個萬能指標,理解為id即可
- store為寫入記憶體
- load為從記憶體中讀取出來
- bitcast為型別轉換
- getelementptr inbounds取指定記憶體偏移
程式碼中既有彙編的趕腳,又有高階語言的味道。基本上註釋都補全了,程式碼中的邏輯和上文中我們自己實現的/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的做法應該是有他的道理的,可能是出於效能考慮?