iOS super關(guān)鍵字幫我們做了什么?

本篇文章講的是super的實(shí)際運(yùn)作原理,如有同學(xué)對(duì)super與self的區(qū)分還有疑惑的,請(qǐng)參考ChenYilong大神的《招聘一個(gè)靠譜的iOS》面試題參考答案(上)。

super究竟在干什么?

官方提到的super關(guān)鍵字?

打開(kāi)蘋(píng)果API文檔,搜索objc_msgSendSuper(對(duì)該函數(shù)陌生的先去補(bǔ)補(bǔ)rumtime)。

super官方解釋

里面明確提到了使用super關(guān)鍵字發(fā)送消息會(huì)被編譯器轉(zhuǎn)化為調(diào)用objc_msgSendSuper以及相關(guān)函數(shù)(由返回值決定)。

再讓我們看看該函數(shù)的定義(這是文檔中的定義):

id objc_msgSendSuper(struct objc_super *super, SEL op, ...);

這里的super已經(jīng)不再是我們調(diào)用時(shí)寫(xiě)的[super init]super了,這里指代的是struct objc_super結(jié)構(gòu)體指針。文檔中明確指出,該結(jié)構(gòu)體需要包含接收消息的實(shí)例以及一開(kāi)始尋找方法實(shí)現(xiàn)的父類

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 */
};
objc_super結(jié)構(gòu)體

既然知道了super是如何調(diào)用的,那么我們來(lái)嘗試自己實(shí)現(xiàn)一個(gè)super。

手動(dòng)實(shí)現(xiàn)super關(guān)鍵字

讓我們先定義兩個(gè)類:

這是父類: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類重寫(xiě)了父類的eat方法,里面只做一件事,就是調(diào)用父類的eat方法。

讓我們?cè)趍ain中開(kāi)始進(jìn)行測(cè)試:

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

到這里沒(méi)毛病,一個(gè)Son對(duì)象調(diào)用了eat方法(內(nèi)部調(diào)用父類的eat),輸出了結(jié)果。

1. 下面,我們來(lái)自己實(shí)現(xiàn)super的效果:

改寫(xiě)Son.m:

// Son.m

- (void)eat {
//    [super eat];
    
    struct objc_super superReceiver = {
        self,
        [self superclass]
    };
    objc_msgSendSuper(&superReceiver, _cmd);    
}

運(yùn)行我們的main函數(shù):

//輸出
2017-05-14 22:47:00.109379+0800 TestSuper[7417:3790621] Father eat

沒(méi)毛病,我們可是根據(jù)官方文檔來(lái)實(shí)現(xiàn)super的效果。

難道super真的就是如此?

讓我們持懷疑的態(tài)度看看下面這個(gè)例子:

在這里,我們又有個(gè)Son的子類出現(xiàn)了:Grandson類

// Grandson.h
@interface Grandson : Son

@end
  
// Grandson.m
@implementation Grandson

@end

該類啥什么都沒(méi)實(shí)現(xiàn),純粹繼承自Son。

然后讓我們改寫(xiě)main函數(shù):

int main(int argc, char * argv[]) {
    Grandson *grandson = [Grandson new];
    [grandson eat];
}

運(yùn)行起來(lái),過(guò)一會(huì)就crash了,如圖:

崩潰提示

再看看相關(guān)線程中的方法調(diào)用:

crash方法調(diào)用

這是一個(gè)死循環(huán),所以系統(tǒng)讓該段代碼強(qiáng)制停止了。可為什么這里會(huì)構(gòu)成死循環(huán)呢?讓我們好好分析分析:

  1. Grandson中沒(méi)有實(shí)現(xiàn)eat方法,所以main函數(shù)中Grandson的實(shí)例執(zhí)行eat方法是這樣的:根據(jù)類繼承關(guān)系自下而上尋找,在Grandson的父類Son類中找到了eat方法,進(jìn)行調(diào)用。
  2. 在Son的eat方法的實(shí)現(xiàn)中,我們構(gòu)建了一個(gè)superReceiver結(jié)構(gòu)體,內(nèi)部包含了self以及[self superclass]。在調(diào)用過(guò)程中,self指代的應(yīng)是Grandson實(shí)例,也就是grandson這個(gè)變量,那么[self superclass]方法返回值也就是Son這個(gè)類。
  3. 根據(jù)第2點(diǎn)的分析,以及我們?cè)谖恼麻_(kāi)頭的文檔中,蘋(píng)果指出superReceiver中的父類就是開(kāi)始尋找方法實(shí)現(xiàn)的那個(gè)父類,我們可以得出,此時(shí)的objc_msgSendSuper(&superReceiver, _cmd)函數(shù)調(diào)用的方法實(shí)現(xiàn)即是Son類中的eat方法的實(shí)現(xiàn)。即,構(gòu)成了遞歸。

