iOS实录14:浅谈iOS Crash(一)

[这是第14篇]

序: iOS Crash问题是iOS开发中难以忽视的存在,本文就捕获iOS CrashCrash日志组成Crash日志符号化异常信息解读常见的Crash五部分介绍。

一、捕获iOS Crash

1、设置异常断点并运行
设置异常断点.png

说明:设置Xcode异常断点后运行程序,发生Crash时,断点会定位到出错的代码行,但仅适用于开发阶段。线上APP的Crash还需要通过收集Crash机制来捕获Crash并记录在日志中。

2、Mach异常 、Unix Signal 和 NSException

1) Mach异常是最底层的内核级异常,如EXC_BAD_ACCESS(内存访问异常),可以参考 Mach的exception_types定义

2) Unix Signal是Unix系统中的一种异步通知机制,Mach异常在host层被ux_exception转换为相应的Unix Signal,并通过threadsignal将信号投递到出错的线程。如SIGABRT(程序中止命令中止信号)、 SIGALRM(程序超时信号),具体信号枚举在iOS的sys/signal.h文件中。 它们可以利用Unix标准的signal机制来处理。

3) NSException是OC层,由iOS库或者各种第三方库或Runtime验证出错误而抛出的异常。如NSRangeException(数组越界异常),它们可以被try catch捕获(苹果不建议用),如果未被捕获或被@throw抛出,可以通过注册NSSetUncaughtExceptionHandler函数来捕获处理。

4) 当错误发生时候,先在最底层产生Mach异常;Mach异常在host层被转换为相应的Unix Signal; 在OC层如果有对应的NSException(OC异常),就转换成OC异常,OC异常可以在OC层得到处理;如果OC异常一直得不到处理,程序会强行发送SIGABRT信号中断程序。在OC层如果没有对应的NSException,就只能让Unix标准的signal机制来处理了。

5) 在捕获Crash事件时,优选Mach异常。因为Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。而转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样就不必了解Mach内核也可以通过Unix信号的方式来兼容开发。

6) 在方案实现时,通过捕获Mach异常+Unix信号组合方式来捕获Crash事件。在选择具体方案时,可以选择PLCrashReporter这样优秀的开源项目,也可以选择友盟Bugly 这类完善的Crash上报和统计的产品(试项目需求而定)。

3、捕获Crash

NSException是OC层的异常。 在OC中虽然可以通过try catch捕获NSException,阻止其继续往外抛而导致Crash,但是苹果不建议这么做。不使用try catch,就意味着放弃捕获OC层的异常。最后只能注册NSSetUncaughtExceptionHandler这个函数来捕获,记录异常信息,帮助解决问题。

1) OC层中未被捕获的异常,通过注册NSUncaughtExceptionHandler捕获异常信息

//注册异常处理函数
NSSetUncaughtExceptionHandler(&uncaught_exception_handler);
//异常处理函数
static void uncaught_exception_handler (NSException *exception) {
  //可以取到 NSException 信息
  //...
  abort();
}

2) OC中层不能转换的Mach异常,利用Unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。

//注册处理SIGSEGV信号
signal(SIGSEGV,handleSignal); 
// 注册处理其他信号 ....

//信号处理函数
static void handleSignal( int sig ) {

}

二、Crash日志组成

上部分介绍了Crash的捕获,这部分来看看Crash日志的组成。

1、日志内容Demo#####

日志主要分为六个部分:进程信息基本信息异常信息线程回溯线程状态二进制映像。下面是从某APP具体的Crash日志抽出的主要信息,展示如下:

//1、进程信息
Hardware Model: iPhone9,2
Process: AppName [3580]
Path: /var/containers/Bundle/Application/C7B90C8A-E269-4413-A011-552971D1ED39/AppName.app
Identifier: xxxx.xxx.xxxx.xxx
Version: xx.xx
Code Type: ARM-64 (Native)
Parent Process:  [1]

//2、基本信息
Date/Time: 2017-05-22 03:05:06.743 +0800
OS Version: iPhone OS 10.2.1 (14D27)

//3、异常信息
Exception Type: NSInvalidArgumentException(SIGABRT)
Exception Codes: -[NSNull integerValue]: unrecognized selector sent to instance 0x1a9d88ef8 at 0x00000001835c7014
Crashed Thread: 0

