宏定義以及調試相關

---------------------------------------------------------

大部分人調試程序都是看日誌吧,這裏我就給大家總結一下iphone程序中添加保存日誌的方法。

Objective-C開發程序的時候,有專門的日誌操作類NSLog,它將指定的輸出到標準的錯誤輸出上(stderr)。我們可以利用它在Xcode的日誌輸出窗口,或者是輸出到具體的文件當中。

下面是我在程序中常用到的日誌宏,用DEBUG開關管理,也就是說只有在DEBUG模式下才讓日誌輸出 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifdef DEBUG
#  define LOG(fmt, ...) do {                                            \
        NSString* file = [[NSString alloc] initWithFormat:@"%s", __FILE__]; \
        NSLog((@"%@(%d) " fmt), [file lastPathComponent], __LINE__, ##__VA_ARGS__); \
        [file release];                                                 \
    } while(0)
#  define LOG_METHOD NSLog(@"%s", __func__)
#  define LOG_CMETHOD NSLog(@"%@/%@", NSStringFromClass([self class]), NSStringFromSelector(_cmd))
#  define COUNT(p) NSLog(@"%s(%d): count = %d\n", __func__, __LINE__, [p retainCount]);
#  define LOG_TRACE(x) do {printf x; putchar('\n'); fflush(stdout);} while (0)
#else
#  define LOG(...)
#  define LOG_METHOD
#  define LOG_CMETHOD
#  define COUNT(p)
#  define LOG_TRACE(x)
#endif

可以看到,除了標準的用戶定義輸出外,我還加入了許多有用的信息,比如源程序文件位置,行號,類名,函數名等。具體的應用可以在具體的開發過程中添加、刪除。

真機測試的時候,可以利用freopen將標準錯誤輸出保存到指定的文件當中,這樣就可以在問題發生後分析日誌文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)redirectNSLogToDocumentFolder{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *fileName =[NSString stringWithFormat:@"%@.log",[NSDate date]];
    NSString *logFilePath = [documentsDirectory stringByAppendingPathComponent:fileName];
    freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
}

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    // 真機測試時保存日誌
    if ([CDeviceInfo getModelType] != SIMULATOR) {
        [self redirectNSLogToDocumentFolder];
    }

    .....
}

--------------------------------------------------------

程序crash後的調試:

模擬器上顯示堆棧信息

當我們在模擬器上調試時,可能經常遇到下面的內存訪問錯誤:

1
2011-01-17 20:21:11.41 App[26067:207] *** -[Testedit retain]: message sent to deallocated instance 0x12e4b0

dealloced

首先,我們爲了定位問題,需要Xcode幫我們顯示棧信息,可以通過Scode中執行文件的屬性來設置。如下圖所示,選中 MallocStackLogging 選項。該選項只能在模擬器上有效,並且如果你改變了iOS的版本後也需要再次設定該選項。

MallocStackLogging

這之後,你就可以在終端輸入 info malloc-history 命令,如下所示;

1
(gdb) info malloc-history 0x12e4b0

之後得到如下的堆棧信息,從此分析具體的問題所在。

MallocStackLogging

除此之外,也可以使用下面的命令;

1
(gdb) shell malloc_history {pid/partial-process-name} {address}

例如下圖所示;

EXC_BAD_ACCESS

另外,內存使用時“EXC_BAD_ACCESS”的錯誤信息也是經常遇到的,這時我們只要將上面執行文件屬性中的 NSZombieEnabled 選上,也能定位該問題。

最後,這些設置信息都是可以在運行期確認的,如下;

1
NSLog(@"NSZombieEnabled: %s", getenv("NSZombieEnabled"));


------------------------------------------------

在iPhone上輸出日誌

如果不是在模擬器上,又或者我們的設備沒有連接到PC上,那麼如何調試我們的程序呢?假如我們通過AdHoc發佈了程序,希望隨時得到測試人員的反饋,可以利用下面的方法,將標準出力(stderr)信息記錄到文件中,然後通過郵件新式發給開發者。

1. 設置一個開關,用來清空日誌文件內容,並切換輸出位置;

