高效編寫代碼的方法(二十六):使用“Zombies”來Debug

作用

簡單來說就用來Debug野指針的情況。
我們向一個(gè)被釋放的對(duì)象發(fā)送消息是不安全的,但是有時(shí)候又沒有問題。這主要取決于這個(gè)對(duì)象之前所在的內(nèi)存空間是否被重寫過了,這是不確定的,因此所造成的情況也是不確定的。當(dāng)這塊內(nèi)存空間被復(fù)寫為另一個(gè)對(duì)象的時(shí)候,如果不能識(shí)別我們發(fā)送的消息,那么崩潰是必然的。如果有時(shí)候能被識(shí)別,那么debug起來就非常困難。

Zombies

Cocoa 有個(gè)很好的功能:“Zombies”,此時(shí)就能派上很大用處。當(dāng)“Zombies”開啟的時(shí)候,runtime將會(huì)把所有要被銷毀(deallocated)的對(duì)象變成特殊的僵尸對(duì)象,而不是按正常流程進(jìn)行銷毀。而存放這些僵尸對(duì)象的內(nèi)存空間是不可復(fù)用的,從而杜絕以上這種野指針的情況。
此時(shí)我們向一個(gè)zombie發(fā)送消息的話,將會(huì)拋出異常并明確告知消息被發(fā)送到了一個(gè)已經(jīng)銷毀的對(duì)象上。
如下:

*** -[CFString respondToSelector:]: message sent to  deallocated instance 0x7ff9e9c080e0

使用

在Xcode中:
Edit Scheme

Edit Scheme

按照?qǐng)D中注解給Zombie Objects打上勾即可。

原理

主要還是依賴于runtime、Foundation和CoreFoundation框架。如果我們打開了Zombie Objects選項(xiàng),當(dāng)一個(gè)對(duì)象即將deallocated的時(shí)候,將會(huì)額外多一步,就是額外的這一步將該對(duì)象轉(zhuǎn)換為僵尸對(duì)象而不是直接deallocated。
下面一段代碼做參考:


@interface EOCClass : NSObject
@end

@implementation EOCClass
    
@end
void PrintClassInfo(id obj) {
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"===%s : %s ===",class_getName(cls),class_getName(superCls));
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        EOCClass *obj = [[EOCClass alloc] init];
        NSLog(@"Before release:");
        PrintClassInfo(obj);
        [obj release];
        NSLog(@"After release:");
        PrintClassInfo(obj);
    }
}

這段代碼用了MRC,以此更方便展示將是對(duì)象的產(chǎn)生。
最后的運(yùn)行結(jié)果如下:

Before release:
=== EOCClass : NSObject ===
After release:
=== _NSZombie_EOCClass : nil ===

從上可以看出,當(dāng)一個(gè)對(duì)象變成僵尸的時(shí)候,它的類也從EOCClass變成了_NSZombie_EOCClass。問題是,這個(gè)類是哪里來的?
自然而然我們會(huì)想到runtime創(chuàng)建了這個(gè)僵尸類。
以上這個(gè)僵尸類是模板NSZombie類的一個(gè)副本,它并沒有什么別的作用,只是簡單的作為一個(gè)標(biāo)記。
以下是一段偽代碼,大致展現(xiàn)了這個(gè)僵尸類是如何創(chuàng)建的,并且該對(duì)象是如何變成一個(gè)僵尸對(duì)象的:

//Obtain the class of the object being deallocated  獲得將要釋放的對(duì)象的類
Class cls = object_getClass(self);
//Get the class's name   獲得類名
const char *clsName = class_getName(cls);
//Prepend _NSZombie_ to the class name  提前擴(kuò)展好需要的類名
const char *zombieClsName = "_NSZombie_" + clsName;
// See if the specific zombie class exists   檢查該類是否存在
Class zombieCls = objc_lookUpClass(zombieClsName);

//If the specific zombie class doesn't exist,
//then it needs to be created  如果不存在,則創(chuàng)建
if (!zombieCls) {
    //Obtain the template zombie class called _NSZombie_  獲得模板類_NSZombie_
    Class baseZombieCls = objc_loopUpClass("_NSZombie_");
    //Duplicate the base zombie class,where the new class's name is the prepended string from above 以模板為基礎(chǔ)重建一個(gè)類,類名為以上的zombieClsName字符串
    zombieCls = objc_duplicateClass(baseZombieCls,zombieClsName,0);
}
//Perform normal destruction of the object being deallocated 執(zhí)行一般的銷毀流程
objc_destructInstance(self);
//Set the class of the object being deallocated to the zombie class 講對(duì)象的類設(shè)置為僵尸類
objc_setClass(self,zombieCls);
//the class of 'self' is now _NSZombie_OriginalClass 

當(dāng)NSZombieEnabled 這個(gè)選項(xiàng)開啟的時(shí)候,runtime會(huì)將上述代碼與之前常規(guī)的dealloc代碼進(jìn)行互換,由此來保證對(duì)象的類變成僵尸類。
關(guān)鍵一點(diǎn)是,這個(gè)內(nèi)存中的對(duì)象其實(shí)還是活著的,內(nèi)存并沒有被釋放,因此該內(nèi)存也不會(huì)被重復(fù)使用。因?yàn)閷?duì)象被標(biāo)記為了僵尸,所以接收到消息的時(shí)候能提示我們異常所在。
之所以大費(fèi)周章的給每一個(gè)對(duì)象的類都重新制定一個(gè)相對(duì)應(yīng)的僵尸類是因?yàn)檫@樣在反饋問題的時(shí)候會(huì)顯得更加精準(zhǔn)一些,如果都簡單的報(bào)錯(cuò)NSZombie對(duì)象無法識(shí)別方法,那么debug效果就幾乎沒有了

NSZombie

NSZombie本身并不實(shí)現(xiàn)任何方法,也沒有父類,所以它是一個(gè)基類,就像NSObject一樣。因?yàn)樗粚?shí)現(xiàn)任何方法,所以當(dāng)接收到消息的時(shí)候,會(huì)完整的走一遍消息轉(zhuǎn)發(fā)流程。
消息轉(zhuǎn)發(fā)中關(guān)鍵的一環(huán)是forwarding,它做的其中一件事情就是先檢查對(duì)象的類名是否含有前綴NSZombie,如果檢測(cè)到了,那么就直接走報(bào)告僵尸對(duì)象的流程。再打印完錯(cuò)誤信息之后程序就結(jié)束運(yùn)行了。
以下這段偽代碼可以幫助理解在forwarding里是怎么處理zombie對(duì)象的:

//Obtain the object's class  取得對(duì)象的類
Class cls = object_getClass(self);
//Get the class's name  取得類名
const char *clsName = class_getName(cls);
//Check if the class is prefixed with _NSZombie_ 檢查是否含有前綴_NSZombie_
if(string_has_prefix(clsName,"_NSZombie_")) {
    //if so, this object is a zombie 如果前綴符合,那么它是一個(gè)僵尸對(duì)象
    //Get the original class name by skipping past the _NSZombie_, i.e. taking the substring from character 10   獲取原始的類名
    const char *originalClsName = substring_from(clsName,10);
    //Get the selector name of the message  獲取方法名
    const char *selectorName = sel_getName(_cmd);
    //Log a message to indicate which selector is being sent yo which zombie  打印錯(cuò)誤信息
    Log("*** -[%s %s]: message sent to deallocated instance %p", originalClsName,selectorName,self);
    //Kill the application 結(jié)束程序
    abort();
}  
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容