最近準備在app中加個功能,就是當(dāng)app crash的時候?qū)rash信息發(fā)送郵件給開發(fā)者。目前我們app中使用的異常檢測工具是Bugly,這個工具還是挺好用的,界面很清晰,對crash分析也比較到位,還可以綁定微信,將每日app的崩潰情況定時發(fā)送給開發(fā)者。不得不說騰訊的產(chǎn)品還是很任性,符合咱們的用戶習(xí)慣。但是沒有即時發(fā)送崩潰信息的功能。所以決定自己想想辦法看能不能找到方法。
在iOS中發(fā)送郵件系統(tǒng)自帶有兩個方式,openURL和MFMailComposeViewController,這兩種方式都不是很好,因為要彈出別的界面,顯然不符合我們靜默的需求。網(wǎng)上說使用SKPSMTPMessage可以實現(xiàn),試了一下,確實可以,但是里面坑很多,很多文章沒有講清楚,這里就介紹一下要注意的一些地方吧。最后給出了Demo[GHWSendEmail]
1. 配置發(fā)收郵箱相關(guān)信息
-(void)sendEmail:(NSString*)content
{
SKPSMTPMessage *myMessage = [[SKPSMTPMessage alloc] init];
myMessage.delegate = self;
myMessage.fromEmail = @"guohongwei719@126.com";//發(fā)送者郵箱
myMessage.pass = @"********";//發(fā)送者郵箱的密碼
myMessage.login = @"guohongwei719";//發(fā)送者郵箱的用戶名
myMessage.toEmail = @"guohongwei719@126.com";//收件郵箱
//myMessage.bccEmail = @"******@qq.com";//抄送
myMessage.relayHost = @"smtp.126.com";
myMessage.requiresAuth = YES;
myMessage.wantsSecure = YES;//為gmail郵箱設(shè)置 smtp.gmail.com
myMessage.subject = @"iOS崩潰日志";//郵件主題
/* >>>>>>>>>>>>>>>>>>>> * 設(shè)置郵件內(nèi)容 * <<<<<<<<<<<<<<<<<<<< */
NSDictionary *plainPart = [NSDictionary dictionaryWithObjectsAndKeys:@"text/plain; charset=UTF-8",kSKPSMTPPartContentTypeKey, content,kSKPSMTPPartMessageKey,@"8bit",kSKPSMTPPartContentTransferEncodingKey,nil];
myMessage.parts = [NSArray arrayWithObjects:plainPart,nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[myMessage send];
});
}
發(fā)送郵件的時候配置信息非常關(guān)鍵,這里發(fā)送者郵箱要多試一試,有的可以,有的不可以,不要一個不行就放棄了,網(wǎng)上很多文章沒強調(diào)這一點。我開始也是放棄了,后來發(fā)現(xiàn)別人可以,才多試了試發(fā)現(xiàn)是可以的,這里一定要注意。反正試了幾個126的郵箱是可以的,163的有的不行。使用相應(yīng)的郵箱要配置對應(yīng)的代理服務(wù)器主機,比如126的郵箱就是smtp.126.com,163的郵箱是smtp.163.com,QQ郵箱是smtp.qq.com。最后還是強調(diào)下配置郵箱,用126多試試。
SKPSMTPMessageDelegate里面有兩個方法,提供了發(fā)送郵件成功和失敗的回調(diào):
#pragma mark - SKPSMTPMessageDelegate
- (void)messageSent:(SKPSMTPMessage *)message
{
NSLog(@"發(fā)送郵件成功");
[[GHWCrashHandler sharedInstance] configDismissed];
}
- (void)messageFailed:(SKPSMTPMessage *)message error:(NSError *)error
{
NSLog(@"message - %@\nerror - %@", message, error);
}```
##2. App crash的時候發(fā)送郵件
iOS里面捕獲異常的方法如下
void UncaughtExceptionHandler(NSException *exception) {
NSArray *arr = [exception callStackSymbols]; // 得到當(dāng)前的調(diào)用棧信息
NSString *reason = [exception reason];//非常重要,就是崩潰的原因
NSString *name = [exception name];//異常類型
NSLog(@"exception type : %@ n crash reason : %@ n call stack info : %@", name, reason, arr);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler (&UncaughtExceptionHandler);
return YES;
}
在上面我們已經(jīng)配置好了相關(guān)信息,可以直接發(fā)送郵件了,那是不是在這里捕獲到異常以后直接調(diào)用發(fā)送郵件就可以了呢?答案是錯的。這里直接發(fā)郵件是發(fā)布出去的。最后在網(wǎng)上找了一個方法,可以解決這個問題,使用到了RunLoop。還是先建一個類了,代碼如下:
GHWCrashHandler.h
import <Foundation/Foundation.h>
import <UIKit/UIKit.h>
@interface GHWCrashHandler : NSObject
{
BOOL dismissed;
}
- (void)configDismissed;
- (GHWCrashHandler *)sharedInstance;
void InstallCrashExceptionHandler();
@end```
GHWCrashHandler.m
#import "GHWCrashHandler.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>
#import "GHWEmailManager.h"
NSString * const YDCrashHandlerSignalExceptionName = @"YDCrashHandlerSignalExceptionName";
NSString * const YDCrashHandlerSignalKey = @"YDCrashHandlerSignalKey";
NSString * const YDCrashHandlerAddressesKey = @"YDCrashHandlerAddressesKey";
volatile int32_t UncaughtExceptionCount = 0;
const int32_t UncaughtExceptionMaximum = 10;
const NSInteger UncaughtExceptionHandlerSkipAddressCount = 4;
const NSInteger UncaughtExceptionHandlerReportAddressCount = 5;
@implementation GHWCrashHandler
+ (GHWCrashHandler *)sharedInstance
{
static GHWCrashHandler *crashHandler;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
crashHandler = [[GHWCrashHandler alloc] init];
});
return crashHandler;
}
- (void)configDismissed
{
dismissed = YES;
}
+ (NSArray *)backtrace
{
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (
i = UncaughtExceptionHandlerSkipAddressCount;
i < UncaughtExceptionHandlerSkipAddressCount +
UncaughtExceptionHandlerReportAddressCount;
i++)
{
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
- (void)alertView:(UIAlertView *)anAlertView clickedButtonAtIndex:(NSInteger)anIndex
{
dismissed = YES;
}
- (void)handleException:(NSException *)exception
{
NSArray *arr = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
NSDate *nowDate = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString *nowDateString = [formatter stringFromDate:nowDate];
NSString *strError = [NSString stringWithFormat:@"\n\n\n=============異常崩潰報告=============\n崩潰發(fā)生的時間:\n %@\n崩潰名稱:\n%@\n崩潰原因:\n%@\n堆棧信息:\n%@" ,nowDateString,name,reason, arr];
[[GHWEmailManager shareInstance] sendEmail:strError];
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!dismissed)
{
for (NSString *mode in (NSArray *)CFBridgingRelease(allModes))
{
CFRunLoopRunInMode((CFStringRef)CFBridgingRetain(mode), 0.001, false);
}
}
CFRelease(allModes);
NSSetUncaughtExceptionHandler(NULL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
if ([[exception name] isEqual:YDCrashHandlerSignalExceptionName])
{
kill(getpid(), [[[exception userInfo] objectForKey:YDCrashHandlerSignalKey] intValue]);
}
else
{
[exception raise];
}
}
@end
void HandleException(NSException *exception)
{
int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount);
if (exceptionCount > UncaughtExceptionMaximum) {
return;
}
NSArray *callStack = [GHWCrashHandler backtrace];
NSMutableDictionary *userInfo =
[NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
[userInfo setObject:callStack forKey:YDCrashHandlerAddressesKey];
[[[GHWCrashHandler alloc] init] performSelectorOnMainThread:@selector(handleException:)
withObject:[NSException exceptionWithName:[exception name]
reason:[exception reason]
userInfo:userInfo]
waitUntilDone:YES];
}
void SignalHandler(int signal)
{
int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount);
if (exceptionCount > UncaughtExceptionMaximum) {
return;
}
NSMutableDictionary *userInfo =
[NSMutableDictionary dictionaryWithObject:[NSNumber numberWithInt:signal] forKey:YDCrashHandlerSignalKey];
NSArray *callStack = [GHWCrashHandler backtrace];
[userInfo setObject:callStack forKey:YDCrashHandlerAddressesKey];
[[[GHWCrashHandler alloc] init] performSelectorOnMainThread:@selector(handleException:)
withObject:[NSException
exceptionWithName:YDCrashHandlerSignalExceptionName
reason:[NSString stringWithFormat:@"Signal %d was raised.", signal]
userInfo:[NSDictionary
dictionaryWithObject:[NSNumber numberWithInt:signal]
forKey:YDCrashHandlerSignalKey]]
waitUntilDone:YES];
}
void InstallCrashExceptionHandler()
{
NSSetUncaughtExceptionHandler(&HandleException);
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
}
```
然后App啟動的時候還要注冊一下
```
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
InstallCrashExceptionHandler();
return YES;
}
```
這樣就可以了,在SMTPLibrary的發(fā)送成功回調(diào)方法里面,我們還設(shè)置了
[[GHWCrashHandler sharedInstance] configDismissed];
這樣app發(fā)送了郵件后就可以真正crash了,回到手機桌面,不會停留在當(dāng)前界面,點不動。不過在我的demo中發(fā)現(xiàn)還是不行,在我的項目里面是可以的,后面我再找找原因。