1
2
3
4
5
6
7
8
- (BOOL)deleteLogFile {

    [self finishLog];
    BOOL success = [[NSFileManager defaultManager] removeItemAtPath:[self loggingPath] error:nil];
    [self startLog];
    return success;

}

當我們調用上面的deleteLogFile後,就會清空之前的日誌,並設置新的輸出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)finishLog {

    fflush(stderr);
    dup2(dup(STDERR_FILENO), STDERR_FILENO);
    close(dup(STDERR_FILENO));

}

- (NSString*)loggingPath {

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *logPath = [documentsDirectory stringByAppendingPathComponent:@"console.log"];
    return logPath;

}

- (void)startLog {
   freopen([[self loggingPath] cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
}

2. 當日志取得之後,可以通過下面的方式發送郵件給開發者

1
2
3
4
5
6
7
8
9
10
11
- (void)sendLogByMail {

    MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
    picker.mailComposeDelegate = self;
    [picker setSubject:[NSString stringWithFormat:@"%@ - Log", [self appName]]];
    NSString *message = [NSString stringWithContentsOfFile:[self loggingPath] encoding:NSUTF8StringEncoding error:nil];
    [picker setMessageBody:message isHTML:NO];
    [self.navigationController presentModalViewController:picker animated:YES];
    [picker release];

}


------------------------------------------------------

iPhone應用程序的CrashReporter機能

蘋果在固件2.0發佈的時候,其中一項特性是向iPhone開發者通過郵件發送錯誤報告,以便開發人員更好的瞭解自己的軟件運行狀況。不過不少開發者報告此服務有時無法獲取到~/Library/Logs/CrashReporter/MobileDevice directory的錯誤信息。

現在蘋果提供了一種更簡單的方法,使iPhone開發者可以通過iTunes更容易的查看崩潰報告。具體方法使進入iTunesConnect(在進入之前確定你有iPhone開發者帳號),點擊管理你應用程序,之後就可以看到用戶崩潰日誌了。

這裏我介紹一下從設備中取出CrashLog,並解析的方法。

CrashLog的位置

程序Crash之後,將設備與PC中的iTunes連接,設備中的CrashLog文件也將一併同步到PC中。其中位置如下;

1
2
3
4
5
6
7
8
Mac:
~/Library/Logs/CrashReporter/MobileDevice

Windows Vista/7:
C:\Users\<user_name>\AppData\Roaming\Apple computer\Logs\CrashReporter/MobileDevice

Windows XP:
C:\Documents and Settings\<user_name>\Application Data\Apple computer\Logs\CrashReporter

在這些目錄下,會有具體設備的目錄,其下就是許多*.crash的文件。

比如程序TestEditor在iPhone1設備上的crashLog如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
~Library/Logs/CrashReporter/MobileDevice/iPhone1/TestEditor_2010-09-23-454678_iPhone1.crash

Incident Identifier: CAF9ED40-2D59-45EA-96B0-52BDA1115E9F
CrashReporter Key:   30af939d26f6ecc5f0d08653b2aaf47933ad8b8e
Process:         TestEditor [12506]
Path:            /var/mobile/Applications/60ACEDBC-600E-42AF-9252-42E32188A044/TestEditor.app/TestEditor
Identifier:      TestEditor
Version:         ??? (???)
Code Type:       ARM (Native)
Parent Process:  launchd [1]

Date/Time:       2010-09-23 11:25:56.357 +0900
OS Version:      iPhone OS 3.1.3 (7E18)
Report Version:  104

Exception Type:  EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x00000059
Crashed Thread:  0

Thread 0 Crashed:
0   UIKit                           0x332b98d8 0x331b2000 + 1079512
1   UIKit                           0x3321d1a8 0x331b2000 + 438696
2   UIKit                           0x3321d028 0x331b2000 + 438312
3   UIKit                           0x332b9628 0x331b2000 + 1078824
4   UIKit                           0x33209d70 0x331b2000 + 359792
5   UIKit                           0x33209c08 0x331b2000 + 359432
6   QuartzCore                      0x324cc05c 0x324ae000 + 122972
7   QuartzCore                      0x324cbe64 0x324ae000 + 122468
8   CoreFoundation                  0x3244f4bc 0x323f8000 + 357564
9   CoreFoundation                  0x3244ec18 0x323f8000 + 355352
10  GraphicsServices                0x342e91c0 0x342e5000 + 16832
11  UIKit                           0x331b5c28 0x331b2000 + 15400
12  UIKit                           0x331b4228 0x331b2000 + 8744
13  TestEditor                      0x00002c3a 0x1000 + 7226
14  TestEditor                      0x00002c04 0x1000 + 7172
... (以下略)

雖然我們看到了出爲題時的堆棧信息,但是因爲沒有符號信息,仍然不知道到底哪裏出問題了...

.dSYM文件

編譯調試相關的符號信息都被包含在編譯時的 xxxx.app.dSYM 文件當中,所以我們在發佈程序前將它們保存起來,調試Crash問題的時候會很有用。

首先,我們來找到該文件。

用Xcode編譯的程序,在其編譯目錄下都會生成 [程序名].app.dSMY 文件,比如 Xcode 4 的編譯目錄缺省的是

1
2
3
4
5
~Library/Developer/Xcode/DerivedData

# 在改目錄下搜尋編譯後的.dSMY文件
$ cd ~/Library/Developer/Xcode/DerivedData
$ find . -name '*.dSYM'

另外,我們也可以通過 Xcode的Preferences... -> Locations -> Locations 的Derived Data來確認該目錄的位置。

上面例子中的程序,我們就找到了其位置是
1
~/Library/Developer/Xcode/DerivedData/TestEditor-aahmlrjpobenlsdvhjppcfqhogru/ArchiveIntermediates/TestEditor/BuildProductsPath/Release-iphoneos/TestEditor.app.dSYM

※ 大家每次像App Store發佈自己程序的時候都記着保存該文件哦,要不然出現Crash的時候,就無從下手了。

解決符號問題

接下來,我們再來介紹一下使用.dSYM文件來恢復程序符號的方法。

首先,使用一個Xcode提供的叫做 symbolicatecrash 的小工具,它可以實現我們在CrashLog中添加符號信息的機能。該文件位於下面的位置,爲方便起見,可以把它拷貝到系統默認路徑下。

1
2
3
/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash

$ sudo cp /Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash /usr/local/bin
使用下面的命令,可以在終端輸出有符號信息的CrashLog
1
2
3
$ symbolicatecrash [CrashLog file] [dSYM file]

$ symbolicatecrash TestEditor_2010-09-23-454678_iPhone1.crash TestEditor.app.dSYM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Thread 0 Crashed:
0   UIKit                           0x332b98d8 -[UIWindowController transitionViewDidComplete:fromView:toView:] + 668
1   UIKit                           0x3321d1a8 -[UITransitionView notifyDidCompleteTransition:] + 160
2   UIKit                           0x3321d028 -[UITransitionView _didCompleteTransition:] + 704
3   UIKit                           0x332b9628 -[UITransitionView _transitionDidStop:finished:] + 44
4   UIKit                           0x33209d70 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 284
5   UIKit                           0x33209c08 -[UIViewAnimationState animationDidStop:finished:] + 60
6   QuartzCore                      0x324cc05c _ZL23run_animation_callbacksdPv + 440
7   QuartzCore                      0x324cbe64 _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv + 156
8   CoreFoundation                  0x3244f4bc CFRunLoopRunSpecific + 2192
9   CoreFoundation                  0x3244ec18 CFRunLoopRunInMode + 44
10  GraphicsServices                0x342e91c0 GSEventRunModal + 188
11  UIKit                           0x331b5c28 -[UIApplication _run] + 552
12  UIKit                           0x331b4228 UIApplicationMain + 960
13  TestEditor                      0x00002c3a main (main.m:14)
14  TestEditor                      0x00002c04 0x1000 + 7172

由此,我們可以具體定位程序中出問題的地方。

用StackTrace取得崩潰時的日誌

異常處理機制

任何語言都有異常的處理機制,Objective-C也不例外。與C++/Java類似的語法,它也提供@try, @catch, @throw, @finally關鍵字。使用方法如下。

1
2
3
4
5
6
7
8
9
10
11
12
@try {
...   }
@catch (CustomException *ce) {
...   }
@catch (NSException *ne) {
// Perform processing necessary at this level.
...  }
@catch (id ue) {
...   }
@finally {
// Perform processing necessary whether an exception occurred or not.
...   }

同時對於系統Crash而引起的程序異常退出,可以通過UncaughtExceptionHandler機制捕獲;也就是說在程序中catch以外的內容,被系統自帶的錯誤處理而捕獲。我們要做的就是用自定義的函數替代該ExceptionHandler即可。

這裏主要有兩個函數
  • NSGetUncaughtExceptionHandler() 得到現在系統自帶處理Handler;得到它後,如果程序正常退出時用來回復系統原先設置
  • NSSetUncaughtExceptionHandler() 紅色設置自定義的函數
簡單的使用例子如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void MyUncaughtExceptionHandler(NSException *exception)
{
    printf("uncaught %s\n", [[exception name] cString]);
    // 
    // 顯示當前堆棧內容
    NSArray *callStackArray = [exception callStackReturnAddresses];
    int frameCount = [callStackArray count];
    void *backtraceFrames[frameCount];

    for (int i=0; i<frameCount; i++) {
        backtraceFrames[i] = (void *)[[callStackArray objectAtIndex:i] unsignedIntegerValue];
    }
}

int main()
{
    //     NSUncaughtExceptionHandler *ueh = NSGetUncaughtExceptionHandler();
    NSSetUncaughtExceptionHandler(&MyUncaughtExceptionHandler);
    // }

- (void)exit_processing:(NSNotification *)notification {
    NSSetUncaughtExceptionHandler(ueh);
}

- (void)viewDidLoad {
    // 這裏重載程序正常退出時UIApplicationWillTerminateNotification接口
    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(exit_processing:)
                                                 name:UIApplicationWillTerminateNotification
                                               object:app]
}

處理signal

使用Objective-C的異常處理是不能得到signal的,如果要處理它,我們還要利用unix標準的signal機制,註冊SIGABRT, SIGBUS, SIGSEGV等信號發生時的處理函數。該函數中我們可以輸出棧信息,版本信息等其他一切我們所想要的。

例子代碼如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <signal.h>

void stacktrace(int sig, siginfo_t *info, void *context)
{
    [mstr appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <; frames; ++i) {
        [mstr appendFormat:@"%s\n", strs[i]];
    }
}

int main(int argc, char *argv[])
{
      struct sigaction mySigAction;
      mySigAction.sa_sigaction = stacktrace;
      mySigAction.sa_flags = SA_SIGINFO;

      sigemptyset(&mySigAction.sa_mask);
      sigaction(SIGQUIT, &mySigAction, NULL);
      sigaction(SIGILL , &mySigAction, NULL);
      sigaction(SIGTRAP, &mySigAction, NULL);
      sigaction(SIGABRT, &mySigAction, NULL);
      sigaction(SIGEMT , &mySigAction, NULL);
      sigaction(SIGFPE , &mySigAction, NULL);
      sigaction(SIGBUS , &mySigAction, NULL);
      sigaction(SIGSEGV, &mySigAction, NULL);
      sigaction(SIGSYS , &mySigAction, NULL);
      sigaction(SIGPIPE, &mySigAction, NULL);
      sigaction(SIGALRM, &mySigAction, NULL);
      sigaction(SIGXCPU, &mySigAction, NULL);
      sigaction(SIGXFSZ, &mySigAction, NULL);

      NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
      int retVal = UIApplicationMain(argc, argv, nil, nil);
      [pool release];
      return (retVal);
}

總結

綜上所述,我們可以看到用StackTrace取得崩潰時日誌的手順如下
  • 用NSGetUncaughtExceptionHandler()取得當前系統異常處理Handler
  • 用NSSetUncaughtExceptionHandler()註冊自定義異常處理Handler
  • 註冊signal處理機制
    • 註冊Handler中打印堆棧,版本號等信息
    • 必要的時候將其保存到dump.txt文件
  • 異常程序退出
  • 如果程序不是異常退出,則還原之前系統的異常處理函數句柄
  • 如果下次程序啓動,發現有dump.txt的異常文件,啓動郵件發送報告機制
整體的代碼框架如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <signal.h>

void stacktrace(int sig, siginfo_t *info, void *context)
{
    [mstr appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <; frames; ++i) {
        [mstr appendFormat:@"%s\n", strs[i]];
    }
}

void MyUncaughtExceptionHandler(NSException *exception)
{
    printf("uncaught %s\n", [[exception name] cString]);
    // 
    // 顯示當前堆棧內容
    NSArray *callStackArray = [exception callStackReturnAddresses];
    int frameCount = [callStackArray count];
    void *backtraceFrames[frameCount];

    for (int i=0; i<frameCount; i++) {
        backtraceFrames[i] = (void *)[[callStackArray objectAtIndex:i] unsignedIntegerValue];
    }
}

int main(int argc, char *argv[])
{
      struct sigaction mySigAction;
      mySigAction.sa_sigaction = stacktrace;
      mySigAction.sa_flags = SA_SIGINFO;

      sigemptyset(&mySigAction.sa_mask);
      sigaction(SIGQUIT, &mySigAction, NULL);
      sigaction(SIGILL , &mySigAction, NULL);
      sigaction(SIGTRAP, &mySigAction, NULL);
      sigaction(SIGABRT, &mySigAction, NULL);
      sigaction(SIGEMT , &mySigAction, NULL);
      sigaction(SIGFPE , &mySigAction, NULL);
      sigaction(SIGBUS , &mySigAction, NULL);
      sigaction(SIGSEGV, &mySigAction, NULL);
      sigaction(SIGSYS , &mySigAction, NULL);
      sigaction(SIGPIPE, &mySigAction, NULL);
      sigaction(SIGALRM, &mySigAction, NULL);
      sigaction(SIGXCPU, &mySigAction, NULL);
      sigaction(SIGXFSZ, &mySigAction, NULL);

      //       NSUncaughtExceptionHandler *ueh = NSGetUncaughtExceptionHandler();
      NSSetUncaughtExceptionHandler(&MyUncaughtExceptionHandler);
      // 
      NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
      int retVal = UIApplicationMain(argc, argv, nil, nil);
      [pool release];
      return (retVal);
}

- (void)exit_processing:(NSNotification *)notification {
    NSSetUncaughtExceptionHandler(ueh);
}

- (void)viewDidLoad {
    // 這裏重載程序正常退出時UIApplicationWillTerminateNotification接口
    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(exit_processing:)
                                                 name:UIApplicationWillTerminateNotification
                                               object:app]
}
輸入的CrashLog如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Signal:10
Stack:
0 TestEditor 0x0006989d dump + 64
1 TestEditor 0x00069b4b signalHandler + 46
2 libSystem.B.dylib 0x31dcd60b _sigtramp + 26
3 TestEditor 0x000252b9 -[PopClientcreateUnreadMessageWithUIDL:maxMessageCount:] + 76
4 TestEditor 0x00025b85 -[PopClientgetUnreadIdList:] + 348
5 TestEditor 0x000454dd -[Connection receiveMessages:] + 688
6 TestEditor 0x00042db1 -[Connection main] + 188
7 Foundation 0x305023f9 __NSThread__main__ + 858
8 libSystem.B.dylib 0x31d6a5a8 _pthread_body + 28
AppVer:TestEditor 1.2.0
System:iPhone OS
OS Ver:3.0
Model:iPhoneDate:09/06/08 21:25:59JST

其中從_sigtramp函數下面開始進入我們的程序,即地址0x000252b9開始。其所對應的具體文件名和行號我們能知道嗎?

利用之前介紹的dSYM文件和gdb,我們可以得到這些信息。

1
2
3
4
5
6
7
cd $PROJ_PATH$/build/Release-iphoneos/TestEditor.app.dSYM/
cd Contents/Resources/DWARF
gdb TestEditor
gdb>info line *0x000252b9
Line 333 of "~/IbisMail/Classes/Models/PopClient.m";
starts at address 0x2a386 <-[PopClient retrieve:]+86> and
ends at 0x2a390 <-[PopClient retrieve:]+96>


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章