寫程序遇到 Bug 并不可怕,大部分的問題,通過簡單的 Log 或者 代碼分析并不難找到原因所在。但是在 Objective-C 編程中遇到 EXC_BAD_ACCESS 問題的時候,通過簡單常規(guī)的手段很難發(fā)現(xiàn)問題。這篇文章,給大家介紹一個常用的查找 EXC_BAD_ACCESS 問題根源的方法。
首先說一下 EXC_BAD_ACCESS 這個錯誤,可以這么說,90%的錯誤來源在于對一個已經(jīng)釋放的對象進行release操作。舉一個簡單的例子來說明吧,首先看一段Java代碼:
public class Test{
public static void main(String[] args){
String s = “This is a test string”;
s = s.substring(s.indexOf(“a”),(s.length()));
System.out.println(s);
}
}
這種寫法在Java中很常見也很普遍,這不會產(chǎn)生任何問題。但是到了 Objective-C 中,就會出事,考慮這個程序:
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString* s = [[NSString alloc]initWithString:@”This is a test string”];
s = [s substringFromIndex:[s rangeOfString:@"a"].location];//內(nèi)存泄露
[s release];//錯誤釋放
[pool drain];//EXC_BAD_ACCESS
return 0;
}
這個例子當(dāng)然狠容易的看出問題所在,如果這段代碼包含在一個很大的邏輯中,確實容易被忽略。Objective-C 這段代碼有三個致命問題:1、內(nèi)存泄露;2、錯誤釋放;3、造成 EXC_BAD_ACCESS 錯誤。
1, NSString* s = [[NSString alloc]initWithString:@”This is a test string”]; 創(chuàng)建了一個 NSString Object,隨后的 s = [s substringFromIndex:[s rangeOfString:@"a"].location]; 執(zhí)行后,導(dǎo)致創(chuàng)建的對象引用消失,直接造成內(nèi)存泄露。
2,錯誤釋放。[s release]; 這個問題,原因之一是一個邏輯錯誤,以為 s 還是我們最初創(chuàng)建的那個 NSString 對象。第二是因為從 substringFromIndex:(NSUInteger i) 這個方法返回的 NSString 對象,并不需要我們來釋放,它其實是一個被 substringFromIndex 方法標(biāo)記為 autorelease 的對象。如果我們強行的釋放了它,那么會造成 EXC_BAD_ACCESS 問題。
3, EXC_BAD_ACCESS。由于 s 指向的 NSString 對象被標(biāo)記為 autorelease, 則在 NSAutoreleasePool 中已有記錄。但是由于我們在前面錯誤的釋放了該對象,則當(dāng) [pool drain] 的時候,NSAutoreleasePool 又一次的對它記錄的 s 對象調(diào)用了 release 方法,但這個時候 s 已經(jīng)被釋放不復(fù)存在,則直接導(dǎo)致了 EXC_BAD_ACCESS問題。
那么,知道了 EXC_BAD_ACCESS 的誘因之一后,如何快速高效的定位問題?
1: 為工程運行時加入 NSZombieEnabled 環(huán)境變量,并設(shè)為啟用,則在 EXC_BAD_ACCESS 發(fā)生時,XCode 的 Console 會打印出問題描述。
首先雙擊 XCode 工程中,Executables 下的 可執(zhí)行模組,
在彈出窗口中,Variables to be set in the environment,添加 NSZombieEnabled,并設(shè)定為 YES,點擊選中復(fù)選框啟用此變量。
這樣,運行上述 Objective-C 時會看到控制臺輸出:Untitled[3646:a0f] *** -[CFString release]: message sent to deallocated instance 0x10010d340
這條消息對于定位問題有很好的提示作用。但是很多時候,只有這條提示是不夠的,我們需要更多的提示來幫助定位問題,這時候再加入 MallocStackLogging 來啟用malloc記錄。
當(dāng)錯誤發(fā)生后,在終端執(zhí)行:
malloc_history ${App_PID} ${Object_instance_addr}
則會獲得相應(yīng)的 malloc 歷史記錄,比如對于上一個控制臺輸出
Untitled[3646:a0f] *** -[CFString release]: message sent to deallocated instance 0x10010d340
則我們可以在終端執(zhí)行,結(jié)果如下:
Buick-Wongs-MacBook-Pro:Downloads buick$ malloc_history 3646 0x10010d340
malloc_history Report Version: 2.0
Process: Untitled [3646]
Path: /Users/buick/Desktop/Untitled/build/Debug/Untitled
Load Address: 0×100000000
Identifier: Untitled
Version: ??? (???)
Code Type: X86-64 (Native)
Parent Process: gdb-i386-apple-darwin [3638]
Date/Time: 2011-02-01 15:07:04.181 +0800
OS Version: Mac OS X 10.6.6 (10J567)
Report Version: 6
ALLOC 0x10010d340-0x10010d357 [size=24]: thread_7fff70118ca0 |start | main | objc_msgSend | lookUpMethod | prepareForMethodLookup | _class_initialize | +[NSString initialize] | objc_msgSend | lookUpMethod | prepareForMethodLookup | _class_initialize | NXCreateMapTableFromZone | malloc_zone_malloc
—-
FREE 0x10010d340-0x10010d357 [size=24]: thread_7fff70118ca0 |start | main | objc_msgSend | lookUpMethod | prepareForMethodLookup | _class_initialize | _finishInitializing | free
ALLOC 0x10010d340-0x10010d357 [size=24]: thread_7fff70118ca0 |start | main | -[NSPlaceholderString initWithString:] | objc_msgSend | lookUpMethod | prepareForMethodLookup | _class_initialize | _class_initialize | +[NSMutableString initialize] | objc_msgSend | lookUpMethod | prepareForMethodLookup | _class_initialize | NXCreateMapTableFromZone | malloc_zone_malloc
—-
FREE 0x10010d340-0x10010d357 [size=24]: thread_7fff70118ca0 |start | main | -[NSPlaceholderString initWithString:] | objc_msgSend | lookUpMethod | prepareForMethodLookup | _class_initialize | _class_initialize | _finishInitializing | free
ALLOC 0x10010d340-0x10010d35f [size=32]: thread_7fff70118ca0 |start | main | -[NSCFString substringWithRange:] | CFStringCreateWithSubstring | __CFStringCreateImmutableFunnel3 | _CFRuntimeCreateInstance | malloc_zone_malloc
這樣就可以很快的定位出問題的代碼片段了,注意輸出的最后一行,,,這行雖然不是問題的最終原因,但是離問題點已經(jīng)很近了,隨著它找下去,八成就會找到問題。