Thread 1: EXC_BAD_ACCESS

来源:互联网 发布:云视通监控软件下载 编辑:程序博客网 时间:2024/05/21 14:06

IOS 開發中,如果提前釋放一個指針的內存,在以後還繼續使用這個指針,那麼程序會立刻 crash 掉,而且很難有報錯信息,我以前都是靠猜測去判斷錯誤的原因,我們應該利用工具去找到錯誤的地方,然後快速准確的定位到錯誤地方,及其錯誤原因,最後進行改進。

    其實 iOS 控制台提供這種機制,如果你選擇 debug 模式(必須在這個模式下),在程序 crash 之後,在控制台輸入 bt,就可以顯示 crash 堆棧:

    Program received signal:  “EXC_BAD_ACCESS”.
    warning: Unable to read symbols for /Developer/ios4.2.1/Platforms/iPhoneOS.platform/DeviceSupport/4.2.1 (8C148)/Symbols/Developer/usr/lib/libXcodeDebuggerSupport.dylib (file not found).
    (gdb) bt
    #0  0x33a06464 in objc_msgSend ()
    #1  0x3139de2e in -[UIImageView setImage:] ()
    #2  0x00009ecc in -[RoundMenuView touchesEnded:withEvent:] (self=0x29e140, _cmd=0x316b1a7b, touches=0x2e1050, event=0x2424f0) at /Users/wangjun/workspace/iphone/Classes/RoundMenuView.m:130
    #3  0x313b1354 in -[UIWindow _sendTouchesForEvent:] ()
    #4  0x313b0cce in -[UIWindow sendEvent:] ()
    #5  0x3139bfc6 in -[UIApplication sendEvent:] ()
    #6  0x3139b906 in _UIApplicationHandleEvent ()
    #7  0x31eecf02 in PurpleEventCallback ()
    #8  0x304236fe in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ ()
    #9  0x304236c2 in __CFRunLoopDoSource1 ()
    #10 0x30415f7c in __CFRunLoopRun ()
    #11 0x30415c86 in CFRunLoopRunSpecific ()
    #12 0x30415b8e in CFRunLoopRunInMode ()
    #13 0x31eec4aa in GSEventRunModal ()
    #14 0x31eec556 in GSEventRun ()
    #15 0x313cf328 in -[UIApplication _run] ()
    #16 0x313cce92 in UIApplicationMain ()
    #17 0x00002da2 in main (argc=1, argv=0x2fdff44c) at /Users/wangjun/workspace/iphone/main.m:19
    kill
    quit

    利用堆棧信息,就可以准確的定位到錯誤地方。

MallocStackLogging <wbr>的設置方法 <wbr>查找 <wbr>EXC_BAD_ACCESS <wbr>問題根源的方法
MallocStackLogging <wbr>的設置方法 <wbr>查找 <wbr>EXC_BAD_ACCESS <wbr>問題根源的方法

  寫程序遇到 Bug 並不可怕,大部分的問題,通過簡單的 Log 或者 代碼分析並不難找到原因所在。但是在 Objective-C 編程中遇到 EXC_BAD_ACCESS 問題的時候,通過簡單常規的手段很難發現問題。這篇文章,给大家介紹一個常用的查找 EXC_BAD_ACCESS 問題根源的方法。

    首先說一下 EXC_BAD_ACCESS 這個錯誤,可以這麼說,90%的錯誤來源在於對一個已經釋放的對象進行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中很常見也很普遍,這不會產生任何問題。但是到了 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];//內存泄露
        [s release];//錯誤釋放
[pool drain];//EXC_BAD_ACCESS
return 0;
}


    這個例子當然狠容易的看出問題所在,如果這段代碼包含在一個很大的邏輯中,確實容易被忽略。Objective-C 這段代碼有三個致命問題:1、內存泄露;2、錯誤釋放;3、造成 EXC_BAD_ACCESS 錯誤。

    1, NSString* s = [[NSString alloc]initWithString:@”This is a test string”]; 創建了一個 NSString Object,隨後的 s = [s substringFromIndex:[s rangeOfString:@"a"].location]; 執行後,導致創建的對象引用消失,直接造成內存泄露。

    2,錯誤釋放。[s release]; 這個問題,原因之一是一個邏輯錯誤,以为 s 還是我們最初創建的那個 NSString 對象。第二是因为從 substringFromIndex:(NSUInteger i) 這個方法返回的 NSString 對象,並不需要我們來釋放,它其實是一個被 substringFromIndex 方法標記为 autorelease 的對象。如果我們強行的釋放了它,那麼會造成 EXC_BAD_ACCESS 問題。

    3, EXC_BAD_ACCESS。由於 s 指向的 NSString 對象被標記为 autorelease, 則在 NSAutoreleasePool 中已有記錄。但是由於我們在前面錯誤的釋放了該對象,則當 [pool drain] 的時候,NSAutoreleasePool 又一次的對它記錄的 s 對象調用了 release 方法,但這個時候 s 已經被釋放不复存在,則直接導致了 EXC_BAD_ACCESS問題。

    那麼,知道了 EXC_BAD_ACCESS 的誘因之一後,如何快速高效的定位問題?

1: 为工程運行時加入 NSZombieEnabled 環境變量,並設为启用,則在 EXC_BAD_ACCESS 發生時,XCode 的 Console 會打印出問題描述。

首先雙擊 XCode 工程中,Executables 下的 可執行模組

在彈出窗口中,Variables to be set in the environment,添加 NSZombieEnabled,並設定为 YES,點擊選中复選框启用此變量。


    這样,運行上述 Objective-C 時會看到控制台輸出:Untitled[3646:a0f] *** -[CFString release]: message sent to deallocated instance 0x10010d340

這條消息對於定位問題有很好的提示作用。但是很多時候,只有這條提示是不夠的,我們需要更多的提示來幫助定位問題,這時候再加入 MallocStackLogging 來启用malloc記錄。

 當錯誤發生後,在終端執行:

shell  malloc_history ${App_PID} ${Object_instance_addr}

輸入: malloc_history 3646 0x100110d340  

    則會獲得相應的 malloc 曆史記錄,比如對於上一個控制台輸出

Untitled[3646:a0f] *** -[CFString release]: message sent to deallocated instance 0x10010d340

    則我們可以在終端執行,結果如下:

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

    這样就可以很快的定位出問題的代碼片段了,注意輸出的最後一行,,,這行雖然不是問題的最終原因,但是離問題點已經很近了,隨着它找下去,八成就會找到問題。

 

下面還有一個更極端的方法:

有時程序崩溃根本不知錯誤發生在什麼地方。比如程序出現EXEC_BAD_ACCESS的時候,雖然大部分情況使用設定 NSZombieEnabled環境變量可以幫助你找到問題的所在,但少數情況下,即使設定了NSZombieEnabled環境變量,還是不知道程序崩溃在什麼地方。那麼就需要使用下列代碼進行幫助了:



#ifdef _FOR_DEBUG_
-(BOOL) respondsToSelector:(SEL)aSelector {
    printf("SELECTOR: %s\n", [NSStringFromSelector(aSelector) UTF8String]);
    return [super respondsToSelector:aSelector];
}
#endif

你需要在每個object的.m或者.mm文件中加入上面代碼,並且在other c flags中加入-D _FOR_DEBUG_(記住請只在Debug Configuration下加入此標記)。這样當你程序崩溃時,Xcode的console上就會准確地記錄了最後運行的object的方法。



直接用Category覆蓋NSObject的這個方法可能會更好。

0 0