既然這里不能使用superclass方法,那么我們要如何自己實(shí)現(xiàn)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中要尋找方法實(shí)現(xiàn)的父類:Father。這里必定有人會(huì)問(wèn):這樣子豈不是每個(gè)調(diào)用[super xxxx]的地方都需要直接指明父類?

“直接指明”的意思是,代碼中直接寫(xiě)出這個(gè)類,比如直接寫(xiě):[Father class]或者objc_getClass("Father"),這里面的Father與"Father"就是我們?cè)诖a里寫(xiě)死的。

先不談這個(gè)疑問(wèn),我們來(lái)分析這段代碼:

  1. Grandson中沒(méi)有實(shí)現(xiàn)eat方法,所以main函數(shù)中Grandson的實(shí)例執(zhí)行eat方法是這樣的:根據(jù)類繼承關(guān)系自下而上尋找,在Grandson的父類Son類中找到了eat方法,進(jìn)行調(diào)用。
  2. 在Son的eat方法的實(shí)現(xiàn)中,我們構(gòu)建了一個(gè)superReceiver結(jié)構(gòu)體,內(nèi)部包含了self以及Father這個(gè)類。
  3. objc_msgSendSuper函數(shù)直接去Father類中尋找eat方法的實(shí)現(xiàn),并執(zhí)行(輸出)。

現(xiàn)在這段代碼是以正常邏輯執(zhí)行的。

2. [super xxxx]真的要直接指明父類?

我們使用clang的rewrite指令重寫(xiě)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"));
}

