iOS崩溃分析
崩溃的分析
最近修复了一些iOS项目的崩溃,想分析总结一下这些崩溃的原因,以及预防。崩溃的原因一般有下面几种:
内存访问错误(这个出现的比较多,原因多种多样)
非法指令的执行(超出权限范围内的指令)
非法的IO访问
系统调用参数出错
指令条用参数错误(除以0之类)
想分析用户崩溃,收集崩溃的日志非常重要,我们项目中用的是Twitter的Crashlytics,现在叫fabric,
能够收集到比较详细的崩溃信息:各线程的崩溃栈和设备的一些信息。有一个小问题就是没有收集到各个
寄存器里面的值(看是不是我没有找到地方)。
选了出现次数最多的一个崩溃进行分析:
# OS Version: 13.1.2 (17A860)
# Device: iPhone 8
# RAM Free: 1.9%
# Disk Free: 15.7%
#24. Crashed: NSOperationQueue 0x107964a70 (QOS: UNSPECIFIED)
0 libobjc.A.dylib 0x1b394f150 objc_release + 16
1 _appstore 0x10184b694 -[YNP_VRHomeCoreViewModel voiceRoomDidChangeSpeakingUser:] + 373 (YNP_VRHomeCoreViewModel.m:373)
2 Aipai_appstore 0x1015a6144 __63-[YNP_VoiceRoomManager makeDelegatesPerformSelector:obj:async:]_block_invoke + 1633 (YNP_VoiceRoomManager.m:1633)
3 Foundation 0x1b3fd161c __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 16
4 Foundation 0x1b3edb3d8 -[NSBlockOperation main] + 100
5 Foundation 0x1b3fd38a4 __NSOPERATION_IS_INVOKING_MAIN__ + 20
6 Foundation 0x1b3edb070 -[NSOperation start] + 732
7 Foundation 0x1b3fd429c __NSOPERATIONQUEUE_IS_STARTING_AN_OPERATION__ + 20
8 Foundation 0x1b3fd3d68 __NSOQSchedule_f + 180
9 libdispatch.dylib 0x1b38bd9a8 _dispatch_block_async_invoke2 + 104
10 libdispatch.dylib 0x1b38da184 _dispatch_client_callout + 16
11 libdispatch.dylib 0x1b38b3eb8 _dispatch_continuation_pop$VARIANT$armv81 + 404
12 libdispatch.dylib 0x1b38b362c _dispatch_async_redirect_invoke + 592
13 libdispatch.dylib 0x1b38c0110 _dispatch_root_queue_drain + 344
14 libdispatch.dylib 0x1b38c08b0 _dispatch_worker_thread2 + 116
15 libsystem_pthread.dylib 0x1b3929f64 _pthread_wqthread + 212
16 libsystem_pthread.dylib 0x1b392cae0 start_wqthread + 8
崩溃的原因是EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x00000009d32f8c80,这个是属于内存访问错误,崩溃行数是373,崩溃处的代码如下:
- (void)voiceRoomDidChangeSpeakingUser:(NSArray<NSString *> *)bids
{
@synchronized (self.seatInfos) {
if (!self.seatInfos || ![self.seatInfos isKindOfClass:[NSArray class]]) {
return;
}
[self.seatInfos enumerateObjectsUsingBlock:^(YNP_VRSeatInfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (!bids || !bids.count || !obj.user || ![bids containsObject:obj.user.bid]) {
obj.user.native_isTalking = NO;
} else {
obj.user.native_isTalking = YES;
}
}];
} //>>>>>>>>>>>line 373
[self didChangeSeatInfos];
}
乍一看比较难看出这里为什么会崩溃,为什么会调用到objc_release函数中去了,一般在OC里ARC的机制下,引用计数减1,调用这个函数。
注意到我们这个崩溃是在非主线程里面的,self.seaInfos是一个数组,查看一下上下环境,它在不同线程被改变,可能在其它线程被释放了,然后在这个地方又被释放了一次,造成内存错误崩溃。我们先暂时这么想吧,后面再验证,崩溃最好的方式是在Xcode里面重现它,调试解决,但是这个项目业务很复杂,多线程的问题比较难以重现,所以我们可以写个小demo来模拟该段代码验证一下。 demo如下:
- (void)testFun
{
dispatch_queue_t queue1 = dispatch_queue_create("queue1", 0);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", 0);
__block NSMutableArray* array = [NSMutableArray array];
dispatch_async(queue1, ^{
while (true) {
array = [NSMutableArray array];
}
});
dispatch_async(queue2, ^{
while (true) {
@synchronized (array) {
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"obj=%@",obj);
}];
}
}
});
}
尝试运行几次,出现了和项目类似的崩溃,截图如下:
崩溃函数的位置也是一样,先看看崩溃这段的汇编代码,结合OBJC的源码分析前面几条指令:
1.判断obj是否为空,空的话跳转到ret返回;2.测试地址最高位是否为1,执行返回跳转;3.取出对象的isa指针赋值给x8;4.得到对象的Class对象指针赋值给x8
如何获取isa指针的class对象;5.取class对象偏移32个字节的数据到w8寄存器的低32位。
libobjc.A.dylib`objc_release:
0x1aa1f3140 <+0>: cbz x0, 0x1aa1f318c ; <+76> // 1
0x1aa1f3144 <+4>: tbnz x0, #0x3f, 0x1aa1f318c ; <+76> // 2
0x1aa1f3148 <+8>: ldr x8, [x0] //3
0x1aa1f314c <+12>: and x8, x8, #0xffffffff8 // 4
-> 0x1aa1f3150 <+16>: ldrb w8, [x8, #0x20] //5
0x1aa1f3154 <+20>: tbz w8, #0x2, 0x1aa1f31b8 ; <+120>
0x1aa1f3158 <+24>: orr x8, xzr, #0x200000000000
0x1aa1f315c <+28>: ldxr x9, [x0]
0x1aa1f3160 <+32>: tbz w9, #0x0, 0x1aa1f31a0 ; <+96>
0x1aa1f3164 <+36>: subs x10, x9, x8
到这里基本可以确认是self.seatinfos在其它地方被释放了,但是在这个地方为什么会调用objc_release函数呢?看看这里的@synchronized (self.seatInfos),这里本想对这段代码加锁,但是使用self.seatInfos作为参数,明显不合适,self.seatInfos作为一个变量在其它线程会被改变,根本达不到加锁的效果。在ARC的环境下@synchronized会不会对self.seatInfos对象的引用产生变化呢。代码里面试验一下:
NSMutableArray* array = [NSMutableArray array];
NSLog(@"before count = %lu",(unsigned long)CFGetRetainCount((__bridge CFTypeRef)array));
@synchronized (array) {
NSLog(@"in syn count = %lu", (unsigned long)CFGetRetainCount((__bridge CFTypeRef)array));
}
NSLog(@"after count = %lu", (unsigned long)CFGetRetainCount((__bridge CFTypeRef)array));
输出的结果为1,2,1。由此可见synchronized的实现对array的引用计数产生了影响。直接看一下@synchronized的汇编实现:
stub for: objc_msgSend
0x104831548 <+84>: mov x29, x29
0x10483154c <+88>: bl 0x10483259c ; symbol stub for: objc_retainAutoreleasedReturnValue
0x104831550 <+92>: stur x0, [x29, #-0x28]
0x104831554 <+96>: ldur x0, [x29, #-0x28]
0x104831558 <+100>: bl 0x104832488 ; symbol stub for: CFGetRetainCount
0x10483155c <+104>: mov x1, sp
0x104831560 <+108>: str x0, [x1]
0x104831564 <+112>: adrp x0, 3
0x104831568 <+116>: add x0, x0, #0x360 ; =0x360
0x10483156c <+120>: bl 0x104832494 ; symbol stub for: NSLog
-> 0x104831570 <+124>: ldur x0, [x29, #-0x28]
0x104831574 <+128>: bl 0x104832584 ; symbol stub for: objc_retain
0x104831578 <+132>: mov x1, x0
0x10483157c <+136>: mov x30, x0
0x104831580 <+140>: str x1, [sp, #0x50]
0x104831584 <+144>: str x30, [sp, #0x48]
0x104831588 <+148>: bl 0x1048325b4 ; symbol stub for: objc_sync_enter
0x10483158c <+152>: ldur x1, [x29, #-0x28]
0x104831590 <+156>: str w0, [sp, #0x44]
0x104831594 <+160>: mov x0, x1
0x104831598 <+164>: bl 0x104832488 ; symbol stub for: CFGetRetainCount
0x10483159c <+168>: str x0, [sp, #0x38]
0x1048315a0 <+172>: b 0x1048315a4 ; <+176> at ViewController.m:174:9
0x1048315a4 <+176>: mov x8, sp
0x1048315a8 <+180>: ldr x9, [sp, #0x38]
0x1048315ac <+184>: str x9, [x8]
0x1048315b0 <+188>: adrp x0, 3
0x1048315b4 <+192>: add x0, x0, #0x380 ; =0x380
0x1048315b8 <+196>: bl 0x104832494 ; symbol stub for: NSLog
0x1048315bc <+200>: b 0x1048315c0 ; <+204> at ViewController.m
0x1048315c0 <+204>: ldr x0, [sp, #0x48]
0x1048315c4 <+208>: bl 0x1048325c0 ; symbol stub for: objc_sync_exit
0x1048315c8 <+212>: ldr x30, [sp, #0x50]
0x1048315cc <+216>: str w0, [sp, #0x34]
0x1048315d0 <+220>: mov x0, x30
0x1048315d4 <+224>: bl 0x104832578 ; symbol stub for: objc_release
0x1048315d8 <+228>: ldur x0, [x29, #-0x28]
0x1048315dc <+232>: bl 0x104832488 ; symbol stub for: CFGetRetainCount
0x1048315e0 <+236>: mov x30, sp
0x1048315e4 <+240>: str x0, [x30]
0x1048315e8 <+244>: adrp x0, 3
0x1048315ec <+248>: add x0, x0, #0x3a0 ; =0x3a0
0x1048315f0 <+252>: bl 0x104832494 ; symbol stub for: NSLog
@synchronized实现中在调用objc_sync_enter生成递归锁之前给传入对象进行了objc_retain操作,然后在调用obj_syn_exit之后,调用objc_release释放。但是由于多线程,又没有正确加锁的原因,导致这个对象在其它线程已经被释放了,然后在这里又做了一次release,直接导致崩溃。在ARC环境下的多线程中,我们很容易忽略,那些引起引用计数发生改变的地方,没有正确加锁,这种也是偶发性的,测试环节可能被漏掉,也比较难以重现,导致项目上线,有一些用户发生崩溃,带来糟糕的体验。这里我们直接把@synchronized (self.seatInfos) 修改成@synchronized (self) ,其它地方也修改一下,即可解决这个崩溃。
第二个崩溃
崩溃的堆栈如下:
崩溃处的节选代码如下:
- (void)voiceRoomDidReceiveExtMessage:(YNP_VRExtMessageModel *)messageModel
{
YNP_VRExtMessageActionInfo *actionInfo = messageModel.actionInfo;
if (!actionInfo) {
return;
}
switch (actionInfo.action) {
case YNP_VRExtMessageTypeUserEnter: {
if (!self.menuItems || !self.menuItems.count) { //line -----------------------------------176
[self updateMenuItem];
}
YNP_VRExtMessageUserEnterData *data = actionInfo.userEnterData;
YNP_VoiceRoomUserModel *user = data.baseInfo;
if (!(user.nobilityInfo != nil && user.nobilityInfo.nobilityInvisible)) {
YNP_VRMsgLayout *layout = [YNP_VRMsgLayoutUtil enterRoomMsgLayoutWithUser:user];
[self didReceiverMsgWithLayout:layout];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(VRHomeCoreViewModelDidReceiveUserEnterMsg:)]) {
[self.delegate VRHomeCoreViewModelDidReceiveUserEnterMsg:user];
}
break;
}
case YNP_VRExtMessageTypeLeaveRoom: {
break;
}
......
}
}
可以看出是崩溃在objc_retain函数里面,是怎么调用到这个函数里面的呢?在ARC的环境下,self.menuItems隐式调用该成员变量的getter方法,调试代码的时候发现objc_retainAutoreleasedReturnValue在getter方法返回时被调用。编译器在代码插入类似于:
NSArray *temp = objc_retainAutoreleasedReturnValue([self menuItems]);
在MRC环境中的getter方法,举个例子:
- (Foo *)foo
{
Foo *foo = [[Foo alloc] init];
return [foo autorelease];
}
- (void)testFoo
{
Foo *foo = [self foo];
[foo retain];
//do something with foo
[foo release];
}
//merge two methods above
- (void)testFoo1
{
Foo *foo = [[Foo alloc] init];
[foo autorelease];
[foo retain];
//do something with foo
[foo release];
}
}
从testFoo1中可以看出,我们的有些步骤是多余的,把对象放到自动释放池,然后又retain,大量的这种调用显的没有必要,又会影响性能。所以objc_retainAutoreleaseReturnValue和objc_retainAutoreleasedReturnValue被使用来优化这个流程,它们一般成对出现,在方法的返回中使用objc_retainAutoreleaseReturnValue(foo),这个函数的实现会检查返回的地址是不是调用了objc_retainAutoreleasedReturnValue函数,如果是则会跳过autorelease和retain的流程,直接返回对象地址,否则就走老的一套。在我们的项目中menuItems的getter方法是直接根据self地址,算出偏移,直接返回对象地址,没有调用objc_retainAutoreleaseReturnValue。所以我们在后面调用objc_retainAutoreleasedReturnValue还是会调用到objc_retain函数。上面的这些分析说明了在调用getter方法,怎么调到objc_retain函数里面。崩溃的原因还是因为多线程导致的,所以在可能会导致引用计数改变的一些情况,注意加锁,避免崩溃。
参考:
[https://en.wikipedia.org/wiki/Crash_(computing)](https://en.wikipedia.org/wiki/Crash_(computing)
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-132741
https://www.galloway.me.uk/2012/02/how-does-objc_retainautoreleasedreturnvalue-work/
https://github.com/xietao3/Study-Plan/tree/master/ARC
iOS崩溃分析的更多相关文章
- iOS 崩溃分析
崩溃统计分析,在APP中是非常常见一种优化APP,发现APP的BUG的方式. 1.异常处理 可通过try catch 方式处理,如果发生异常,会走catch ,最终走fianlly.对一些我们不想他崩 ...
- iOS 崩溃日志分析(个人总结,最实用)
iOS 崩溃日志分析(个人总结,最实用) 要分析奔溃日志需要三个文件:crash日志,symbolicatecrash分析工具,.dSYM符号集 0. 在桌面创建一个crash文件夹 1. 需要Xco ...
- iOS Crash 分析 符号化崩溃日志
参考: http://blog.csdn.net/diyagoanyhacker/article/details/41247367 http://blog.csdn.net/diyagoanyhack ...
- iOS --------Crash 分析(一)
iOS Crash 分析(文一)- 开始 1. 名词解释 1. UUID 一个字符串,在iOS上每个可执行文件或库文件都包含至少一个UUID.目的是为了唯一识别这个文件. 2. dwarfdump 苹 ...
- iOS崩溃调试的使用和技巧总结
在iOS开发调试过程中以及上线之后,程序经常会出现崩溃的问题.简单的崩溃还好说,复杂的崩溃就需要我们通过解析Crash文件来分析了,解析Crash文件在iOS开发中是比较常见的. 现在网上有很多关于解 ...
- iOS崩溃日志ips文件解析
iOS崩溃日志ips文件解析 一 简介 测试组的同事在进行稳定性测试时,通常会遇到一些崩溃,然后他们会将这些崩溃日志(一般是ips格式的文件)反馈给开发进行分析,但是这些ips文件中的内容通常是如下 ...
- 转: iOS崩溃堆栈符号表使用与用途
转:http://bugly.qq.com/blog/?p=119 iOS崩溃堆栈符号化,定位问题分分钟搞定! 2015.3.16 腾讯Bugly 微信分享 最近一段时间,在跟开发者沟通过程中,萝 ...
- 常用获取Android崩溃日志和IOS崩溃日志的几种方法
一:前言 在日常测试app时,经常会遇到崩溃问题,测试快速抓取到崩溃日志可以有效方便开发进行定位,快速解决问题所在测试做到测试分析,定位是非常重要的,这也是判断一个测试能力指标的一大维度. 二:And ...
- Android&iOS崩溃堆栈上报
Android&iOS崩溃堆栈上报 原文地址:http://www.cnblogs.com/songcf/p/4885468.html 通过崩溃捕获和收集,可以收集到已发布应用(游戏)的异常, ...
随机推荐
- 20175212童皓桢 《Java程序设计》第十周学习总结
学号 2016-2017-2 <Java程序设计>第X周学习总结 教材学习内容总结 一.Java中的线程的状态 建的线程在它的一个完整的生命周期中通常要经历如下的四种状态: 1.新建: 当 ...
- SpringBoot整合kafka(安装)
项目路径:https://github.com/zhaopeng01/springboot-study/tree/master/study_14 序言 Kafka 是一种高吞吐的分布式发布订阅消息系统 ...
- leetcode-easy-others-118 Pascal's Triangle
mycode 16.47% class Solution(object): def generate(self, numRows): """ :type numRow ...
- vue中limitBy,filterBy,orderBy的用法
1.limitBy的用法 <body> <div id="box"> <ul> <li v-for="val in arr | ...
- DAY 5模拟赛
DAY 5 廖俊豪神仙出题 T1 最小差异矩阵(a.cpp, a.in, a.out) [题目描述] 有一个 n*m 的矩阵,矩阵的每个位置上可以放置一个数.对于第 i 行,第 i 行的差异定义为该行 ...
- golang gRPC初探
gRPC使用protocol buffers作为Interface Definition Language (IDL). gRPC的底层信息交互格式也使用的是protocol buffers. 默认情 ...
- IDEA创建maven各种原型项目汇总
1: internal -> appfuse-basic-jsf (创建一个基于Hibernate,Spring和JSF的Web应用程序的原型) 2: internal -> appfus ...
- MYSQL5.6二进制包的安装
二进制1. 下载包 wget http://mirrors.sohu.com/mysql/MySQL-5.6/mysql-5.6.26-linux-glibc2.5-x86_64.tar.gz2. 解 ...
- ATP检测 BAPI BAPI_MATERIAL_AVAILABILITY
*****ATP检测 DATA: END_RLT LIKE EBAN-LFDAT, NOT_AVAILABLE, AVAILABLE( ...
- Linux(Debian)发行版中文输入法
Linux发行版下有两大输入法框架:ibus 和fcitx,其中fcitx 的体验要比ibus 好,因此选择 fcitx 框架,并安装中文输入法. 中文输入法中你可以选择fcitx-pinyin or ...