僵屍對象(NSZombie)用於調試內存管理問題的時候是相當有用的,此前我有談論過僵屍對象(NSZombie)的實現,在ШпиркоАлексей 的建議下,今天我們要談一談如何從頭開始構建這個類。
回顧
僵屍對象(NSZombie)能用於嗅探內存錯誤,更確切的說法是,當某一指針發送消息到已經釋放的Objective-C對象的時候,僵屍對象就能檢測到。一個比較常見的就是"use after free"錯誤。
在通常情況下,這樣的錯誤會導致消息發送到已被重寫的內存,或者這段內存已經返回給內核了。無論是哪種結果,都會引發沖突。如果說這段內存恰好被一個新的Objective-C對象重寫,那麼消息可能就會發送給與原來風馬牛不相及的對象,而且會由於無法識別方法選擇器而拋出異常,假如這個錯誤的對象也有該方法的話,那麼就會發生更奇怪的問題。
當然也有可能這段內存中存放的對象尚未被更改,處於將被銷毀的狀態,那麼這會引起更匪夷所思的錯誤。舉個例子,假如該對象裡面包含有一個UNIX文件句柄,那麼就有可能重復調用兩次關閉文件,而其中重復關閉的文件描述符也許是程序裡其他地方打開的文件,這樣的情況導致的錯誤會更為奇葩,你發現錯誤的時候甚至找不到bug在哪。
雖然ARC(Automatic Reference Counting)機制大大的減少了這類錯誤的發生,然而並非能斬草除根。由於各種各樣的原因問題還是要發生,比如多線程應用、沒有開啟ARC機制、錯配方法聲明或者類型系統本身存在改寫ARC存儲編輯器的弊端。
僵屍對象會與對象的回收掛鉤,這時候對象回收的最後一步並不是真正的釋放所占內存,而是將其改為一個僵屍類,並截斷所有發送給該對象的消息。這樣,在有消息發給該對象時候都會檢測到出錯信息,而不是像正常調試下那樣出現五花八門的狀況。當然也有在重寫對象之後再釋放內存的方式,不過這樣做的話就達不到什麼目的了,因為釋放的內存很快又會被占用,我一般都會忽略掉這種做法。
要實現這種機制,就需要先實現與對象回收掛鉤,然後再構建適當的僵屍類型,那麼下面就開始吧。
捕獲所有消息
假如構建一個空的類,其中沒有任何方法,那麼任何發到這個類實例的消息都會進入消息轉發機制。顯然這個時候會調用forwardInvocation:方法來完成消息重定向,不過這一方法的調用其實要晚一些,在它運行前,運行時需要一個方法來創建NSInvocation對象,也就是說,methodSignatureForSelector:會先執行。吶,這個方法就是我們需要重寫來為僵屍類發送消息的重點。
動態分配
除了發送的選擇器之外,僵屍對象還需記錄覆蓋對象的原本類型。然而,對於僵屍對象來說,不一定有足夠的空間來存儲一個對象原本類型的應用。如果對於該類沒有其他的實例變量,而僵屍對象又不能重新為其分配存儲空間,那麼在僵屍類定義的時候就應該預留空間來存儲覆蓋空間原本的類型。這也意味著僵屍對象的內存空間必須是動態分配的,對於每個類來說,一旦它有某個實例僵死(被回收),它就會擁有自己的僵屍類。
那麼問題來了,這段內存原本對應的類的引用應該存放在哪。使用額外的存儲空間來分配這個類的存儲是可行的,但是在使用起來的時候並不方便。一個較為簡單的方法就是使用類名,因為Objective-C中每個類都存在於一個大的命名空間中,所以對於辨識一個類來說只要用類名就夠了。給該類名加上前綴並用來命名僵屍類,這樣就既可以描述該類本身,又能從其類名還原本來的類。在這裡我用的是MAZombie_作為前綴。
方法的實現
請注意本文中所有的代碼均默認沒有開啟ARC功能,因為使用ARC內存管理的話會影響NSZombie的構建,有礙觀瞻。
我們從一個最簡單的方法開始實現,下面是一個空方法:
void EmptyIMP(id obj, SEL _cmd) {}
在Objective-C中每個類及其子類在被第一次發送消息前都會先調用它的+initialize方法,從而使其能夠初始化本身。如果運行時發現該類沒有實現+initialize方法,就會將消息轉發,當然在此如果讓消息轉發的話我們的僵屍類也就沒什麼用了。所以需要添加一個空的+initialize方法來避免這個問題,EmptyIMP方法則就是作為僵屍類中+initialize方法的實現而構造的。
而-methodSignatureForSelector:方法則更有意思:
NSMethodSignature *ZombieMethodSignatureForSelector(id obj, SEL _cmd, SEL selector) {
原內存中的對象的類型可以從中提取,在僵屍類裡面是這樣的:
Class class = object_getClass(obj); NSString *className = NSStringFromClass(class);
去掉類名的前綴就可以提取原本類型的名稱了:
className = [className substringFromIndex: [@"MAZombie_" length]];
接著記錄出錯信息並調用abort()來確保你能夠注意到:
NSLog(@"Selector %@ sent to deallocated instance %p of class %@", NSStringFromSelector(selector), obj, className); abort(); }
創建類
ZombifyClass方法用於從一個普通類生成一個僵屍類,如果必要的話可以像這樣創建它:
Class ZombifyClass(Class class) {
僵屍類的類名在用於檢測其是否存在時相當有用,當然如果不存在的情況下也要用到這個類名:
NSString *className = NSStringFromClass(class); NSString *zombieClassName = [@"MAZombie_" stringByAppendingString: className];
使用NSClassFromeString來檢測僵屍類是否存在,然後如果存在的話會返回一個僵屍類:
Class zombieClass = NSClassFromString(zombieClassName); if(zombieClass) return zombieClass;
請注意這裡會產生一個競爭機制:如果有同一個類的兩個實例分別在不同的線程中同時僵死的話,它們都會試圖創建僵屍類。在即使實際編碼的時候你最好對這一段代碼進行上鎖,以保證類似的沖突不會發生:
調用objc_allocateClassPair方法為僵屍類分配內存:
zombieClass = objc_allocateClassPair(nil, [zombieClassName UTF8String], 0);
使用class_addMethod來添加methodSignature方法的實現。其中的參數"@@::"表示其返回的帶有三個參數的對象:一個對象(self),一個選擇器(_cmd),以及另一個選擇器(確切的選擇器參數):
class_addMethod(zombieClass, @selector(methodSignatureForSelector:), (IMP)ZombieMethodSignatureForSelector, "@@::");
前面的空方法一樣也要添加到+initialize方法的實現中,並沒有單獨的函數用於添加類方法,所以我們要將方法添加到類的類中,也就是元類中(Objective-C的類其實也可以看作是對象,即類對象,這個類對象的類就是元類):
class_addMethod(object_getClass(zombieClass), @selector(initialize), (IMP)EmptyIMP, "v@:");
現在僵屍類已經全部設置完畢,接著它就可以在運行時注冊並返回了:
objc_registerClassPair(zombieClass); return zombieClass; }
對象僵屍化
為了能夠使得對象能夠僵屍化,我們需要重寫NSObject的dealloc方法。子類的dealloc方法照舊,不過一旦dealloc回溯到NSObject的時候,有關僵屍化的代碼就會運行了。這段代碼會防止對象被徹底的銷毀,然後確保用一個僵屍類來保存這個對象的信息。這些操作都由一個函數來封裝實現:
void EnableZombies(void) { Method m = class_getInstanceMethod([NSObject class], @selector(dealloc)); method_setImplementation(m, (IMP)ZombieDealloc); }
我們可以在main()函數或者其他相應的地方調用EnableZombies(),剩下的事就由它自行解決了。下面這個ZombieDealloc函數簡單明了,它會調用ZombifyClass來給需要回收的對象生成僵屍類型,然後用object_setClass來將此對象的類型更改為僵屍類:
void ZombieDealloc(id obj, SEL _cmd) { Class c = ZombifyClass(object_getClass(obj)); object_setClass(obj, c); }
測試
現在需要確保下面這段能正常運行:
obj = [[NSIndexSet alloc] init]; [obj release]; [obj count];
我在這裡算是很隨意的用了NSIndexSet類,該類無需與CoreFoundation框架作奇怪的橋接。應用僵屍類運行之後的結果如下:
a.out[5796:527741] Selector count sent to deallocated instance 0x100111240 of class NSIndexSet
大功告成~
總結
僵屍類的實現按說相當簡單。通過動態的給類分配空間,我們可以在無需依賴僵屍對象內部空間的情況下保存對象原本類型的信息。methodSignatureForSelector:方法為截斷發送給僵屍類的消息提供了便於突破的節點。僅需要掛鉤-[NSObject dealloc]方法就可以將一般對象轉換為僵屍對象,而不用在其減少為零的時候銷毀。
(本文由CocoaChina翻譯自Mike Ash的博客,轉載請注明出處)