iOS中常见 Crash 及缓解方案

来源:枫影JustinYan

链接:http://justinyan.me/post/1609

 

一、访问了二个业已被放出的对象

 

在不使用 A帕杰罗C 的时候,内存要本人管理,那时重复或过早释放都有十分的大概率造成
Crash。

 

例子

 

NSObject * aObj = [[NSObject alloc] init];

[aObj release];

 

NSLog(@”%@”, aObj);

 

原因

 

aObj
那个指标已经被放飞,不过指针未有置空,那时访问那个指针指向的内部存储器就会
Crash。

 

化解办法

 

  • 行使前要咬定非空,释放后要置空。正确的放飞应该是:

 

[aObj release];

aObj = nil;

 

鉴于ObjC的表征,调用 nil
指针的别样措施也就是无效益,所以正是有人在选取那么些指针时从没看清至少还不会挂掉。

在ObjC里面,壹切基于
NSObject 的指标都施用指针来拓展调用,所以在不能够担保该指针一定有值的图景下,要先判断指针非空再展开调用。

 

if (aObj) {

    //…

}

 

科普的如判断2个字符串是不是为空:

 

if (aString && aString.length > 0) {//…}

 

  • 伏贴使用
    autorelease。有个别时候不能够领悟自个儿创造的靶子如何时候要拓展放飞,能够动用
    autoRelease,可是不鼓励施用。因为 autoRelease 的指标要等到近期的二个autoReleasePool
    销毁的时候才会销毁,假使协调知道如何时候会用完那个指标,当然霎时放飞功效要更高。若是一定要用
    autoRelease 来创设大气指标或许大数据对象,最棒温馨显式地开创三个autoReleasePool,在运用后手动销毁。在此以前要协调手动初阶化
    autoReleasePool,现在得以用以下写法:

 

@autoreleasepool{

    for (int i = 0; i < 100; ++i) {

        NSObject * aObj = [[[NSObject alloc] init]
autorelease];

        //….

    }

}

 

2、访问数组类对象越界或插队了空对象

 

NSMutableArray/NSMutableDictionary/NSMutableSet 等类下标越界,恐怕insert 了三个 nil 对象。

 

原因

 

三个定位数组有1块延续内部存款和储蓄器,数组指针指向内部存款和储蓄器首地址,靠下标来总结成分地址,要是下标越界则指针偏移出那块内部存款和储蓄器,会访问到野数据,ObjC
为了安全就一直让程序 Crash 了。

 

而 nil 对象在数组类的 init 方法里面是表示数组的终结,所以采用 addObject
方法来插入对象就会使程序挂掉。若是实在要在数组里面插足贰个空对象,那就应用
NSNull。

 

[array addObject:[NSNull null]];

 

消除办法

 

应用数组时只顾看清下标是还是不是越界,插入对象前先判断该目的是还是不是为空。

 

if (aObj) {

    [array addObject:aObj];

}

 

能够行使 Cocoa 的 Category 特性间接增添 NSMutable 类的 Add/Insert
方法。比如:

 

@interface NSMutableArray (SafeInsert)

-(void) safeAddObject:(id)anObject;

@end

 

@implementation NSMutableArray (SafeInsert)

-(void) safeAddObject:(id)anObject {

    if (anObject) {

        [self addObject:anObject];

    }

}

@end

 

这么,现在在工程里面使用 NSMutableArray 就能够直接选择 safeAddObject
方法来躲避 Crash。

 

三、访问了不设有的章程

 

ObjC 的格局调用跟 C++ 很不1样。 C++
在编写翻译的时候就曾经绑定了类和情势,一个类不可能调用贰个不存在的办法,否则就报编写翻译错误。而
ObjC 则是在 runtime 的时候才去搜寻应该调用哪3个主意。

 

那三种完毕各有高低,C++ 的绑定使得调用方法的时候速度赶快,可是只好通过
virtual 关键字来兑现有限的动态绑定。而对 ObjC
来说,事实上他的贯彻是一种音讯传递而不是艺术调用。

 

[aObj aMethod];

 

那般的口舌应该明了为,像 aObj 对象发送二个誉为 aMethod 的新闻,aObj
对象吸收到这一个音信随后,本身去寻觅是不是能调用对应的法子,找不到则上父类找,再找不到就
Crash。由于 ObjC
的那种特点,使得其音信不单能够兑现情势调用,仍可以紧系转载,对八个 obj
传递二个 selector 要求调用某艺术,他得以一贯不理会,转载给其余 obj
让其他 obj 来响应,格外灵活。

 

例子

 

[self methodNotExists];

 

调用1个不存在的方法,能够编写翻译通过,运转时平昔挂掉,报
NSInvalidArgumentException 万分:

 

-[WSMainViewController methodNotExist]: unrecognized selector sent
to instance 0x1dd96160

2013-10-23 15:49:52.167 WSCrashSample[5578:907] *** Terminating
app due to uncaught exception ‘NSInvalidArgumentException’, reason:
‘-[WSMainViewController methodNotExist]: unrecognized selector sent
to instance 0x1dd96160’

 

消除方案

 

像那体系型的荒谬常常出未来利用 delegate 的时候,因为 delegate 日常是2个id 泛型,所以 IDE 也不会报告警察方告,所以那种时候要用 respondsToSelector
方法先判断一下,然后再进行调用。

 

if ([self respondsToSelector:@selector(methodNotExist)]) {

    [self methodNotExist];

}

 

四、字节对齐

 

唯恐是因为强制类型转换大概强制写内部存款和储蓄器等操作,CPU 执行 STMIA
指令时发现写入的内存地址不是自然边界,就会硬件报错挂掉。三星 五s 的 CPU
从三十四个人变成六10位,有望晤面世1些字节对齐的题材造成 Crash 率进步的。

 

例子

 

char *mem = malloc(16); // alloc 16 bytes of data

double *dbl = mem + 2;

double set = 10.0;

*dbl = set;

 

像上面那段代码,执行到

 

*dbl = set;

 

那句的时候,报了 EXC_BAD_ACCESS(code=EXC_ARM_DA_ALIGN) 错误。

 

原因

 

要明白字节对齐错误还供给一小点背景知识,知道的童鞋能够略过一向看后面了。

 


 

背景知识

 

电脑最小数据单位是bit(位),也正是0或一。

 

而内部存款和储蓄器空间最小单元是byte(字节),1个byte为八个bit。

 

内部存款和储蓄器地址空间以byte划分,所以理论上访问内部存款和储蓄器地址能够从任意byte开首,不过实际大家不是平昔访问硬件地址,而是通过操作系统的虚拟内部存款和储蓄器地址来拜访,虚拟内部存款和储蓄器地址是以字为单位的。八个三十二个人机器的字长便是30位,所以三十3个人机器一回访问内部存款和储蓄器大小就是五个byte。再者为了性能怀念,数据结构(特别是栈)应该尽量地在自然边界上对齐。原因在于,为了访问未对齐的内部存款和储蓄器,处理器须要作三遍内部存款和储蓄器访问;而对齐的内部存储器访问仅需求一遍访问。

 

举2个板栗:

 

struct foo {

    char aChar1;

    short aShort;

    char aChar2;

    int i;

};

 

上边那几个结构体,在三十八位机器上,char 长度为陆位,占三个byte,short
占一个byte, int 6个byte。

设若内存地址从 0 开始,那么理论上挨家挨户分配的地点应该是:

 

aChar1 0x00000000

aShort 0x00000001

aChar2 0x00000003

i      0x00000004

 

唯独其实编写翻译后,那个变量的地点是这么的:

 

aChar1 0x00000000

aShort 0x00000002

aChar2 0x00000004

i      0x00000008

 

那便是 aChar一 和 aChar二 都被做了内部存款和储蓄器对齐优化,都变成 二 byte 了。

 


 

解决办法

 

应用 memcpy 来作内部存款和储蓄器拷贝,而不是一贯对指针赋值。对上边的例证作修改就是:

 

char *mem = malloc(16); // alloc 16 bytes of data

double *dbl = mem + 2;

double set = 10.0;

memcpy(dbl, &set, sizeof(set));

 

改用 memcpy 之后运维就不会有标题了,那是因为 memcpy
本人的落到实处就曾经做了字节对齐的优化了。大家来看glibc二.5中的memcpy的源码:

 

void *memcpy (void *dstpp, const void *srcpp, size_t len) {

 

    unsigned long int dstp = (long int) dstpp;

    unsigned long int srcp = (long int) srcpp;

 

    if (len >= OP_T_THRES) {

      len -= (-dstp) % OPSIZ;

      BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);

      PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);

      WORD_COPY_FWD (dstp, srcp, len, len);

    }

    BYTE_COPY_FWD (dstp, srcp, len);

    return dstpp;

}

 

