iOS super 關鍵字幫咱們作了什麼?

本篇文章講的是super的實際運做原理,若有同窗對super與self的區分還有疑惑的,請參考ChenYilong大神的《招聘一個靠譜的iOS》面試題參考答案(上)html

super究竟在幹什麼?

官方提到的super關鍵字?

打開蘋果API文檔,搜索objc_msgSendSuper(對該函數陌生的先去補補rumtime)。c++

super官方解釋

裏面明確提到了使用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 */
};複製代碼

objc_super結構體

既然知道了super是如何調用的,那麼咱們來嘗試本身實現一個superobjective-c

手動實現super關鍵字

讓咱們先定義兩個類: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),輸出告終果。

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了,如圖:

崩潰提示

再看看相關線程中的方法調用:

crash方法調用

這是一個死循環,因此係統讓該段代碼強制中止了。可爲何這裏會構成死循環呢?讓咱們好好分析分析:

  1. Grandson中沒有實現eat方法,因此main函數中Grandson的實例執行eat方法是這樣的:根據類繼承關係自下而上尋找,在Grandson的父類Son類中找到了eat方法,進行調用。
  2. 在Son的eat方法的實現中,咱們構建了一個superReceiver結構體,內部包含了self以及[self superclass]。在調用過程當中,self指代的應是Grandson實例,也就是grandson這個變量,那麼[self superclass]方法返回值也就是Son這個類。
  3. 根據第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"就是咱們在代碼裏寫死的。

先不談這個疑問,咱們來分析這段代碼:

  1. Grandson中沒有實現eat方法,因此main函數中Grandson的實例執行eat方法是這樣的:根據類繼承關係自下而上尋找,在Grandson的父類Son類中找到了eat方法,進行調用。
  2. 在Son的eat方法的實現中,咱們構建了一個superReceiver結構體,內部包含了self以及Father這個類。
  3. 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_supersuper_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複製代碼

這是一段彙編代碼,沒錯,蘋果爲了提升運行效率,發送消息相關的函數是直接用匯編實現的。

這裏咱們來簡單分析下這個函數:

  1. ldp x0, x16, [x0]:從x0出讀取兩個字數據到x0與x16中,根據註釋,讀取的數據應該是對應的self[Son class]
  2. ldr x16, [x16, #SUPERCLASS]:將x16的數值+SUPERCLASS值的偏移做爲地址,取出該地址的數值保存在x16中。這裏的SUPERCLASS定義是#define SUPERCLASS 8,也就是偏移8位,那麼取到的應該就是@"OBJC_CLASS_$_Father"這個父類[Father class]到x16中。
  3. 執行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調用鏈路搞搞清楚了:

  1. 編譯器指定一個struct._objc_super結構體, 結構體中self爲接收對象,直接指明自身的類爲結構體第二個class類型的值。
  2. 調用_objc_msgSendSuper2函數,傳入上述struct._objc_super結構體。
  3. _objc_msgSendSuper2函數中直接經過偏移量直接查找父類。
  4. 調用CacheLookup函數去父類中查找指定方法。

結論

因此,從真實的IR代碼中,super關鍵字實際上是直接指明本類Son,再結合_objc_msgSendSuper2函數直接獲取父類去查找方法的,而並不是像clang重寫的那樣,指明本類,再經過runtime查找父類。

其實先指明本類,再經過runtime查找父類,也是沒有問題的,這還能夠避免一些運行時「更改父類」的狀況。可是LLVM的作法應該是有他的道理的,多是出於性能考慮?

相關文章
相關標籤/搜索