iOS内存管理机制
15 Jan 2016
前言
最近重温了一遍C语言的内存机制,对内存四区模型的理解又稍微深刻了那么一点。回过头来再看iOS的内存管理,其实是有想通之处的。想深入理解iOS的内存管理机制,就得先了解C的内存机制。
C语言内存
内存四区模型
在程序执行之前的过程大概是:
- 操作系统把物理硬盘代码load到内存
- 操作系统把c代码分成四个区
- 操作系统找到main函数入口执行
内存四区
-
栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。后进先出(LIFO)。
-
堆区(heap): 一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。先进先出(FIFO)。
-
数据区:主要包括静态全局区和常量区,如果要站在汇编角度细分的话还可以分为很多小的区。
全局区/静态区(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后有系统释放。
常量区:常量字符串就是放在这里的。程序结束后由系统释放。
- 代码区:存放函数体的二进制代码。
堆栈的生长方向
-
栈是逆向生长,先进栈的所分配的内存空间地址更大。
-
堆是顺序生长,先进栈的所分配的内存空间地址更小。
注意:对于指针指向的所分配的某一块内存(无论是堆还是栈)的首地址永远是这块内存中最小的。
堆栈的简化模型
iOS内存分区
iOS的内存分区跟C语言类似:
-
栈区(stack):存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;程序猿不需要管理栈区变量的内存;栈区地址从高到低分配。
-
堆区(heap):堆区的内存分配使用的是alloc;需要程序猿管理内存;ARC的内存的管理,是编译器再编译的时候自动添加retain、release、autorelease;堆区的地址是从低到高分配。
-
全局区/静态区(static):包括两个部分:未初始化过 、初始化过;也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域;eg:int a;未初始化的。int a = 10;已初始化的。
-
常量区:常量字符串就是放在这里。
-
代码区:存放App二进制代码。
其他事项:
在iOS中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的。
系统使用一个链表来维护所有已经分配的内存空间(系统仅仅纪录,并不管理具体的内容)。
变量使用结束后,需要释放内存,OC中是根据引用计数是否为0时,为0就说明没有任何变量使用该空间,那么系统将直接收回。
当一个app启动后,代码区,常量区,全局区大小已固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这两个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)。
操作系统使用stack段中的指针值访问heap段中的对象。如果stack对象的指针没有了,则heap中的对象就不能访问。这也是内存泄露的原因。
iOS内存管理
iOS开发中,内存中的对象主要有两类,一类是值类型,比如int、float、struct等基本数据类型,另一类是引用类型,也就是继承自NSObject类的所有的OC对象。
值类型会被放入栈中,依次紧密排列,在内存中占有一块连续的内存空间,遵循先进后出的原则。
引用类型会被放到堆中,当给对象分配内存空间时,会随机的从内存当中开辟空间,对象与对象之间可能会留有不确定大小的空白空间,因此会产生很多内存碎片,需要我们管理。
栈内存与堆内存从性能上比较,栈内存要优于堆内存,这是因为栈遵循先进后出的原则,因此当数据量过大时,存入栈会明显的降低性能。因此,我们会把大量的数据存入堆中,然后栈中存放堆的地址,当需要调用数据时,就可以快速的通过栈内的地址找到堆中的数据。
ARC
Objective-C中提供了两种内存管理机制MRC(Mannul Reference Counting)
和ARC(Automatic Reference Counting)
。
这里主要介绍下ARC的机制。ARC是IOS5推出的新功能,通过ARC,可以自动的管理内存。在ARC模式下,只要没有强指针(强引用)指向对象,对象就会被释放。在ARC模式下,不允许使用retain、release、retainCount等方法。并且,如果使用dealloc方法时,不允许调用[super dealloc]
方法。ARC模式下的property变量修饰词为strong、weak,相当于MRC模式下的retain、assign。strong :代替retain,缺省关键词,代表强引用。**weak:代替assign,声明了一个可以自动设置nil的弱引用,但是比assign多一个功能,指针指向的地址被释放之后,指针本身也会自动被释放。**
与内存有关的修饰符
- strong :强引用,ARC中使用,与MRC中retain类似,使用之后,计数器+1。
- weak :弱引用 ,ARC中使用,如果只想的对象被释放了,其指向nil,可以有效的避免野指针,其引用计数为1。
- readwrite : 可读可写特性,需要生成getter方法和setter方法时使用。
- readonly : 只读特性,只会生成getter方法 不会生成setter方法,不希望属性在类外改变。
- assign :赋值特性,不涉及引用计数,弱引用,setter方法将传入参数赋值给实例变量,仅设置变量时使用。
- retain :表示持有特性,setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。
- copy :表示拷贝特性,setter方法将传入对象复制一份,需要完全一份新的变量时。
- nonatomic :非原子操作,不加同步,多线程访问可提高性能,但是线程不安全的。决定编译器生成的setter getter是否是原子操作。
- atomic :原子操作,同步的,表示多线程安全,与nonatomic相反。
MRC与ARC混编
在ARC的项目中,对MRC的文件可以添加编译选项-fno-objc-arc
的标识;在MRC的项目中,对ARC的文件可以添加编译选项-fobjc-arc
的标识。
ARC内存管理
即便有了ARC,也是有可能会内存泄露的。
Block
有这样一个场景:
在网络工具类NetworkFetch
中有一个网络请求的回调Block:
//.h
//定义Block
typedef void (^NetworkCompletionHandler)(NSData *data);
//网络请求方法
- (void)startWithCompletionHandler:(NetworkCompletionHandler)completion;
//.m
//Block属性
@property (nonatomic, copy) (NetworkCompletionHandler)completionHandler;
某个类中使用网络工具类发送请求并处理回调:
- (void)fetchData {
NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
_networkFetcher = [[NetworkFetch alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"request url %@ finished.", _networkFetcher);
_fetcherData = data;
}]
}
很明显在使用block的过程中形成了循环引用:self持有 networkFetecher;networkFetecher持有block;block持有 self。三者形成循环引用,内存泄露。
下面的例子也会造成内存泄露:
- (void)fetchData {
NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
NetworkFetecher *networkFetcher = [[NetworkFetch alloc] initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"request url %@ finished.", networkFetcher);
}]
}
networkFetecher持有block,block持有networkFetecher,形成内存孤岛,无法释放。
解决方案有两种:
- 将对象置为nil,消除引用,打破循环引用。这种方法容易漏掉某个该置nil的属性。
// 代码中任意地方
_networkFetecher = nil;
- 将强引用转换成弱引用,打破循环引用。
__weak __typeof(self) weakSelf = self;
//如果想防止 weakSelf 被释放,可以再次强引用
__typeof(&*weakSelf) strongSelf = weakSelf;
if (strongSelf) {
//do something with strongSelf
}
&*weakSelf
中的&*
主要是为了兼容早期的LLVM。也可以使用RAC比较优雅的@weakify(self)
和@strongify(self)
。
另外方法名带有usingBlock
的CocoaFramework
方法或GCD的API本身会对传入的block做一个复制的操作,也需要注意循环引用的问题。关于Block的内存分配在iOS的Block的内存分配中有深入的阐述。
performSelector
performSelector
就是在运行时执行一个selector。
- (id)performSelector:(SEL)selector;
[object methodName];
[object performSelector:@selector(methodName)];
如果有以下的代码:
SEL selector;
if (/* some condition */) {
selector = @selector(newObject);
} else if (/* some other condition */) {
selector = @selector(copy);
} else {
selector = @selector(someProperty);
}
id ret = [object performSelector:selector];
这段代码就相当于在动态之上再动态绑定。正是由于动态,编译器不知道即将调用的selector是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,所以编译器无法用ARC的内存管理规则来判断返回值是否应该释放。因此,ARC采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能将其持有,从而可能导致内存泄露。
以本段代码为例,前两种情况(newObject, copy)都需要再次释放,而第三种情况不需要。这种泄露隐藏得如此之深,以至于使用static analyzer都很难检测到。如果把代码的最后一行改成:
[object performSelector:selector];
不创建一个返回值变量测试分析,简直难以想象这里居然会出现内存问题。所以如果你使用的selector
有返回值,一定要处理掉。
NSNotificationcenter
这个比较常见,记得removeObserver
就可以了,也可以用RAC的对应方法。
NSTimer
在使用NSTimer addtarget
时,为了防止target被释放而导致的程序异常,timer会持有target,所以这也是一处内存泄露的隐患。
@property (nonatomic, strong) NSTimer *timer;
- (void)someMethod{
timer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(handleTimer:)
userInfo:nil
repeats:YES];
}
异常处理
JAVA开发的同事一直不理解为什么iOS开发没有try...catch
。Apple 提供了错误处理(NSError)和异常处理(NSException)两种机制,而 try...catch
就是使用exception
捕获异常。NSError应用在在绝大部分的场景下,并且这也是Apple所推荐。那什么时候用NSException 呢?在极其严重的直接导致程序崩溃情况下才使用,并且无需考虑恢复问题。
NSArray *array = @[@"a", @"b", @"c"];
@try {
// 可能抛出异常的代码
[array objectAtIndex:3];
}
@catch (NSException *exception) {
// 处理异常
NSLog(@"throw an exception: %@", exception.reason);
}
@finally {
NSLog(@"finally execution");
}
MRC下的try...catch
:
// 注意:在 @try @catch @finally 块内定义的变量都是局部变量
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingMayThrowException];
[object release];
}
@catch (NSException *exception) {
NSLog(@"throw an exception: %@", exception.reason);
}
如果doSomethingMayThrowException
方法抛出了异常,那么object对象就无法释放。如果object对象持有了重要且稀缺的资源,就可能会造成严重后果。
不过,OC中大部分crash如:内存溢出、野指针等都是无法捕获的,而能捕获的只是像数组越界之类(这在代码洁癖的眼中难道不应该提前判断么)。