iOS Crash异常日志收集

异常日志的产生与存储

任何一款应用在使用过程中出现各种异常问题在所难免,如何能快速还原场景定位异常,对于能否及时修复问题非常重要.在apple的文档中我们发现了这样一张图,完整地描述了应用的整个流程:

  1. 编译器将源代码编译成机器代码的过程中,会生成调试符号,这些调试符号将生成的二机制文件中的每一条机器指令映射回源代码行.根据调试信息格式的构建设置(setting builds--Debug Information Format),这些调试符号会存储在二进制文件中或者附随的调试文件中(.dSYM).在默认的情况下,应用的调试版本,会将调试符号会被保存在已经编译的二进制文件中;而在发布版本中,会单独生成附随的调试符号表(.dSYM)以减小二进制文件的大小.应用在每次构建时,都会生成一个用于标记该次构建过程的唯一的uuid,调试符号和应用程序二进制文件通过UUID进行绑定.某次构建过程中生成的调试符号,即使同样的源代码,也不能操作非本次构建生成的应用二进制文件.
  2. 当打包应用程序进行分发时,Xcode将收集应用程序二进制文件以及.dSYM文件,并将它们存储在主文件夹内的某个位置。 可以在Xcode Organizer的“ Archived”部分下找到所有已存档的应用程序。 有关创建档案的更多信息,请参阅应用发布指南。如果想要符号化这些异常日志,就必须保存每次发布应用时构建的打包文件(.xcarchive文件)
  3. 如果要通过App Store分发应用程序,或使用Test Flight进行Beta测试,在将应用上传到iTunes Connect时,可以选择是否包括dSYM文件。 在提交对话框中,选中“Include app symbols for your application…”。 上传dSYM文件对于接收从TestFlight用户和选择共享诊断数据的客户收集的崩溃报告是必要的.需要注意的是,及时你上传了.dysm文件从App Review获取到的异常日志也是未符号话的,需要使用Xcode来进行符号化,请参阅使用Xcode来进行符号化;
  4. 当应用在设备上发生异常时,系统会产生异常日志并存储在手机设备上;
  5. 用户可以按照调试已部署的iOS应用中的步骤直接从其设备检索崩溃报告。 如果已经通过AdHoc或Enterprise方式分发了应用程序,则这是从用户那里获取崩溃报告的唯一方法;
  6. 从移动设备中检索到的崩溃报告没有符号化,需要使用Xcode进行符号化。 Xcode使用与应用程序二进制文件关联的dSYM文件将回溯中的每个地址替换为其源代码中的原始位置,得到的结果是一个符号化的崩溃报告;
  7. 如果用户选择与Apple共享诊断数据,或者用户已通过TestFlight安装了应用程序的Beta版本,则崩溃报告将上传到App Store;
  8. App Store象征着崩溃报告,并将其与类似的崩溃报告进行分组。 这种相似的崩溃报告的汇总称为崩溃点;
  9. 带符号的崩溃报告可在Xcode的崩溃管理器中进行查看使用.

在新的iTC后台中,已经删除了关于异常的日志的显示,只保存了闪退率的统计数据.异常日志的源头在发生异常的设备中,所以可以直接从异常设备中进行获取;另外如果用户共享了设备诊断数据,或者用户安装了TestFlight测试包,可以在Xcode中下载异常日志;当然也可以通过apple提供的接口实现自定义的异常捕获.

获取异常日志的获取途径

从手机设备获取

手机设备是异常发生的第一现场,iPhone设备在手机上生成并保存了这些异常日志.可以通过以下方式查看:

  • 直接在手机上查看

在 设置-->隐私-->分析-->分析数据中可以查看到异常,不过这里的日志是没有符号化的.可以选择右上角的分享,将文件分享到能够符号化的设备上进行符号化处理:

...
Application Specific Information:
abort() called

Last Exception Backtrace:
(0x1c1e11c30 0x1c1b2c0c8 0x1c1e6a5f4 0x1c1e74414 0x1c1cf85f0 0x1c1ce9e9c 0x100dbbce0 0x1c5e9dd80 0x1c5e9fb90 0x1c5ea5568 0x1c564d710 0x1c5af97e8 0x1c564e248 0x1c564dc78 0x1c564e064 0x1c564d8e8 0x1c5652098 0x1c5b13214 0x1c5a26e90 0x1c5b131cc 0x1c5651db0 0x1c5b130b4 0x1c5651c0c 0x1c54bd630 0x1c54bc0f4 0x1c54bd360 0x1c5ea391c 0x1c5a48d7c 0x1c6f7b014 0x1c6fa1bd0 0x1c6f860f8 0x1c6fa1864 0x1c1ab900c 0x1c1abbd50 0x1c6fc8384 0x1c6fc8030 0x1c6fc859c 0x1c1d8d260 0x1c1d8d1b4 0x1c1d8c920 0x1c1d877ec 0x1c1d87098 0x1cbef1534 0x1c5ea77ac 0x100dbbb68 0x1c1c06f30)

Thread 0 name:  Dispatch queue: com.apple.main-thread

...
  • 连接手机,在Xcode查看.

打开Xcode,在window --> Devices and Simulators --> View Devices Logs 中,

可以选择查看所有的日常日志或者该设备上的异常日志.这里获取到的异常日志是经过Xcode符号化的,所以可以清楚看到异常调用的堆栈信息:

 

从Xcode获取

如果应用已经通过app store进行发布,就可以在打包Xcode的设备上通过Xcode进行查看.