解析那些函数,首先比较一下供给拷贝的内部存款和储蓄器块大小,假若低于 OP_T_THRES
(那里定义为
1陆),则一向字节拷贝就完了,假如过量这些值,视为大内部存款和储蓄器块拷贝,采取优化算法。

 

len -= (-dstp) % OPSIZ;

BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);

 

// #define OPSIZ   (sizeof(op_t))

// enum op_t

 

OPSIZE 是 op_t 的长度,op_t 是字的体系,所以那边 OPSIZE
是获取当前平台的字长。

dstp 是内部存款和储蓄器地址,内部存储器地址是按byte来算的,对内部存款和储蓄器地址 unsigned long
取负数再模 OPSIZE
获得须求对齐的那部分多少的尺寸,然后用字节拷贝做内部存款和储蓄器对齐。取负数是因为要以dstp的地方作为源点来实行理并答复制,要是一向取模那就变成0作为起源去做运算了。

 

对 BYTE_COPY_FWD 那些宏的源码有趣味的同室能够看看那篇:BYTE_COPY_FWD
源码解析(感激 @raincai 同学指示)

http://www.justinyan.me/post/1689

 

诸如此类对齐了后来,再做大数据量部分的正片:

 

PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);

 

看这几个宏的源码,尽恐怕多地作页拷贝,剩下的大小会写入len变量。

 

