之前接觸到了一道面試題目产禾,分析之后覺得這道題目很有意思绝骚,考察了很多的底層知識抖棘。記錄下來以便幫自己整理思路...
有這樣的一個簡單的Person
類:
// ------ .h中
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
// ------ .m中
@implementation Person
- (void)print{
NSLog(@"---self.name is---%@------",self.name);
}
@end
然后在ViewController
中是這樣子的:
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
問題就是:- (void)print;
方法是否可以調(diào)用匿醒,如果可以調(diào)用卵牍,打印結(jié)果是什么?
運行一下程序,查看打印結(jié)果:
---self.name is---<ViewController: 0x159d09ae0>------
當(dāng)我看到打印結(jié)果的時候是一臉懵逼狀態(tài)削罩,我猜對了可以調(diào)用- (void)print;
方法瞄勾,但是卻沒有猜對打印的結(jié)果。
那么下面我們就一步步去分析弥激,為何可以調(diào)用方法以及打印結(jié)果是這樣的进陡。
為何可以調(diào)用?
平時我們調(diào)用方法的時候是這個樣子的:
Person *person = [[Person alloc] init];
[person print];
這兩句代碼做了什么呢微服?我是這樣理解的:在函數(shù)棧內(nèi)存放了一個叫person
的指針趾疚,指針里的存放的是Person類
的一個instance
實例對象在堆空間中的內(nèi)存地址。調(diào)用- (void)print;
方法是通過person
指針找到instance
對象以蕴,再通過instance
對象的isa
指針找到到Person
類對象糙麦。從Person
類對象中的方法緩存或方法列表中取出方法,進行調(diào)用舒裤。
現(xiàn)在我們再來看面試題中的變量之間的關(guān)系:
cls
中存放的是Person
類對象的地址喳资,那么功能上等價于instance
對象的isa
指針。
那么從流程上似乎就可以說的通了腾供,通過一個指針找到isa
或存儲著類對象地址的指針仆邓,再通過它找到Person
類對象。
對于計算機來講沒有類或者對象伴鳖,計算機只需要知道节值,去哪里讀寫數(shù)據(jù),讀取/寫入多大的數(shù)據(jù)榜聂。
我一開始有個疑問搞疗,因為我們知道從64位CPU開始,isa
并不是直接指向類對象须肆,而是要&
上一個ISA_MASK
值匿乃,來獲取真正的類對象地址(用33位來存儲指向的地址,其余位存儲一些其他的信息豌汇,如:引用計數(shù)幢炸,是否關(guān)聯(lián)對象,是否有析構(gòu)函數(shù)等)拒贱。那么cls
中存儲的是類對象的真實地址宛徊,所以cls
和isa
功能類似佛嬉,值不一定相同,那么它們怎么都能找到類對象呢闸天?
測試發(fā)現(xiàn)暖呕,雖然值不一定相同,但是在objc_msgSend
的時候苞氮,通過cls
和isa
來&ISA_MASK
結(jié)果是相同的湾揽。所以,都可以找到正確的類對象葱淳。
以ARM64平臺為例:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
最后一位為8
,換成二進制就是1000
钝腺,那么假如一個數(shù)A
來&
上ISA_MASK
抛姑,A & ISA_MASK
的作用就是將A
中高28位和低3位清零赞厕,取出中間33位數(shù)來。
A & ISA_MASK & ISA_MASK & ISA_MASK...
的結(jié)果依然和一次按位與結(jié)果相同定硝。
總結(jié)一下吧皿桑,在面試題中調(diào)用- (void)print;
方法,轉(zhuǎn)換成底層的objc_msgSend
方法的時候蔬啡,由于和正常時實例對象調(diào)用方法流程相同诲侮,可以通過cls
找到類對象,從而找到- (void)print;
方法箱蟆,那么就可以調(diào)用方法成功了沟绪。
打印結(jié)果為何如此?
Person
的instance
對象空猜,在內(nèi)存中的結(jié)構(gòu):
isa
是個指針绽慈,在64位處理器中,指針占用8個字節(jié)辈毯。那么訪問成員變量_name
的時候坝疼,就是在isa
地址+8個字節(jié)就可以訪問到_name
了。
那么在這道面試題中谆沃,方法調(diào)用者是cls
钝凶,而cls
是在函數(shù)棧中,那么我們就要分析函數(shù)調(diào)用棧唁影。
隨之這里又考察了一個點耕陷,super
關(guān)鍵字的理解。
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
super
關(guān)鍵字在底層會生成一個結(jié)構(gòu)體据沈,結(jié)構(gòu)體兩個成員哟沫,一個receiver
,一個receiver
的super_class
。這兩個成員都放在棧中卓舵。
通過編程經(jīng)驗或者如果懂得匯編代碼南用,可以比較輕松畫出上面的棧圖。
附上debug模式下,編譯器生成的匯編代碼(release模式下編譯器會做優(yōu)化裹虫,產(chǎn)生的匯編代碼不同):
TEST`-[ViewController viewDidLoad]:
0x100352524 <+0>: sub sp, sp, #0x40 ; =0x40 // 提升sp
0x100352528 <+4>: stp x29, x30, [sp, #0x30] // 保護x29肿嘲,x30寄存器
0x10035252c <+8>: add x29, sp, #0x30 ; =0x30 // 提升fp(x29)
0x100352530 <+12>: add x8, sp, #0x10 ; =0x10
0x100352534 <+16>: adrp x9, 2
0x100352538 <+20>: add x9, x9, #0xdf8 ; =0xdf8
0x10035253c <+24>: adrp x10, 2
0x100352540 <+28>: add x10, x10, #0xe30 ; =0xe30 //猜測:應(yīng)該是查找UIViewCOntroller類
0x100352544 <+32>: stur x0, [x29, #-0x8] //在棧中存self
0x100352548 <+36>: stur x1, [x29, #-0x10] //在棧中存方法viewDidLoad
0x10035254c <+40>: ldur x0, [x29, #-0x8]
0x100352550 <+44>: str x0, [sp, #0x10] //在棧中再存入一個self
0x100352554 <+48>: ldr x10, [x10]
0x100352558 <+52>: str x10, [sp, #0x18] //將UIViewCOntroller存入棧中
0x10035255c <+56>: ldr x1, [x9]
0x100352560 <+60>: mov x0, x8
0x100352564 <+64>: bl 0x100352b00 ; symbol stub for: objc_msgSendSuper2 //調(diào)用objc_msgSendSuper2方法
0x100352568 <+68>: adrp x8, 2
0x10035256c <+72>: add x8, x8, #0xe00 ; =0xe00
0x100352570 <+76>: adrp x9, 2
0x100352574 <+80>: add x9, x9, #0xe20 ; =0xe20
0x100352578 <+84>: ldr x9, [x9]
0x10035257c <+88>: ldr x1, [x8]
0x100352580 <+92>: mov x0, x9
0x100352584 <+96>: bl 0x100352af4 ; symbol stub for: objc_msgSend
0x100352588 <+100>: mov x29, x29
0x10035258c <+104>: bl 0x100352b18 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x100352590 <+108>: adrp x8, 2
0x100352594 <+112>: add x8, x8, #0xe08 ; =0xe08
0x100352598 <+116>: add x9, sp, #0x8 ; =0x8
0x10035259c <+120>: str x0, [sp, #0x8] //將Person類對象地址放入棧中
0x1003525a0 <+124>: str x9, [sp] // 將cls的地址放入棧中
0x1003525a4 <+128>: ldr x0, [sp]
0x1003525a8 <+132>: ldr x1, [x8]
0x1003525ac <+136>: bl 0x100352af4 ; symbol stub for: objc_msgSend
0x1003525b0 <+140>: add x0, sp, #0x8 ; =0x8
0x1003525b4 <+144>: mov x8, #0x0
-> 0x1003525b8 <+148>: mov x1, x8
0x1003525bc <+152>: bl 0x100352b30 ; symbol stub for: objc_storeStrong
0x1003525c0 <+156>: ldp x29, x30, [sp, #0x30]
0x1003525c4 <+160>: add sp, sp, #0x40 ; =0x40
0x1003525c8 <+164>: ret
這樣在訪問_name
的時候,其實是訪問椫空間內(nèi)的cls
地址值+8個字節(jié)的地址里存放的內(nèi)容雳窟,就是viewController
對象了。
可能有的同學(xué)還是對super
關(guān)鍵字不是很理解匣屡,沒事封救,下面這道題目分析一下:
self和super的測試
FGObject1:
@interface FGObject1 : NSObject
@end
@implementation FGObject1
- (Class)class
{
return [NSObject class];
}
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"FGObject1 --- %@ --- %@",[self class],[super class]);
}
return self;
}
@end
FGObject2:
@interface FGObject2 : FGObject1
@end
@implementation FGObject2
- (Class)class
{
return [UIView class];
}
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"FGObject2 --- %@ --- %@",[self class],[super class]);
}
return self;
}
@end
當(dāng)分別創(chuàng)建它們兩個不同的對象的時候,控制臺如何輸出捣作?
FGObject1 *object1 = [[FGObject1 alloc] init];
FGObject2 *object2 = [[FGObject2 alloc] init];
輸出結(jié)果:
2018-04-27 ARCStudy[80422:11252273] FGObject1 --- NSObject --- FGObject1
2018-04-27 ARCStudy[80422:11252273] FGObject1 --- UIView --- FGObject2
2018-04-27 ARCStudy[80422:11252273] FGObject2 --- UIView --- NSObject
那么結(jié)果你答對了嗎?
方法中的隱藏參數(shù)
我們經(jīng)常在方法中使用self
關(guān)鍵字來引用實例本身誉结,但從沒有想過為什么self
就能取到調(diào)用當(dāng)前方法的對象吧。其實self
的內(nèi)容是在方法運行時被偷偷的動態(tài)傳入的券躁。
當(dāng)objc_msgSend
找到方法對應(yīng)的實現(xiàn)時惩坑,它將直接調(diào)用該方法實現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實現(xiàn),同時,它還將傳遞兩個隱藏的參數(shù):
- 接收消息的對象(也就是
self
指向的內(nèi)容) - 方法選擇器(
_cmd
指向的內(nèi)容)
之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數(shù)也拜。它們是在代碼被編譯時被插入實現(xiàn)中的以舒。
在這兩個參數(shù)中,self
更有用慢哈。實際上蔓钟,它是在方法實現(xiàn)中訪問消息接收者對象的實例變量的途徑。
而當(dāng)方法中的super
關(guān)鍵字接收到消息時卵贱,編譯器會創(chuàng)建一個objc_super
結(jié)構(gòu)體:
struct objc_super {id receiver; Class class;};
/**
* Sends a message with a simple return value to the superclass of an instance of a class.
*
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
* context the message was sent to, including the instance of the class that is to receive the
* message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method identified by \e op.
*
* @see objc_msgSend
*/
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
的注釋滥沫,寫的比較清楚,receive
是類的instance
對象艰赞,而查找方法IMP
時是從superclass
來查找的佣谐。
這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。但receiver
仍然是self
本身方妖,這點需要注意狭魂。
如果我們這樣來寫:
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"FGObject1 --- %@ --- %@",[self class],[super class]);
}
return self;
}
打印結(jié)果:
2018-04-27 ARCStudy[80476:11297894] FGObject1 --- FGObject1 --- FGObject1
因為當(dāng)我們想通過[super class]
獲取超類時,編譯器只是將指向self
的id
指針和class
的SEL
傳遞給了objc_msgSendSuper
函數(shù)党觅,因為只有在NSObject
類才能找到class
方法雌澄,然后class
方法調(diào)用object_getClass()
,接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class))
杯瞻,傳入的第一個參數(shù)是指向self
的id
指針镐牺,與調(diào)用[self class]
相同,所以我們得到的永遠都是self
的類型魁莉。
那么最上面那個測試的結(jié)果呢睬涧?不同就是在類中都重寫了- (Class)class募胃;
方法。
FGObject1初始化時:
-
[self class]
:調(diào)用自己重寫的方法畦浓,返回NSObject
痹束。 -
[super class]
:給父類NSObject
發(fā)送消息,那么就是去查找實例對象isa指針指向的類讶请,所以結(jié)果是FGObject1
祷嘶。
FGObject2初始化時:
-
[super init]
中[self class]
:會調(diào)用FGObject2
的- (Class)class;
方法夺溢,打印UIView
论巍。 -
[super init]
中[super class]
:給父類NSObject
發(fā)送消息,那么就是去查找實例對象isa
指針指向的類风响,所以結(jié)果是FGObject2
嘉汰。 - 輪到自己的
[self class]
時,調(diào)用自己的- (Class)class钞诡;
方法郑现,打印UIView
。 - 輪到自己的
[super class]
時荧降,調(diào)用FGObject1
的- (Class)class;
方法攒读,打印的是NSObject
朵诫。