Runtime的本質4-super調用的本質

1. super的本質

1.1 問題

首先來看一道面試題:

// 下列代碼中Person繼承自NSObject,Student繼承自Person,寫出下列代碼輸出內容。

#import "Student.h"
@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]);
        NSLog(@"[self superclass] = %@", [self superclass]);
        NSLog(@"----------------");
        NSLog(@"[super class] = %@", [super class]);
        NSLog(@"[super superclass] = %@", [super superclass]);

    }
    return self;
}
@end

輸出:

2020-02-13 15:45:13.848176+0800 Runtime的本質4[30347:13168682] [self class] = Student
2020-02-13 15:45:13.848628+0800 Runtime的本質4[30347:13168682] [self superclass] = Person
2020-02-13 15:45:13.848718+0800 Runtime的本質4[30347:13168682] ----------------
2020-02-13 15:45:13.848786+0800 Runtime的本質4[30347:13168682] [super class] = Student
2020-02-13 15:45:13.848836+0800 Runtime的本質4[30347:13168682] [super superclass] = Person

上述代碼中可以發現無論是self還是super調用classsuperclass的結果都是相同的。

為什么結果是相同的?super關鍵字在調用方法的時候底層調用流程是怎樣的?

1.2 super調用的本質

我們通過一段代碼來看一下super調用的底層實現,為Person類提供run方法,Student類中重寫run方法,方法內部調用[super run];,將Student.m轉化為c++代碼查看其底層實現:

- (void) run
{
    [super run];
    NSLog(@"Student...");
}

上述代碼轉化為c++代碼

static void _I_Student_run(Student * self, SEL _cmd) {
    
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("run"));
    
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_Student_e677aa_mi_0);
}

通過上述源碼可以發現,[super run]轉化為底層源碼內部其實調用的是objc_msgSendSuper函數。

objc_msgSendSuper函數內傳遞了兩個參數:

  1. __rw_objc_super結構體
  2. sel_registerName("run")方法名。

__rw_objc_super結構體內傳入的參數是selfclass_getSuperclass(objc_getClass("Student"))

class_getSuperclass也就是Student的父類Person

首先我們找到objc_msgSendSuper函數查看內部結構:

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

可以發現objc_msgSendSuper中傳入的結構體是objc_super,我們來到objc_super內部查看其內部結構。

我們通過源碼查找objc_super結構體查看其內部結構:

// 精簡后的objc_super結構體
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接受者的父類
    /* super_class is the first class to search */ 
    // 父類是第一個開始查找的類
};

objc_super結構體中可以發現receiver消息接受者仍然為selfsuper_class僅僅是用來告知消息查找從哪一個類開始,是直接從父類的類對象開始去查找。

我們通過一張圖看一下其中的區別:

runtime_selfsuper

從上圖中我們知道

super調用方法的消息接受者receiver仍然是self,只是從父類Person的類對象開始去查找方法。

class方法的實現

那么此時重新回到面試題,我們知道class的底層實現如下面代碼所示:

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

class方法內部實現是根據消息接受者返回其對應的類對象,最終會找到基類NSObject的方法列表中。

selfsuper的區別僅僅是self從本類類對象開始查找方法,super從父類類對象開始查找方法,它兩的消息接受者都是一樣的,在這兒消息接收者都是self,因此最終得到的結果都是相同的。

另外我們在回到run方法內部,很明顯可以發現,如果super不是從父類開始查找方法,從本類查找方法的話,就調用方法本身造成循環調用方法而crash

superclass方法的實現

同理superclass底層實現同class類似,其底層實現代碼如下入所示

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

因此得到的結果也是相同的。

2. isKindOfClassisMemberOfClass

isKindOfClass isKindOfClass實例方法底層實現:

- (BOOL)isMemberOfClass:(Class)cls {
   // 直接獲取調用者實例類對象并判斷是否等于傳入的類對象
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
   // 向上查詢調用者父類對象等于傳入的類對象則返回YES
   // 直到基類還不相等則返回NO
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

isKindOfClass isKindOfClass類方法底層實現:

// 判斷調用者元類對象是否等于傳入的元類元類對象
// 此時self是類對象 object_getClass((id)self)獲取到的就是元類
+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

// 向上查找調用者元類對象是否等于傳入的元類對象
// 如果找到基類還不相等則返回NO
// 注意:這里會找到基類
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

通過上述源碼分析我們可以知道:

  1. isMemberOfClass 判斷左邊類對象或者元類對象是否剛好等于右邊類型。
  2. isKindOfClass 判斷左邊或者左邊類型的父類是否剛好等于右邊類型(右邊的類型是否是左邊類型的子類)。

注意:類方法內部是獲取其元類對象進行比較

我們查看以下代碼

NSLog(@"%d",[Person isKindOfClass: [Person class]]);
NSLog(@"%d",[Person isKindOfClass: object_getClass([Person class])]);
NSLog(@"%d",[Person isKindOfClass: [NSObject class]]);

//輸出:
2020-02-13 17:30:38.439190+0800 Runtime的本質4[38056:13260008] 0
2020-02-13 17:30:38.439242+0800 Runtime的本質4[38056:13260008] 1
2020-02-13 17:30:38.439313+0800 Runtime的本質4[38056:13260008] 1

分析上述輸出內容:

  1. 第一個 0:上面提到過類方法是獲取self的元類對象與傳入的參數進行比較,但是[Person class]獲取到的是類對象,因此返回NO
  2. 第二個 1:同上,此時我們傳入Person元類對象,此時返回YES。驗證上述說法
  3. 第三個 1:我們發現此時傳入[NSObject class]的是NSObject類對象并不是元類對象,但是返回的值卻是YES
    原因是基元類的superclass指針是指向基類對象NSObject的。如下圖13號線
isa_index_pointer

那么Person元類通過superclass指針一直找到基元類,還是不相等,此時再次通過superclass指針來到基類,那么此時發現相等就會返回YES了。

通過以上驗證我看可以總結出如下的用法:

  1. 通過實例方法來調用,傳入的參數應該是類對象
  2. 通過類方法來調用,傳入的參數應該是元類對象

3. 函數棧的內存分布

通過一道面試題對之前學習的知識進行復習。

//以下代碼是否可以執行成功,如果可以,打印結果是什么。

// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)test;
@end

// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
    NSLog(@"test print name is : %@", self.name);
}
@end

// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}

這道面試題確實很無厘頭的一道題,日常工作中沒有人這樣寫代碼,但是需要解答這道題需要很完備的底層知識,我們通過這道題來復習一下,首先看一下打印結果。

2020-02-13 17:43:09.307047+0800 Runtime的本質4[38983:13272769] test print name is : <ViewController: 0x7faa8b509a00>
2020-02-13 17:43:09.307180+0800 Runtime的本質4[38983:13272769] test print name is : (null)

通過上述打印結果我們可以看出,是可以正常運行并打印的,說明obj可以正常調用test方法,但是我們發現打印self.namee的內容卻是<ViewController: 0x7f95514077a0>。下面person實例調用test不做過多解釋了,主要用來和上面方法調用做對比。

為什么會是這樣的結果呢?首先通過一張圖看一下兩種調用方法的內存信息。

runtime_memory_review

通過上圖我們可以發現兩種方法調用方式很相近。那么obj為什么可以正常調用方法?

obj為什么可以正常調用方法?

首先通過之前的學習我們知道,person調用方法時首先通過isa指針找到類對象進而查找方法并進行調用。

person實例對象內實際上是取最前面8個字節空間(指針類型在64位占8字節)也就是isa,并通過計算得出類對象地址。

而通過上圖我們可以發現,obj在調用test方法時,也會通過其內存地址找到cls,而cls中取出最前面8個字節空間其內部存儲的剛好是Person類對象地址。因此obj是可以正常調用方法的。

super調用的時候會創建局部變量

問題出在[super viewDidLoad]這段代碼中,通過上述對super本質的分析我們知道,super內部調用objc_msgSendSuper2函數。

我們知道objc_msgSendSuper2函數內部會傳入兩個參數,objc_super2結構體和SEL,并且objc_super2結構體內有兩個成員變量消息接受者和其父類。

struct objc_super2 {
    id receiver; // 消息接受者
    Class current_class; // 當前類
};

通過以上分析我們可以得知[super viewDidLoad]內部objc_super2結構體內存儲如下所示

struct objc_super2 = {
    self,
    [ViewController Class]
};

那么objc_msgSendSuper2函數調用之前,會先創建局部變量結構體objc_super2結構體,用于為objc_msgSendSuper2函數傳遞的參數。

為什么會調用objc_msgSendSuper2函數,后面會證明。

3.1 函數局部變量由高地址向低地址分配在棧空間

局部變量是存儲在棧空間內的,并且是由高地址向低地址有序存儲。