//4、线程回溯 (展示发生Crash线程的回溯信息,其他略)
Thread 0 Crashed: 
0  libsystem_kernel.dylib         0x00000001835c7014 __pthread_kill + 4
1  libsystem_c.dylib              0x000000018353b400 abort + 140
2  AppName                         0x0000000100a26704 0x0000000100028000 + 10479360
3  CoreFoundation                 0x00000001845f9538 ___handleUncaughtException +  644
2  CoreFoundation                 0x0000000184600268 ___methodDescriptionForSelector
3  CoreFoundation                 0x00000001845fd270 ____forwarding___ +  916
4  CoreFoundation                 0x00000001844f680c _CF_forwarding_prep_0 + 80
5  AppName                         0x0000000100205280 0x0000000100028000 + 1954432
6  AppName                         0x00000001002ae59c 0x0000000100028000 + 2647440
7  AppName                         0x0000000100482944 0x0000000100028000 + 4565312
16 CoreFoundation                 0x00000001845a6810 ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ +  12
     +  12
17 CoreFoundation                 0x00000001845a43fc ___CFRunLoopRun +  1660
18 CoreFoundation                 0x00000001844d22b8 CFRunLoopRunSpecific + 436

//5、进程状态(展示部分)
Thread 0 crashed with ARM 64 Thread State:
     x0:  000000000000000000    x1: 000000000000000000    x2: 000000000000000000     x3: 0xffffffffffffffff
     x4:  0x0000000000000010    x5: 0x0000000000000020    x6: 000000000000000000     x7: 000000000000000000
     x8:  0x0000000008000000    x9: 0x0000000004000000   x10: 000000000000000000    x11: 0x00000001ac336c83
    x12: 0x00000001ac336c83    x13: 0x0000000000000018   x14: 0x0000000000000001    x15: 0x0000000000000881
    x16: 0x0000000000000148    x17: 000000000000000000   x18: 000000000000000000    x19: 0x0000000000000006

//6、二进制映像 (展示部分)
Binary Images:
0x100028000 - 0x1011dbfff +AppName arm64 <ff7a4009322b386ea3e8e9a3fde05be4> /var/containers/Bundle/Application/C7B90C8A-E269-4413-A011-552971D1ED39/AppName.app/AppName
0x18368a000 - 0x183693fff  libsystem_pthread.dylib arm64 <258dc0c51499393bba7ba3e83dc5bfbb> /usr/lib/system/libsystem_pthread.dylib
0x1835a8000 - 0x1835ccfff  libsystem_kernel.dylib arm64 <1baa3f5629c43467879d4cf463a20b06> /usr/lib/system/libsystem_kernel.dylib
0x1834b1000 - 0x1834b5fff  libdyld.dylib arm64 <db54f120486a3710a684ce8bb1cb9d71> /usr/lib/system/libdyld.dylib
0x1834d8000 - 0x183556fff  libsystem_c.dylib arm64 <8a5a190d70563f3c8d4ce16cab74f599> /usr/lib/system/libsystem_c.dylib
0x183481000 - 0x1834b0fff  libdispatch.dylib arm64 <fb1d0baf642337d1bea0af309586df97> /usr/lib/system/libdispatch.dylib
0x183028000 - 0x183401fff  libobjc.A.dylib arm64 <538f809dcd7c35ceb59d99802248f045> /usr/lib/libobjc.A.dylib
2、日志内容组成分析#####

整个日志内容中,直接和Crash信息相关,最能帮助开发者定位问题部分是: 异常信息线程回溯部分的内容。

1) 进程信息:发生Crash闪退进程的相关信息

  • Hardware Model : 标识设备类型。 如果很多崩溃日志都是来自相同的设备类型,说明应用只在某特定类型的设备上有问题。上面的日志里,崩溃日志产生的设备是iPhone 7 Plus (iPhone 7 Plus 也是2个版本 iPhone9,2 和 iPhone9,4. 硬件代号为 D11AP 和 D111AP. 型号有: A1661, A1784, A1785 和 A1786. )

  • Process 是应用名称。中括号里面的数字是闪退时应用的进程ID。

2) 基本信息:给出了一些基本信息,包括闪退发生的日期和时间,设备的iOS版本。

3) 异常信息:闪退发生时抛出的异常类型。还能看到异常编码和抛出异常的线程。

