ACCESSiOS中时表现 Crash 及缓解方案

来源:枫影JustinYan

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

 

同一、访问了一个已于释放的目标

 

当匪动 ARC 的上,内存要和谐管理,这时再或过早释放都产生或引致
Crash。

 

例子

 

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

[aObj release];

 

NSLog(@”%@”, aObj);

 

原因

 

aObj
这个目标已经于放飞,但是指针没有置空,这时访问这指针指向的内存就会
Crash。

 

解决办法

 

  • 利用前要判非空,释放后只要置空。正确的刑释解教应该是:

 

[aObj release];

aObj = nil;

 

出于ObjC的性状,调用 nil
指针的任何方式相当给无图,所以即便有人在动用这指针时莫看清至少还无见面挂掉。

每当ObjC里面,一切基于
NSObject 的目标都运指针来进行调用,所以在无法保证该指针一定有值的情下,要先行判断指针非空再进行调用。

 

if (aObj) {

    //…

}

 

科普的只要判断一个字符串是否为空:

 

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];

        //….

    }

}

 

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

 

NSMutableArray/NSMutableDictionary/NSMutableSet 等类下标越界,或者
insert 了一个 nil 对象。

 

原因

 

一个稳定数组有雷同块连续内存,数组指针指为内存首地址,靠下标来计量元素地址,如果下标越界则指针偏移有这块内存,会访问到野数据,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++ 很无等同。 C++
在编译的当儿就是已绑定了看似与道,一个看似不容许调用一个请勿有的方,否则即报编译错误。而
ObjC 则是于 runtime 的时段才去寻觅应该调用哪一个艺术。

 

立即简单种实现各级发优劣,C++ 的绑定使得调用方法的时光快迅猛,但是只能通过
virtual 关键字来贯彻有限的动态绑定。而针对 ObjC
来说,事实上他的落实是同一种消息传递而不是道调用。

 

[aObj aMethod];

 

如此的言语应该亮吧,像 aObj 对象发送一个叫作 aMethod 的信息,aObj
对象吸收到是消息后,自己失去搜寻是否会调用对应之方式,找不至则上父类找,再找找不顶即
Crash。由于 ObjC
的这种特性,使得该消息未特可兑现方式调用,还能紧系转发,对一个 obj
传递一个 selector 要求调用某艺术,他得以直接不理会,转发给别的 obj
让别的 obj 来响应,非常灵活。

 

例子

 

[self methodNotExists];

 

调用一个不在的措施,可以编译通过,运行时直接挂掉,报
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 通常是一个
id 泛型,所以 IDE 也不见面报警告,所以这种时刻要用 respondsToSelector
方法先判断一下,然后还展开调用。

 

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

    [self methodNotExist];

}

 

季、字节对旅

 

恐是因为强制类型转换或者强制写内存等操作,CPU 执行 STMIA
指令时发现写副的内存地址不是理所当然边界,就会见硬件报错挂掉。iPhone 5s 的 CPU
从32个变成64个,有或会见起部分字节对同的题材导致 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或1。

 

假使内存空间最小单元是byte(字节),一个byte为8独bit。

 

内存地址空间以byte划分,所以理论及看内存地址可以由任意byte开始,但是其实我们无是直接看硬件地址,而是经过操作系统的虚拟内存地址来聘,虚拟内存地址是坐字也单位的。一个32各类机器的字长就是32各项,所以32各项机器一样蹩脚访问内存大小就是4个byte。再者为了性考虑,数据结构(尤其是仓)应该尽量地于本边界及对共同。原因在于,为了看未对一起之内存,处理器需要发两差内存访问;而针对联合之内存访问仅需一致软拜访。

 

举一个板栗:

 

struct foo {

    char aChar1;

    short aShort;

    char aChar2;

    int i;

};

 

地方这个结构体,在32号机器及,char 长度也8各类,占一个byte,short
占2独byte, int 4个byte。

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

 

aChar1 0x00000000

aShort 0x00000001

aChar2 0x00000003

i      0x00000004

 

而事实上编译后,这些变量的地方是这样的:

 

aChar1 0x00000000

aShort 0x00000002

aChar2 0x00000004

i      0x00000008

 

旋即便是 aChar1 和 aChar2 都深受做了内存对同步优化,都改成 2 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
自己之兑现就已召开了字节对一起的优化了。我们来拘禁glibc2.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
(这里定义为
16),则直字节拷贝就了了,如果盖此价值,视为大内存块拷贝,采用优化算法。

 

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
效率要大多,就因为做了页拷贝和字拷贝的优化。

 

  • 或尽量避免这种内存不针对一头的情形,像这个例子,只要把 +2 改成为
    +4,内存就对伙同了。当然具体还得看逻辑实现的待。

 

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

 

五、堆栈溢出

 

相似情形下行使程序是未需考虑堆和库房的轻重缓急的,总是当作足够大来使用就可知满足一般工作支出。但是其实堆和仓库都不是无论上限的,过多之递归会促成栈溢出,过多的
alloc 变量会造成堆溢起。

 

例子

 

只好说 Cocoa 的内存管理优化做得好好之,单纯用 C++ 在 Mac
下编译后实行以下代码,递归 174671 次后挂掉:

 

#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 的尺寸改成为比 1024 大的如 10240,其内存占用的增进而远慢于
1024。这大概要归功给 Cocoa 的 Flyweight
设计模式,不过暂时性还尚无能够真懂到其优化原理,猜测可能是则内存空间申请了可一直未曾利用,针对这种循环
alloc 的面貌,做了笔录,等及用到内存空间了才真的受来空间。

 

原理

 

iOS 内存布局如下图所示:

 

ACCESS 1

 

每当应用程序分配的内存空间里面,最低地址位是稳定的代码段和数据段,往上是积,用来存放全局变量,对于
ObjC 来说,就是 alloc
出来的变量,都见面加大上这里,堆不敷用底时光便会向上申请空间。最顶部高地址位是仓,局部的主干型变量都见面推广上栈里。
ObjC
的对象都是盖指针进行操控的,局部变量的指针都于栈里,全局的变量在积里,而无什么指针,alloc
出来的都以积里,所以 alloc 出来的变量一定要记 release。

 

对此 autorelease 变量来说,每个函数有一个相应的 autorelease
pool,函数出栈的时段 pool 被销毁,同时调用这个 pool 里面变量的 dealloc
函数来兑现该中间 alloc 出来的变量的放飞。

 

六、多线程并发操作

 

此应该是全平台都见面遇见的题材了。当某个对象见面给多个线程修改的时段,有或一个线程访问这个目标的时节任何一个线程已经将她删掉了,导致
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
    来说暂时没有因此处。

 

  • 无锁

    放弃加锁,采用原子操作,编写无锁队列解决多线程同步的题目。酷壳有首介绍无锁队排的章可参考一下:无锁队排的贯彻

    无锁队列的实现

  • 应用外备选方案代替多线程: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 来兑现定时调用的,当您创造一个 Timer
的下,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; \

    } \

 

}

相关文章