来源:枫影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 0x1dd961602013-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 内存布局如下图所示:
每当应用程序分配的内存空间里面,最低地址位是稳定的代码段和数据段,往上是积,用来存放全局变量,对于
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; \
} \
}