//以上面内容中的异常信息为例:
Exception Type: NSInvalidArgumentException(SIGABRT)
Exception Codes: -[NSNull integerValue]: unrecognized selector sent to instance 0x1a9d88ef8 at 0x00000001835c7014
Crashed Thread: 0
  • Exception Type异常类型:通常包含1.7中的Signal信号和EXC_BAD_ACCESS,NSRangeException等。
  • Exception Codes:异常编码:
  • Crashed Thread:发生Crash的线程id

4) 线程回溯:回溯是闪退发生时所有活动帧清单。它包含闪退发生时调用函数的清单。

5) 线程状态:闪退时寄存器中的值。一般不需要这部分的信息,因为回溯部分的信息已经足够让你找出问题所在。

6) 二进制映像:闪退时已经加载的二进制文件。

三、异常信息解读####

1、Exception Type(异常类型)#####
  • Exception Type:通常包含Signal信号 和 EXC_BAD_ACCESS,NSRangeException等。

|异常类型 | 可能的原因| 调试方法|
|--|--|
| EXC_CRASH | unrecognized selector | All Exception Point |
| EXC_BAD_ACCESS| 内存访问错误 | NSZombie|
| SIGSEGV | 引用了released对象 / 引用未init的对象 / 数组越界/ 试图往没有写权限的内存地址写数据 |NSZombie|
| SIGABRT| 逻辑错误导致的Crash,比如尝试多次释放同一个没存| 逻辑检查 |
| SIGPIPE | TCP突然断开,再发送数据| 添加signal(SIGPIPE,XX)|

具体信号说明参见iOS异常捕获

2、Exception Code(异常编码)#####
  • Exception Code:以一些文字开头,紧接着是一个或多个十六进制值。这些数值说明了Crash发生的本质。

  • 从Exception Code中,可以区分出Crash是因为程序错误、非法内存访问还是其他原因。常见的异常编码如下表:

异常编码 描述
0x8badf00d ate bad food ,表示应用是因为发生watchdog超时而被iOS终止的。通常是应用花费太多时间而无法启动、终止或响应用系统事件。
0xdeadfa11 dead fall,用户强制退出。
0xbaaaaaad 用户按住Home键和音量键,获取当前内存状态,不代表崩溃。
0xbad22222 VoIP 应用因为过于频繁重启而被终止
0xc00010ff cool off,因为太烫了被干掉
0xdead10cc dead lock,表明应用因为在后台运行时占用系统资源(如通讯录数据库)
0xbbadbeef bad beef,发生致命错误

说明1:详细的异常编码代表的含义请参考:Hexspeak

说明2:在后台任务列表中关闭已挂起的应用不会产生崩溃日志。 因为应用一旦被挂起,它何时被终止都是合理的。所以不会产生崩溃日志。

四、Crash日志符号化

1、概述#####

线程回溯部分内容如下:

5  AppName                         0x0000000100205280 0x0000000100028000 + 1954432
6  AppName                         0x00000001002ae59c 0x0000000100028000 + 2647440

这两条记录包括四列:(以第一条记录为例子)

  • 帧编号—— 5(数字越小,发生时间越晚,发生顺序越往后,越好锁定问题的范围)
  • 二进制库的名称 ——此处是 AppName.
  • 调用方法的地址 ——此处是 0x0000000100205280.
  • 第四列分为两个子列,一个基本地址和一个偏移量。此处是 x0000000100028000 + 1954432, 第一个数字指向文件,第二个数字指向文件中的代码行。

说明1:线程回溯部分并不是我们习惯使用方法名和行数,而是十六进制地址。所以我们在分析Crash前需要将这些十六进制地址转化成方法名称和行数,改过程被称为符号化

说明2:符号化Crash日志需要获取对应的应用二进制文件以及生成二进制文件时产生的 .dSYM 文件(符号表)。必需完全匹配才行。否则,日志将无法被完全符号化。

说明3: Xcode编译项目后,会得到同名的 dSYM 文件(符号表),dSYM 文件(符号表)是保存 16 进制函数地址映射信息的中转文件,我们调试的 symbols 都会包含在这个文件中,并且每次编译项目的时候都会生成一个新的 dSYM 文件,位于 /Users/<用户名>/Library/Developer/Xcode/Archives 目录下,对于每一个发布版本我们都很有必要保存对应的 Archives 文件。