/////////////////////////////////////////////////

#if PAGE_COPY_THRESHOLD

 

#include

 

#define PAGE_COPY_FWD_MAYBE(dstp, srcp, nbytes_left,
nbytes)              \

  do                                          \

    {                                         \

      if ((nbytes) >= PAGE_COPY_THRESHOLD
&&                      \

      PAGE_OFFSET ((dstp) – (srcp)) == 0)                     \

    {                                     \

      /* The amount to copy is past the threshold for copying        
\

         pages virtually with kernel VM operations, and the          
\

         source and destination addresses have the same
alignment.  */    \

      size_t nbytes_before = PAGE_OFFSET (-(dstp));              
\

      if (nbytes_before != 0)                         \

        {                                     \

          /* First copy the words before the first page
boundary.  */     \

          WORD_COPY_FWD (dstp, srcp, nbytes_left,
nbytes_before);         \

          assert (nbytes_left == 0);                      \

          nbytes -= nbytes_before;                        \

        }                                     \

      PAGE_COPY_FWD (dstp, srcp, nbytes_left,
nbytes);            \

    }                                     \

    } while (0)

 

/* The page size is always a power of two, so we can avoid modulo
division.  */

#define PAGE_OFFSET(n)  ((n) & (PAGE_SIZE – 1))

 

#else

 

#define PAGE_COPY_FWD_MAYBE(dstp, srcp, nbytes_left, nbytes) /*
nada */

 

