一道OC綜合題的反思

前言

這是一道很有意思的題,題目來自群友,據說原題出自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應該就可以正常顯示了,再次做如下嘗試:

這里有幾個注意點:

  1. 不能使用_name.UTF8String來轉成char *,因為_name并不是NSString *類型
  2. 不能使用(__bridge char *)_name來轉成char *,編譯器會告訴你不兼容
  3. 通過(__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!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容