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
調用class
或superclass
的結果都是相同的。
為什么結果是相同的?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
函數內傳遞了兩個參數:
-
__rw_objc_super
結構體 -
sel_registerName("run")
方法名。
__rw_objc_super
結構體內傳入的參數是self
和class_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
消息接受者仍然為self
,super_class
僅僅是用來告知消息查找從哪一個類開始,是直接從父類的類對象開始去查找。
我們通過一張圖看一下其中的區別:
從上圖中我們知道
super
調用方法的消息接受者receiver
仍然是self
,只是從父類Person
的類對象開始去查找方法。
class
方法的實現
那么此時重新回到面試題,我們知道class的底層實現如下面代碼所示:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
class
方法內部實現是根據消息接受者返回其對應的類對象,最終會找到基類NSObject
的方法列表中。
而self
和super
的區別僅僅是self
從本類類對象開始查找方法,super
從父類類對象開始查找方法,它兩的消息接受者都是一樣的,在這兒消息接收者都是self
,因此最終得到的結果都是相同的。
另外我們在回到run
方法內部,很明顯可以發現,如果super
不是從父類開始查找方法,從本類查找方法的話,就調用方法本身造成循環調用方法而crash
。
superclass
方法的實現
同理superclass
底層實現同class
類似,其底層實現代碼如下入所示
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
因此得到的結果也是相同的。
2. isKindOfClass
與 isMemberOfClass
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;
}
通過上述源碼分析我們可以知道:
-
isMemberOfClass
判斷左邊類對象或者元類對象是否剛好等于右邊類型。 -
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
分析上述輸出內容:
- 第一個 0:上面提到過類方法是獲取
self
的元類對象與傳入的參數進行比較,但是[Person class]
獲取到的是類對象,因此返回NO
。 - 第二個 1:同上,此時我們傳入
Person
元類對象,此時返回YES
。驗證上述說法 - 第三個 1:我們發現此時傳入
[NSObject class]
的是NSObject
類對象并不是元類對象,但是返回的值卻是YES
。
原因是基元類的superclass
指針是指向基類對象NSObject
的。如下圖13
號線
那么Person
元類通過superclass
指針一直找到基元類,還是不相等,此時再次通過superclass
指針來到基類,那么此時發現相等就會返回YES
了。
通過以上驗證我看可以總結出如下的用法:
- 通過實例方法來調用,傳入的參數應該是類對象
- 通過類方法來調用,傳入的參數應該是元類對象
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.name
e的內容卻是<ViewController: 0x7f95514077a0>
。下面person
實例調用test
不做過多解釋了,主要用來和上面方法調用做對比。
為什么會是這樣的結果呢?首先通過一張圖看一下兩種調用方法的內存信息。
通過上圖我們可以發現兩種方法調用方式很相近。那么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
結構體、cls
、obj
。通過一張圖展示一下這些局部變量存儲結構。
上面我們知道當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
就相當于isa
,isa
下面的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];
}
此時的局部變量內存結構如下圖所示
此時在訪問_name
成員變量的時候,越過cls
內存往高地址找就會來到string
(string
也是指針類型,占用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
,此時局部變量內存結構如下圖所示
當需要訪問_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
對象,我們先來看一下其局部變量內存結構
首先通過obj
找到cls
,cls
找到類對象進行方法調用,此時在訪問nickName
時,obj
查找成員變量,首先跳過8字節的cls
,之后跳過第一個成員變量_name
所占的8字節空間,最終再取8字節空間取出其中的值作為成員變量的值,那么此時也就是self
了。
為什么會跳過_name
的8個字節呢?
因為Person
類里面有2個屬性name
、nickName
,現在訪問的是nickName
,name
在低地址、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 總結
總結:這道面試題雖然很無厘頭,讓人感覺無從下手但是考察的內容非常多。
super
的底層本質為調用objc_msgSendSuper2
函數,傳入objc_super2
結構體,結構體內部存儲消息接受者和當前類,用來告知系統方法查找從父類開始。局部變量分配在棧空間,并且從高地址向低地址連續分配。先創建的局部變量分配在高地址,后續創建的局部變量連續分配在較低地址。
方法調用的消息機制,通過
isa
指針找到類對象進行消息發送。指針存儲的是實例變量的首字節地址,上述例子中
person
指針存儲的其實就是實例變量內部的isa
指針的地址。訪問成員變量的本質,找到成員變量的地址,按照成員變量所占的字節數,取出地址中存儲的成員變量的值。
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];
}
通過斷點查看其匯編調用棧
上圖中可以發現super
底層其實調用的是objc_msgSendSuper2
函數,我們來到源碼中查找一下objc_msgSendSuper2
函數的底層實現,我們可以在匯編文件中找到其相關底層實現。
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];
}
上述代碼的局部變量內存結構我們之前已經分析過了,真正的內存結構應該如下圖所示
通過上面對面試題的分析,我們現在想要驗證objc_msgSendSuper2
函數內傳入的結構體參數,只需要拿到cls
的地址,然后向后移8個地址就可以獲取到objc_super
結構體內的self
,在向后移8個地址就是current_class
的內存地址。通過打印current_class
的內容,就可以知道傳入objc_msgSendSuper2
函數內部的是當前類對象還是父類對象了。
通過上圖可以發現,最終打印的內容確實為當前類對象。
因此objc_msgSendSuper2
函數內部其實傳入的是當前類對象,并且在函數內部獲取其父類,告知系統從父類方法開始查找的。