说明4:符号化可以使用Xcode的两种命令 symbolicatecrash命令 + ** atos命令 **

2、symbolicatecrash命令#####

1)首选找到symbolicatecrash命令的位置

  find /Applications -name symbolicatecrash -type f  
  //我的本机命令的位置:/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

2)找到线上版本对应的xcarchive文件。从中找到.dSYM和.app文件

xcarchive所在的路径一般在: /Users/<用户名>/Library/Developer/Xcode/Archives 目录下

3)获取crash日志文件

  • 线上App的Crash日志经由Crash日志收集服务获得(主要来源)。

  • 也可以从真机上获取Crash日志文件。点击Window -> Devices,选择你自己的机器,然后点击View Device Logs,右键可以导出Crash文件。

  • 获取的这些日志文件都需要符号化处理。

4)将symbolicatecrash、.dSYM、.app、crash.crash拷贝到桌面下同一个文件夹下

5)检查 xx.app 和 xx.app.dSYM 文件以及crash 文件这三种的 UUID是否一致。

  • 查看 xx.app 文件的 UUID,terminal 中输入命令 :

    dwarfdump --uuid xx.app/xx (xx代表你的项目名)
    
  • 查看 xx.app.dSYM 文件的 UUID ,在 terminal 中输入命令:

    dwarfdump --uuid xx.app.dSYM 
    
  • 查看crash 日志中的Incident Identifier (crash 文件的 UUID)

6)使用命令,生成“可定位问题的crash文件”

//symbolreportXXX.crash就是符号化后的文件
./symbolicatecrash crashXXX.crash appName.app.dSYM > symbolreportXXX.crash 

7) 根据符号化后的线程回溯信息,可以帮助定位出问题的代码行。

说明:如果执行symbolicatecrash命令出现 Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash...这样的错误,可以在执行命令前,输入export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"

3、atos命令#####

在符号化时候,还可以使用atos命令。发现armv7处理器上的crash使用symbolicatecrash无法符号化。

1)将.dSYM、.app、crash.crash放到同一个文件夹下。

2) 知道crash文件的UUID:执行grep "AppName arm" *crash,得到结果

crash1.crash:0x100040000 - 0x100e23fff +AppName arm64 <ba0e190dcd1b37349e1362be7e9b7e62> /var/containers/Bundle/Application/55A4D641-847F-4D24-86E1-129B28461858/AppName.app/AppName
crash2.crash:0x100060000 - 0x100e43fff +AppName arm64 <ba0e190dcd1b37349e1362be7e9b7e62> /var/containers/Bundle/Application/3229ED68-8D19-406D-A3F5-EC0310C9DB7C/QAppName.app/AppName
crash3.crash:    0x5000 -   0xce8fff +AppName armv7 <7d62327effef37d384658020625a9944> /var/containers/Bundle/Application/C6BE271D-2EAC-42C0-8E72-4523F88C76B2/AppName.app/AppName

其中0x100040000、0x100060000、0x5000是加载地址(loadingAddress), 而arm64、armv7 是 architecture 的值(architectureValue),这两个值后面都要用。

3)然后执行atos命令,输入成功,进入待输入状态

xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l loadingAddress -arch architectureValue

4) 此时输入App对应的Crash地址,得到发生crash的信息。

实例1

    grep "AppName arm" *crash
    xcrun atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -l 0x100040000 -arch arm64

实例2

    grep "AppName arm" *crash
    xcrun atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -l 0x5000 -arch armv7

五、常见的Crash####

有一些Crash比较常见,下面罗列出5种常见的Crash。

1、数组操作
  • 场景1:取数据索引越界。一般发生在UITableView的使用中,因为cellForRowAtIndexPath代理方法是异步执行的,UITableView对象的dataSource一旦在加载数据过程中发生变化,极有可能发生数组越界的异常。在多线程场景下,列表界面的数据有可能经常变化,很可能发生;当列表界面数据不怎么变化的时候,几乎感知不到这种异常的存在。