我們通過一段代碼驗證一下。

long long a = 1;
long long b = 2;
long long c = 3;
NSLog(@"%p %p %p", &a,&b,&c);

// 輸出:
0x7ffee36dfc20 0x7ffee36dfc18 0x7ffee36dfc10

通過上述代碼打印內容,我們可以驗證局部變量在棧空間內是由高地址向低地址連續存儲的。

那么我們回到題中,通過上述分析我們知道,此時代碼中包含局部變量依次為:objc_super2 結構體、clsobj。通過一張圖展示一下這些局部變量存儲結構。

runtime_super_review

上面我們知道當person實例對象調用方法的時候,會取實例變量前8個字節空間也就是isa來找到類對象地址。那么當訪問實例變量的時候,就跳過isa的8個字節空間往高地址去找實例變量。

那么當obj在調用test方法的時候同樣找到cls中取出前8個字節,也就是Person類對象的內存地址,那么當訪問成員變量_name的時候,會繼續向高地址內存空間查找,此時就會找到objc_super結構體,從中取出8個字節空間也就是self,因此此時訪問到的self.name就是ViewController對象。

當訪問成員變量_name的時候,test函數中的self也就是方法調用者其實是obj,那么self.name就是通過obj去找_name,跳過cls的8個指針,再取8個指針,此時自然獲取到ViewController對象。

因此上述代碼中cls就相當于isaisa下面的8個字節空間就相當于_name成員變量。因此成員變量_name的訪問到的值就是cls地址后向高地址位取8個字節地址空間存儲的值。

為了驗證上述說法,我們做一個實驗,在cls后高地址中添加一個string,那么此時cls下面的高地址位就是string。以下示例代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *string = @"string";
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}

此時的局部變量內存結構如下圖所示

runtime_super_review1

此時在訪問_name成員變量的時候,越過cls內存往高地址找就會來到stringstring也是指針類型,占用8字節),此時拿到的成員變量就是string了。

輸出:

2020-02-13 18:50:45.454119+0800 Runtime的本質4[44051:13338753] test print name is : string
2020-02-13 18:50:45.454212+0800 Runtime的本質4[44051:13338753] test print name is : (null)
Tips:

通過之前的底層學習,我們知道了一個OC的函數在底層默認是有兩個參數self_cmd的,但是在這兒確沒有入棧空間,這是因為arm64架構下,函數的參數一般都是通過寄存器來存儲的,通過寄存器來操作內存效率會更高。

3.2 其他情況

再通過一段代碼使用int數據進行試驗

- (void)viewDidLoad {
    [super viewDidLoad];

    int a = 3;
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}
// 程序crash,壞地址訪問

我們發現程序因為壞地址訪問而crash,此時局部變量內存結構如下圖所示

runtime_super_review2

當需要訪問_name成員變量的時候,會在cls后的高地址查找8位的字節空間,而我們知道int占4位字節,那么此時8位的內存空間同時占據int數據及objc_super結構體內,因此就會造成壞地址訪問而crash

我們添加新的成員變量進行訪問

// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;
- (void)test;
@end
------------
// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
    NSLog(@"test print name is : %@", self.nickName);
}
@end
--------
//  ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    NSObject *obj1 = [[NSObject alloc] init];
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}

輸出:

2020-02-13 19:04:55.572495+0800 Runtime的本質4[45176:13357077] test print name is : <ViewController: 0x7f8672f08c30>
2020-02-13 19:04:55.572609+0800 Runtime的本質4[45176:13357077] test print name is : (null)

可以發現此時打印的仍然是ViewController對象,我們先來看一下其局部變量內存結構

runtime_super_review3

首先通過obj找到clscls找到類對象進行方法調用,此時在訪問nickName時,obj查找成員變量,首先跳過8字節的cls,之后跳過第一個成員變量_name所占的8字節空間,最終再取8字節空間取出其中的值作為成員變量的值,那么此時也就是self了。

為什么會跳過_name的8個字節呢?

因為Person類里面有2個屬性namenickName,現在訪問的是nickNamename在低地址、nickname在高地址

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;

- (void)test {
    NSLog(@"test print name is : %@", self.nickName);
}

現在我們把為Person類里面有2個屬性交換位置

@property (nonatomic, strong) NSString *nickName;
@property (nonatomic, strong) NSString *name;

- (void)test {
    NSLog(@"test print name is : %@", self.nickName);
}

輸出:

2020-02-13 19:29:43.272317+0800 Runtime的本質4[47065:13384489] test print name is : <NSObject: 0x600003edc690>
2020-02-13 19:29:43.272413+0800 Runtime的本質4[47065:13384489] test print name is : (null)

我們發現直接輸出了obj1對象,原因是當跳過了8個字節的cls之后,會直接取8個字節空間取出其中的值作為nickName的值,因為現在的nickName在低地址。

現在我們再修改test方法如下:

- (void)test {
    NSLog(@"test print name is : %@", self.name);
}

輸出:

2020-02-13 19:34:38.304233+0800 Runtime的本質4[47437:13389601] test print name is : <ViewController: 0x7f7f08d07050>
2020-02-13 19:34:38.304346+0800 Runtime的本質4[47437:13389601] test print name is : (null)

我們發現又打印出了ViewController,原因上面已經說明了。

3.3 總結

總結:這道面試題雖然很無厘頭,讓人感覺無從下手但是考察的內容非常多。

  1. super的底層本質為調用objc_msgSendSuper2函數,傳入objc_super2結構體,結構體內部存儲消息接受者和當前類,用來告知系統方法查找從父類開始。

  2. 局部變量分配在棧空間,并且從高地址向低地址連續分配。先創建的局部變量分配在高地址,后續創建的局部變量連續分配在較低地址。

  3. 方法調用的消息機制,通過isa指針找到類對象進行消息發送。

  4. 指針存儲的是實例變量的首字節地址,上述例子中person指針存儲的其實就是實例變量內部的isa指針的地址。

  5. 訪問成員變量的本質,找到成員變量的地址,按照成員變量所占的字節數,取出地址中存儲的成員變量的值。

4. super調用的更深入探究

4.1 objc_msgSendSuper2函數

我們在第2節的時候知道了通過super調用的時候,OC代碼轉化為c++代碼底層會調用objc_msgSendSuper

// 精簡后的objc_super結構體
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接受者的父類
    /* super_class is the first class to search */ 
    // 父類是第一個開始查找的類
};

但是這并不能說明super底層調用函數就一定objc_msgSendSuper

其實super底層真正調用的函數是objc_msgSendSuper2函數我們可以通過查看super調用方法轉化為匯編代碼來驗證這一說法:

- (void)viewDidLoad {
    [super viewDidLoad];
}

通過斷點查看其匯編調用棧

runtime_msgsendsuper2

上圖中可以發現super底層其實調用的是objc_msgSendSuper2函數,我們來到源碼中查找一下objc_msgSendSuper2函數的底層實現,我們可以在匯編文件中找到其相關底層實現。

objc源碼路徑:https://opensource.apple.com/source/objc4/objc4-756.2/runtime/Messengers.subproj/objc-msg-arm64.s.auto.html

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame

ldp p0, p16, [x0]       // p0 = real receiver, p16 = class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2

通過上面匯編代碼我們可以發現,其實底層是在函數內部調用的class->superclass獲取父類,并不是我們上面分析的直接傳入的就是父類的類對象。

其實_objc_msgSendSuper2內傳入的結構體為objc_super2
struct objc_super2 {
    id receiver;
    Class current_class;
};

我們可以發現objc_super2中除了消息接受者receiver,另一個成員變量current_class也就是當前類對象。

與我們上面分析的不同,_objc_msgSendSuper2函數內其實傳入的是當前類對象,然后在函數內部獲取當前類對象的父類,并且從父類開始查找方法。

4.2 證明結構體objc_super2內部傳入的是當前類,而不是父類

我們也可以通過代碼驗證上述結構體內成員變量究竟是當前類對象還是父類對象。下文中我們會通過另外一道面試題驗證。

我們使用以下代碼來驗證上文中遺留的問題

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
}

上述代碼的局部變量內存結構我們之前已經分析過了,真正的內存結構應該如下圖所示

runtime_super_review4

通過上面對面試題的分析,我們現在想要驗證objc_msgSendSuper2函數內傳入的結構體參數,只需要拿到cls的地址,然后向后移8個地址就可以獲取到objc_super結構體內的self,在向后移8個地址就是current_class的內存地址。通過打印current_class的內容,就可以知道傳入objc_msgSendSuper2函數內部的是當前類對象還是父類對象了。

runtime_super_review5

通過上圖可以發現,最終打印的內容確實為當前類對象。
因此objc_msgSendSuper2函數內部其實傳入的是當前類對象,并且在函數內部獲取其父類,告知系統從父類方法開始查找的。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。