#endif

 

PAGE_COPY_FWD 的宏定义:

 

#define PAGE_COPY_FWD   (       dstp,

    srcp,

    nbytes_left,

    nbytes

)      

Value:

((nbytes_left) = ((nbytes) –                              \

            (__vm_copy (__mach_task_self
(),                  \

                (vm_address_t) srcp, trunc_page (nbytes),     \

                (vm_address_t) dstp) == KERN_SUCCESS          \

             ? trunc_page (nbytes)                    \

             : 0)))

 

页拷贝剩余部分,再做一下字拷贝:

 

#define WORD_COPY_FWD   (       dst_bp,

src_bp,

nbytes_left,

nbytes

     )      

        Value:

     do                                        \

      {                                         \

        if (src_bp % OPSIZ == 0)                            \

          _wordcopy_fwd_aligned (dst_bp, src_bp, (nbytes) /
OPSIZ);         \

           else                                   \

        _wordcopy_fwd_dest_aligned (dst_bp, src_bp, (nbytes) /
OPSIZ);       \

            src_bp += (nbytes) & -OPSIZ;                        \

            dst_bp += (nbytes) & -OPSIZ;                        \

            (nbytes_left) = (nbytes) % OPSIZ;                      
\

          } while (0)

 

再再最后就是多余的一点数据量了,直接字节拷贝截至。memcpy
能够用来消除内部存款和储蓄器对齐难题,同时对于大数据量的内存拷贝,使用 memcpy
功用要高很多,就因为做了页拷贝和字拷贝的优化。

 

  • 抑或尽量制止这种内部存款和储蓄器不对齐的气象,像这一个事例,只要把 +二 改成
    +四,内部存款和储蓄器就对齐了。当然具体还得看逻辑达成的急需。

 

char *mem = malloc(16); // alloc 16 bytes of data

double *dbl = mem + 4;

double set = 10.0;

*dbl = set;

 

References

 

ARM Hacking: EXC_ARM_DA_ALIGN exception

http://www.galloway.me.uk/2010/10/arm-hacking-exc_arm_da_align-exception/

 

GlibC 2.18 memcpy source code

http://fossies.org/dox/glibc-2.18/string_2memcpy_8c_source.html

 

5、堆栈溢出

 

貌似意况下应用程序是不须求思考堆和栈的大大小小的,总是当作丰裕大来使用就能满意一般工作支付。然而其实堆和栈都不是无上限的,过多的递归会促成栈溢出,过多的
alloc 变量会促成堆溢出。

 

例子

 

不得不说 Cocoa 的内部存款和储蓄器管理优化做得挺好的,单纯用 C++ 在 Mac
下编写翻译后实施以下代码,递归 17467壹 次后挂掉:

 

#include

#include

 