這一行到底的代碼可讀性太差,讓我們稍稍分解下(由于語(yǔ)法問(wèn)題我們作了少量語(yǔ)法修改以通過(guò)編譯,實(shí)際作用與原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運(yùn)行起來(lái):

// Son.m

- (void)eat {
//    [super eat];
    
  //_I_Son_eat即為重寫(xiě)的函數(shù)
    _I_Son_eat(self, _cmd);
}

// 輸出
2017-05-15 00:08:37.782519+0800 TestSuper[7460:3810248] Father eat

沒(méi)有毛病。

重寫(xiě)的代碼里構(gòu)建了一個(gè)__rw_objc_super的結(jié)構(gòu)體,定義如下:

struct __rw_objc_super { 
    struct objc_object *object; 
    struct objc_object *superClass; 
    // cpp里的語(yǔ)法,忽略即可
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};

該結(jié)構(gòu)體與struct objc_super一致。之后我們將objc_msgSendSuper函數(shù)轉(zhuǎn)換為指定參數(shù)的函數(shù)func進(jìn)行調(diào)用。這里請(qǐng)注意__rw_objc_super superReceiver中的第二個(gè)值class_getSuperclass(objc_getClass("Son"))。

該代碼直接指明的類是本類:Son類。但是__rw_objc_super結(jié)構(gòu)體中的superClass并不是本類,而是通過(guò)runtime查找出的父類。這與我們自己實(shí)現(xiàn)的 “直接指明Father為objc_super結(jié)構(gòu)體的super_class值” 最后達(dá)到的效果是一樣的。

所以,[super xxxx]肯定要通過(guò)指明一個(gè)類,可以是父類,也可以是本類,來(lái)達(dá)到正確調(diào)用父類方法的目的!只不過(guò)“直接指明”這件事,編譯器會(huì)幫我們搞定,我們只管寫(xiě)super即可。

clang rewrite不可靠

為何clang不可靠

clang的rewrite功能所提供的重寫(xiě)后的代碼并非編譯器(LLVM)轉(zhuǎn)換后的代碼,如今的編譯器在Xcode開(kāi)啟bitcode功能后會(huì)生成一種中間代碼:LLVM Intermediate Representation(LLVM IR)。該代碼向上可統(tǒng)一大部分高級(jí)語(yǔ)言,向下可支持多種不同架構(gòu)的CPU,具體可查看LLVM文檔。所以我們的目標(biāo)是從IR代碼求證super究竟在做什么事!

查看IR代碼

終端里cd到Son.m文件所在目錄,執(zhí)行:

clang -emit-llvm Son.m -S -o son.ll

生成的IR代碼比較多,我們挑重點(diǎn)進(jìn)行查看:

%0 = type opaque

// Son的eat方法
define internal void @"\01-[Son eat]"(%0*, i8*) #0 {
  %3 = alloca %0*, align 8    // 分配一個(gè)指針的內(nèi)存,8字節(jié)對(duì)齊(聲明一個(gè)指針變量)
  %4 = alloca i8*, align 8    // 分配一個(gè)char *的內(nèi)存(聲明一個(gè)char *指針變量)
  %5 = alloca %struct._objc_super, align 8    // 給_objc_super分配內(nèi)存(聲明一個(gè)struct._objc_super變量)
  store %0* %0, %0** %3, align 8    // 將第一個(gè)參數(shù),id self 寫(xiě)入%3分配的內(nèi)存中去
  store i8* %1, i8** %4, align 8    // 將_cmd寫(xiě)入%4分配的內(nèi)存中區(qū)
  %6 = load %0*, %0** %3, align 8   // 讀出%3內(nèi)存中的數(shù)據(jù)到%6這個(gè)臨時(shí)變量(%3中存的是self)
  %7 = bitcast %0* %6 to i8*        // 將%6變量的類型轉(zhuǎn)換為char *指針類型,指向的還是self
  %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0    // 取struct._objc_super變量(%5)中的第0個(gè)元素,聲明為%8
  store i8* %7, i8** %8, align 8    // 將%7存入%8這個(gè)變量中,即把i8* 類型的 self存入了結(jié)構(gòu)體第0個(gè)元素中
  %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8    // 聲明%9臨時(shí)變量為struct._class_t*類型,內(nèi)容為@"OBJC_CLASSLIST_SUP_REFS_$_"
  %10 = bitcast %struct._class_t* %9 to i8*   // 將%9的變量強(qiáng)轉(zhuǎn)為char *類型
  %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1   // 取struct._objc_super變量(%5)中的第1個(gè)元素,聲明為%11
  store i8* %10, i8** %11, align 8    // 將%9的變量,即@"OBJC_CLASSLIST_SUP_REFS_$_"存入結(jié)構(gòu)體第1個(gè)元素中
  %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !7    // 將@selector(eat)的引用放入char *類型的%12變量中

  // 函數(shù)調(diào)用,傳入?yún)?shù)為上述生成的struct._objc_super結(jié)構(gòu)體和 @selector(eat),調(diào)用函數(shù)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

// 直接存放進(jìn)入struct._objc_super的變量, 內(nèi)容為@"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的語(yǔ)法其實(shí)不難記,還是比較好懂的。這里我們只要對(duì)照著看即可:

  • %1,%2,@xxx之類的都是指代變量,理解為變量名就可以了
  • i8指8位的int類型,即1個(gè)字節(jié)的char類型。i8*就是指char *指針
  • alloca指分配內(nèi)存,理解為聲明一個(gè)變量即可,如alloca i8*即為一個(gè)char *的變量
  • %0在開(kāi)頭的代碼里說(shuō)明了是一個(gè)不透明的類型,所以%0*就指代一個(gè)萬(wàn)能指針,理解為id即可
  • store為寫(xiě)入內(nèi)存
  • load為從內(nèi)存中讀取出來(lái)
  • bitcast為類型轉(zhuǎn)換
  • getelementptr inbounds取指定內(nèi)存偏移

代碼中既有匯編的趕腳,又有高級(jí)語(yǔ)言的味道?;旧献⑨尪佳a(bǔ)全了,代碼中的邏輯和上文中我們自己實(shí)現(xiàn)的/clang重寫(xiě)的代碼基本相似。但是這里注意@"OBJC_CLASSLIST_SUP_REFS_$_"這個(gè)變量。

@"OBJC_CLASSLIST_SUP_REFS_$_"其實(shí)就是對(duì)應(yīng)到struct objc_super結(jié)構(gòu)中的第二個(gè)元素:super_class。在IR代碼的%11以及后面那一行就是體現(xiàn)。

@"OBJC_CLASSLIST_SUP_REFS_$_"的定義就是@"OBJC_CLASS_$_Son"這個(gè)全局變量。@"OBJC_CLASS_$_Son"全局變量就是Son這個(gè)類對(duì)象,里面包含了元類:@"OBJC_METACLASS_$_Son",以及父類:@"OBJC_CLASS_$_Father",以及其他的一些數(shù)據(jù)。然而,看到這里,我們發(fā)現(xiàn)這和我們自己實(shí)現(xiàn)的super,以及clang重寫(xiě)的super都不一樣:這里是直接將[Son class]作為struct objc_supersuper_class,但是并沒(méi)有任何調(diào)用class_getSuperclass的地方...

查看匯編源碼

但是,這里唯一的一個(gè)函數(shù)@objc_msgSendSuper2貌似與眾不同,與我們之前看到的objc_msgSendSuper相比多了個(gè)2,難道是這個(gè)函數(shù)在作鬼?那就讓我們到官方的objc4-709源碼里查詢下這個(gè)函數(shù)(位于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

這是一段匯編代碼,沒(méi)錯(cuò),蘋(píng)果為了提高運(yùn)行效率,發(fā)送消息相關(guān)的函數(shù)是直接用匯編實(shí)現(xiàn)的。

這里我們來(lái)簡(jiǎn)單分析下這個(gè)函數(shù):

  1. ldp x0, x16, [x0]:從x0出讀取兩個(gè)字?jǐn)?shù)據(jù)到x0與x16中,根據(jù)注釋,讀取的數(shù)據(jù)應(yīng)該是對(duì)應(yīng)的self[Son class]
  2. ldr x16, [x16, #SUPERCLASS]:將x16的數(shù)值+SUPERCLASS值的偏移作為地址,取出該地址的數(shù)值保存在x16中。這里的SUPERCLASS定義是#define SUPERCLASS 8,也就是偏移8位,那么取到的應(yīng)該就是@"OBJC_CLASS_$_Father"這個(gè)父類[Father class]到x16中。
  3. 執(zhí)行CacheLookup函數(shù),參數(shù)為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我們這里就不再展開(kāi)了,我們只關(guān)心這里是從哪里查找方法的。在注釋中,明確說(shuō)到這是一個(gè)“去類的方法緩存中尋找方法實(shí)現(xiàn)”的函數(shù),參入的參數(shù)是x1中的selector,x16中的class(class to be searched 就是說(shuō)從這個(gè)類中開(kāi)始查找),而這時(shí)候的x16,恰恰是我們剛才在_objc_msgSendSuper2存入的父類[Father class],因此,方法會(huì)從這個(gè)類中開(kāi)始查找

整體調(diào)用流程

從手動(dòng)實(shí)現(xiàn)->查看clang重寫(xiě)->查看IR碼->查看匯編源碼這幾個(gè)過(guò)程分析下來(lái),我們總算是把這條真實(shí)的super調(diào)用鏈路搞搞清楚了:

  1. 編譯器指定一個(gè)struct._objc_super結(jié)構(gòu)體, 結(jié)構(gòu)體中self為接收對(duì)象,直接指明自身的類為結(jié)構(gòu)體第二個(gè)class類型的值。
  2. 調(diào)用_objc_msgSendSuper2函數(shù),傳入上述struct._objc_super結(jié)構(gòu)體。
  3. _objc_msgSendSuper2函數(shù)中直接通過(guò)偏移量直接查找父類。
  4. 調(diào)用CacheLookup函數(shù)去父類中查找指定方法。

結(jié)論

所以,從真實(shí)的IR代碼中,super關(guān)鍵字其實(shí)是直接指明本類Son,再結(jié)合_objc_msgSendSuper2函數(shù)直接獲取父類去查找方法的,而并非像clang重寫(xiě)的那樣,指明本類,再通過(guò)runtime查找父類。

其實(shí)先指明本類,再通過(guò)runtime查找父類,也是沒(méi)有問(wèn)題的,這還可以避免一些運(yùn)行時(shí)“更改父類”的情況。但是LLVM的做法應(yīng)該是有他的道理的,可能是出于性能考慮?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,098評(píng)論 0 9
  • 紫草思雨閱讀 217評(píng)論 1 3
  • 1可以存錢做定投。比如你看好的股票是20元/股,但每個(gè)月的結(jié)余就1000,那你可以每個(gè)月攢1000,每?jī)蓚€(gè)月定投一...
    理財(cái)小能手李孝武閱讀 304評(píng)論 0 0
  • 青和我曾同校不同班,以前我們僅限于認(rèn)識(shí)并不熟悉。偶爾狂街遇到,談了很多舊人舊事,她很開(kāi)朗很愛(ài)笑,她告訴我她有個(gè)可愛(ài)...
    sunyang158閱讀 329評(píng)論 2 0

友情鏈接更多精彩內(nèi)容