打开Xcode在,打开window --> Organizer,选中对应的应用,即可查看不同版本中的异常.

 

从集成第三方统计工具中获取

除了apple默认的收集方式之外,还可以自定义收集方式.目前市场上很多多成熟的第三方解决方案,例如Umeng,腾讯Bugly等.

以友盟2.1.1版本为例,

  • 使用cocoapods引入需要的类库(根据自己的需求也可以手动引入)
    pod 'UMCCommon', '~> 2.1.1'
    pod 'UMCSecurityPlugins'
    pod 'UMCAnalytics'
    pod 'UMCCommonLog'
  • 在应用启动时进行初始化:
#if DEBUG || ISDEV
    [UMConfigure setLogEnabled:true];
    NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
    [UMConfigure initWithAppkey:@"MOBILE_CLICK_KEY_DEVELOP" channel:bundleIdentifier];
#else
    //关闭日志打印
    [UMConfigure setLogEnabled:false];
    //使用加密方式上传日志
    [UMConfigure setEncryptEnabled:true];
    [UMConfigure initWithAppkey:MOBILE_CLICK_KEY_PRODUCTION channel:@"IOS APP"];
#endif
  • 在发布版本时,保存并向友盟后台上传符号表文件:

  • 每天抽时间去后台关注一下应用发生了哪些异常,及时定位异常并修复.

从自定义接口中获取

如果apple自带的异常收集和第三方都不能满足产品的需求,就需要自己定义捕获并收集异常iOS的异常捕获主要是通过:

//定义异常捕获函数原型
typedef void NSUncaughtExceptionHandler(NSException *exception);

//注册捕获函数
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

来实现.

NSException内部声明:

@interface NSException : NSObject <NSCopying, NSSecureCoding> {
    @private
    NSString		*name;
    NSString		*reason;
    NSDictionary	*userInfo;
    id			reserved;
}

+ (NSException *)exceptionWithName:(NSExceptionName)name reason:(nullable NSString *)reason userInfo:(nullable NSDictionary *)userInfo;
- (instancetype)initWithName:(NSExceptionName)aName reason:(nullable NSString *)aReason userInfo:(nullable NSDictionary *)aUserInfo NS_DESIGNATED_INITIALIZER;

@property (readonly, copy) NSExceptionName name;
@property (nullable, readonly, copy) NSString *reason;
@property (nullable, readonly, copy) NSDictionary *userInfo;

@property (readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (readonly, copy) NSArray<NSString *> *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

- (void)raise;

@end

其中:

  • name:是发生异常的名称,例如数据访问越界NSRangeException,参数不合法NSInvalidArgumentException等;
  • reason:是放生异常的原因,是对name的具体解释信息,例如当试图在字典中插入nil时
-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[3]
  • userInfo:用户信息,一般用于自定义异常时的传递信息;
  • callStackSymbols:产生异常的调用堆栈,调用的顺序自下往上;
  • raise:用于抛出异常,中断执行.

大致实现如下:

@interface ExceptionHandler : NSObject
+ (void)registerExceptionHandler;
@end

@implementation ExceptionHandler
+ (void)registerExceptionHandler {
    void(^dealException)(NSException *) = ^(NSException *exception) {
        NSLog(@"name:%@", exception.name);
        NSLog(@"reason:%@", exception.reason);
        NSLog(@"userInfo:%@", exception.userInfo);
        NSLog(@"callStackSymbols:%@", exception.callStackSymbols);
        //保存信息到本地,在需要的时候发送信息到服务器
    };
    IMP uncaughExceptionHandler =  imp_implementationWithBlock(dealException);
    NSSetUncaughtExceptionHandler((void(*)(NSException *))uncaughExceptionHandler);
}

@end

这里有一个细节问题:系统提供了捕获系统异常的方法,但是只有这一个方法,如果应用中有多个捕获异常的实现,那么先注册的异常捕获方法就会被后来注册的方法覆盖掉,造成之前的注册的异常捕获方法不能执行,所以负责的注册方法是在自己注册方法之前保存之前的捕获实现,在异常处理方法中,调用原来的异常捕获实现,从而确保每一个注册的异常捕获方法都在可以执行.

@interface ExceptionHandler : NSObject
+ (void)registerExceptionHandler;
@end

@implementation ExceptionHandler
+ (void)registerExceptionHandler {
    static NSUncaughtExceptionHandler *originalExceptionHandler;
    originalExceptionHandler = NSGetUncaughtExceptionHandler();
    void(^dealException)(NSException *) = ^(NSException *exception) {
        if (originalExceptionHandler) {
            originalExceptionHandler(exception);
        }
        NSLog(@"name:%@", exception.name);
        NSLog(@"reason:%@", exception.reason);
        NSLog(@"userInfo:%@", exception.userInfo);
        NSLog(@"callStackSymbols:%@", exception.callStackSymbols);
        //保存信息到本地,在需要的时候发送信息到服务器(实际操作中还需要保存对应的设备类型以确定所使用的指令集等信息)
        
    };
    IMP uncaughExceptionHandler =  imp_implementationWithBlock(dealException);
    NSSetUncaughtExceptionHandler((void(*)(NSException *))uncaughExceptionHandler);
}

@end

从审核反馈获取

如果你提交的应用在审核期间出现了闪退的情况,据说一般审核人员会把设备上的异常日志附加在附件中反馈给开发者,不过尽量做好测试工作不要收到这种类型的信息.

 

 

 

 

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