void test(int i) {

    void* ap = malloc(1024);

    std::cout

 

而在 iOS 上进行以下代码则怎么也不会挂,连 memory warning 都未有:

 

– (void)stackOverFlow:(int)i {

 

    char * aLeak = malloc(1024);

 

    NSLog(@”try %d”, ++i);

    [self stackOverFlow:i];

}

 

同时壹旦 malloc 的轻重改成比 十24 大的如 10240,其内部存款和储蓄器占用的增强要远慢于
10二四。那大致要归功于 Cocoa 的 Flyweight
设计方式,可是权且还没能真的领悟到其优化原理,估计大概是纵然内部存款和储蓄器空间申请了而是一贯没用到,针对那种循环
alloc 的情形,做了记录,等到用到内部存款和储蓄器空间了才真的付诸空间。

 

原理

 

iOS 内存布局如下图所示:

 

图片 1

 

在应用程序分配的内部存款和储蓄器空间里面,最低地址位是原则性的代码段和数据段,往上是堆,用来存放在全局变量,对于
ObjC 来说,就是 alloc
出来的变量,都会放进那里,堆不够用的时候就会往上申请空间。最顶部高地址位是栈,局地的主干项目变量都会放进栈里。
ObjC
的对象都以以指针实行操控的,局地变量的指针都在栈里,全局的变量在堆里,而随便怎么着指针,alloc
出来的都在堆里,所以 alloc 出来的变量一定要记得 release。

 

对于 autorelease 变量来说,各类函数有二个一拍即合的 autorelease
pool,函数出栈的时候 pool 被灭绝,同时调用那么些 pool 里面变量的 dealloc
函数来完毕其内部 alloc 出来的变量的放飞。

 

陆、二10三十二线程并发操作

 

其一理应是全平台都会赶上的难题了。当有个别对象会被八个线程修改的时候,有不小大概3个线程访问那几个指标的时候另三个线程已经把它删掉了,导致
Crash。相比宽泛的是在网络职务队列之中,主线程往队列之中加入任务,互连网线程同时拓展删减操作造成挂掉。

 

例子

 

本条真要写相比较完好的面世操作的事例就有点复杂了。

 

消除措施

 

  • 加锁

  • NSLock普通的锁,加锁的时候 lock,解锁调用 unlock。

 

– (void)addPlayer:(Player *)player {

   if (player == nil) return;

        NSLock* aLock = [[NSLock alloc] init];

        [aLock lock];

 

        [players addObject:player];

 

        [aLock unlock];

   }

}

 

能够动用标志符 @synchronized 简化代码:

 

– (void)addPlayer:(Player *)player {

   if (player == nil) return;

   @synchronized(players) {

      [players addObject:player];

   }

}

 

  • NSRecursiveLock 递归锁使用普通的 NSLock
    要是在递归的意况下照旧另行加锁的情景下,本人跟自个儿抢财富导致死锁。Cocoa
    提供了 NSRecursiveLock 锁能够频仍加锁而不会死锁,只要 unlock 次数跟
    lock 次数一样就行了。

  • NSConditionLock 条件锁多数景色下锁是不必要关爱怎么着条件下 unlock
    的,要用的时候锁上,用完了就 unlock 就完了。Cocoa
    提供那种原则锁,能够在满意某种条件下才解锁。这几个锁的 lock 和 unlock,
    lockWhenCondition 是随便组合的,能够不要对应起来。

  • NSDistributedLock 分布式锁那是用在多进度之间共享财富的锁,对 iOS
    来说一时没用处。

 

  • 无锁

    舍弃加锁,选取原子操作,编写无锁队列化解三二十四线程同步的标题。酷壳有篇介绍无锁队列的稿子能够参考一下:无锁队列的完结

    http://coolshell.cn/articles/8239.html

  • 动用任何备选方案代替八线程:Operation Objects, GCD, Idle-time
    notifications, Asynchronous functions, Timers, Separate processes。

 

References

 

Threading Programming Guide

http://t.cn/Rcp9lrc

 

七、Repeating NSTimer

 

若果二个 Timer 是不停 repeat,那么自由以前就活该先
invalidate。非repeat的timer在fired的时候会活动调用invalidate,但是repeat的不会。那时固然释放了timer,而timer其实还会回调,回调的时候找不到对象就会挂掉。

 

原因

 

NSTimer 是经过 RunLoop 来贯彻定时调用的,当您成立二个 提姆er
的时候,RunLoop 会持有那些 Timer 的强引用,假诺您创造了贰个 repeating
timer,在下三回回调前就把那些 timer release了,那么 runloop
回调的时候就会找不到指标而 Crash。

 

缓解方案

 

自家写了个宏用来刑满释放Timer

 

/*

* 判断这几个Timer不为nil则结束并释放

* 倘使不先结束可能会促成crash

*/

#define WVSAFA_DELETE_TIMER(timer) { \

    if (timer != nil) { \

        [timer invalidate]; \

        [timer release]; \

        timer = nil; \

    } \

 

}

相关文章