解决办法:从数组中取数据前,校验索引是否正确。

    @implementation NSMutableArray (Safe)

    - (id)safeObjectAtIndex:(NSUInteger)index{

        if (index < self.count){
            return [self objectAtIndex:index];
        }else{
            NSLog(@"警告:数组越界!!!");
        }    
        return nil;
    }

    @end
  • 场景2:数组添加数据对象时nil

解决办法:添加对象到数组前,判断是否是nil

说明:数组的删除等操作处理类似,数组操作前要进行数据校验。

2、多线程下的Crash#####
  • 一般多线程发生的Crash,会收到SIGSEGV信号,表明试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。

  • 场景1:子线程中更新UI

解决办法:将UI更新操作放在主线程中,可以使用performSelectorOnMainThread 或 GCD

    //子线程中,使用宏将更新UI的任务派发到主队列
    #define dispatch_main_sync_safe(block) \
    if ([NSThread isMainThread]) { \
       block(); \
    } else { \
        dispatch_sync(dispatch_get_main_queue(), block); \
    }

    #define dispatch_async_main(block)              dispatch_async(dispatch_get_main_queue(), block)
  • 场景2:多线程中创建单例

    解决办法:使用dispatch_once,保证代码只执行一次,保证线程安全。

      //以QSAccountManager单例为例
      static QSAccountManager *_shareManager = nil;
      + (instancetype)shareManager{
    
          static dispatch_once_t once;
          dispatch_once(&once, ^{
              _shareManager = [[self alloc] init];
          });
          return _shareManager;
      }
    
      + (instancetype)allocWithZone:(struct _NSZone *)zone{
    
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              _shareManager = [super allocWithZone:zone];
          });
          return _shareManager;
      }
    
     - (nonnull id)copyWithZone:(nullable NSZone *)zone{
          return _shareManager;
      }
    
  • 场景3:多线程下非线程安全类的使用,如NSMutableArray、NSMutableDictionary

    解决办法:使用派发队列或锁保证数据读写安全。具体实现详见 iOS实录12:NSMutableArray使用中忽视的问题中第一部分。

  • 场景4:数据缓存到磁盘和读取。

    解决办法:使用派发队列或锁保证数据读写安全。如将数据的读取和写异步放入串行同步队列,保证数据同步,线程安全。

3、WatchDog 超时造成的Crash
  • 一般异常编码是0x8badf00d ,表示应用是因为发生watchdog超时而被iOS终止的。通常是应用花费太多时间而无法启动、终止或响应用系统事件。

  • 场景1:主线程中执行耗时的操作,导致主线程被卡超过一定的时间。

    解决办法:主线程中只负责UI的更新和响应,将耗时的操作采用异步的方式放到后台线程执行。耗时操作包括:网络请求,数据库读写等。

4、performSelector:withObject:afterDelay下的Crash
  • 场景1:对象释放比performSelector:afterDelay要早

    解决办法:在对应类的dealloc中执行cancelPreviousPerformRequestsWithTarget取消执行。

5、SIGPIPE导致的程序退出
  • 当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。而根据信号的默认处理规则,SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。

  • 场景:长连接socket或重定向管道进入后台,没有关闭

    解决办法1:切换到后台时,关闭长连接和管道,回到前台再重建;

    解决办法2:使用signal(SIGPIPE,SIG_IGN),将SIGPIPE交给了系统处理。这么做将SIGPIPE设为SIG_IGN,使得客户端不执行默认动作,即不退出。

End

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 本文就捕获iOS Crash、Crash日志组成、Crash日志符号化、异常信息解读、常见的Crash五部分介绍。...
    xukuangbo_阅读 1,499评论 0 0
  • 本文整理下最近对于crash采集的总结,和踩过的坑。 CrashReporter 首先,iOS有自己的CrashR...
    谈Xx阅读 19,354评论 15 66
  • 比较好的转载:http://www.cocoachina.com/ios/20151218/14748.html转...
    liudhkk阅读 914评论 0 2
  • 第一次数星星是在奶奶的怀里,一颗,两颗,三颗…在奶奶哼的摇篮曲里数着星星沉沉地睡去。童年时,在晚上,奶奶总喜欢带我...
    逮逮爱旅行阅读 579评论 0 0
  • 雾霾的危害…… 介个就不用小编在介绍了吧? 据说是多少年后肺癌会井喷似的出现……因为雾霾里面的颗粒物让人的免疫力下...
    九九朝颜阅读 563评论 0 2