前言
這是一道很有意思的題,題目來自群友,據說原題出自sunny。自以為是的解答這道題后,群友拋出一個新的問題,發(fā)現之前的解釋行不通,遂有此文。
0x00 Code
你可能見過這題,但后面有變形,別急著離開。
@interface A : NSObject
@property (copy) NSString *name;
@end
@implementation A
- (void)print {
NSLog(@"%@", _name);
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [A class];
void *obj = &cls;
[((__bridge id)obj) print];
}
@end
這題的考察點很多,我當時的解釋大概是這樣的:
OC對象的內存分布是 isa+ivars,對象的起始地址存儲著isa,即對象對應類的地址,通過isa可以找到對象的instanceMethods,所以題中的obj可以調用到print方法。至于name屬性的值,還是因為isa+ivars,在函數棧中,先申請的內存地址高于后申請的內存地址,由于A類中只有一個屬性,所以緊挨著cls的上一個變量就是對應name的值,當obj之上沒有顯示變量時,會找到方法對應的隱式參數 self與SEL,由于參數是從右向左壓棧的,所以SEL先于self入棧,所以self與cls相鄰,此時name的值就是方法中對應的隱藏參數self,即vc實例。
0x01 變形的由來
在答完這題后,群友的一句話讓我石化了:如果去掉[super viewDidLoad]調用會崩。
如果之前的解釋是正確的,這里不應該Crash,因為隱式參數還在,但確實崩了。
0x02 探索

如圖,去掉了super調用,并在print處打上斷點,運行后作如下操作:

先通過po查看_name,發(fā)現是個地址并非對象,所以NSLog中通過%@按照對象來解析顯然是無法打印的,查看這個地址后發(fā)現里面存儲著viewDidLoad對應的ASCII碼。
既然是ASCII碼,如果用%s應該就可以正常顯示了,再次做如下嘗試:

這里有幾個注意點:
- 不能使用
_name.UTF8String來轉成char *,因為_name并不是NSString *類型 - 不能使用
(__bridge char *)_name來轉成char *,編譯器會告訴你不兼容 - 通過
(__bridge void *)_name將_name轉成通用類型指針,再通過%s控制符解析成字符串,此時程序是可以正常運行的并輸出viewDidLoad
回到問題本身,去掉super調用后,_name中的數據究竟是什么?由于內部存儲著viewDidLoad 對應的ASCII碼,猜測 _name 中的數據應該是viewDidLoad 對應的 SEL,再次運行程序后做如下操作驗證:

顯然這個猜測是正確的。
0x03 Xcode中參數壓棧順序
做個簡單的測試:
void test(int a, int b) {
int c = 1;
}
test(2, 3);
控制臺作如下操作:

可以看到地址大小關系為a>b>c,并且這3個變量的地址是連續(xù)的??梢奨code中的參數是從左往右壓棧的。
上文已經驗證去掉super時,得到的其實是調用者的SEL,這就是隱藏參數中的SEL(因為參數從左往右壓棧,self先入棧SEL后入棧,索引SEL與cls相鄰)
0x04 回歸
既然如此,那么為什么調用super的時候會打印出 vc的實例,按照隱藏參數來說,不應該是SEL嗎?

將代碼改成原貌,并在[((__bridge id)obj) print]處打上斷點,運行程序后做如下操作:

獲取obj地址,查看偏移8字節(jié)后的值(跳過isa)。發(fā)現圖中兩處
1、2對應標記的地址相同,而且就是self與ViewController的類。此處偏移8字節(jié)后的值其實就是super,具體可見我的另一篇文章 《對super關鍵字的小驗證》
0x05 小結
本題調用super時,對應找到的是super關鍵字生成的結構體的第一個成員變量,即消息接收者self。不調用super時找到的是對應調用函數的SEL(參數從左向右壓棧,這里的SEL就是隱藏參數中的SEL),由于print中使用的是%@控制符,但此時對應找到的是字符串,由此導致崩潰,如果改成NSLog(@"%s", (__bridge void *)_name),仍然可以正常調用,此時輸出viewDidLoad。
Have fun!