<h5>iOS内存分区</h5>
<ul>
<li>栈区,内存管理由系统负责,一个线程对应一个栈区,服从先进后出原则
</li>
<li>堆区,内存管理由程序员负责,一个应用程序对应一个堆区,作为全局资源在整个程序中共享</li>
<li>全局区/静态区,包括两个部分,未初始化过、初始化过;也就是说,(全局区/静态区)在内存中是放在一起的初始化的全局变量和静态变量放在一块区域,未初始化的全局、静态变量放在另一块区域;eg:int a,未初始化的;int a=10,初始化的</li>
<li>常量区,用于存储常量字符串</li>
<li>代码区,存放App二进制代码</li>
</ul>
<h5>堆栈的简化模型</h5>
<h5>堆与栈的比较</h5>
每个线程对应一个栈区,但是整个应用对应一个堆区,
生命周期
栈,与线程绑定,线程创建时分配,线程结束时回收
堆,应用启动时分配,应用退出时回收
所占内存空间大小
栈的内存大小在线程创建时确定;堆的内存大小在应用程序启动时确定,但是可以根据需求增长
性能
栈的内存块通过栈顶指针移动(或数值改变)来进行分配和回收,同一块内存的重复使用率极高,故而栈的创建速度远高于堆;另外,堆作为全局资源需要考虑多线程安全的问题,堆内存的分配与销毁需要与多个heap access同步
对象的创建
栈对象的创建,只要栈的剩余空间大于栈对象申请的空间 ,操作系统将为程序提供这段空间,否则报栈溢出异常;堆对象的创建,操作系统有一个用于记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找一个空间大于所申请的堆对象的节点,然后将此节点从空闲链表中删除,并将空间分配给程序。
对象的存储
int、float、struct等基本数据类型(值类型)存储在栈中,依次紧密排列,占用一块连续的内存空间;继承自NSObject的所有OC对象(引用类型)存储在堆中,对象和对象之间留有不确定大小的空白,因此会产生很多内存碎片
内存管理
栈区是线程的暂存空间,函数调用时,一个内存块会被压入栈顶,用于存储局部变量和一些bookkeeping data(参数?);函数返回时,内存块被回收,等待下一次的函数调用;堆区用于动态内存分配,不存在严格的内存块分配与回收机制,内存的分配或者回收可以发生在任意时刻
故而我们常说栈的内存分配由系统负责,堆的内存分配由程序猿来管理
为什么Objective-C对象要放在堆中?
有一些博客为了引导读者更好地理解栈与堆的相互关系,会有如下写法:栈存储局部变量,堆用于存储全局变量;这其实是一种错误的说法,所有的Objective-C对象都存储在堆中,但是我们不能直接访问这些变量,而是通过存储在栈中的指针变量访问它们
下图可以帮助你很好的理解栈和堆的关系
为什么要这么存储呢?因为栈遵循先进后出的原则,当存入数据量过大时,存入栈会明显的降低性能;因此将大量的数据放入堆中,然后在栈中存放堆的地址,当须要调用数据时,可以快速地通过栈内的地址找到堆中的数据
MRR&ARC
部分博客将MRR叫做MRC(Manual Reference Count),我在此处采用苹果官方的开发者文档的说法——MRR(Manual Retain Relase);其实两种说法都可以体现内存管理的实质。
Objective-C采用引用计数(Reference Counting)管理对象的生命周期,当对象被持有时,它的引用计数加1;当持有者不再持有这个对象时,引用计数减1;对象的引用计数为0时,意味着它可以被销毁,过程如图:
上文中采用较为书面化的“持有”一词可能会让读者有些困惑,对象的持有在MRR和ARC中有不同的表述方法,笔者认为MRR更有助于理解内存管理的实质,所以在下文中重点介绍MRR。
NSObject Protocol中定义了一系列方法,来帮助程序猿手动管理Objective-C对象的引用计数:
<p> alloc 创建一个新对象,引用计数为1
</p>
<p> retain 持有该对象,引用计数为加1
</p>
<p> copy 复制当前对象,新对象的引用计数为1
</p>
<p> release 放弃持有,对象的引用计数减1
</p>
<p> autorelease 放弃持有,但是延迟对象的销毁
</p>
在前文可以看到,我们通过栈中指向对象地址的指针变量来访问存储在堆中的Objective-C对象;而栈区的数据具有一定的生命周期,当函数返回时,函数中定义的所有局部变量(local variable)和形参都会被销毁;这意味着,如果我们没在函数结束之前release这些指针变量指向的对象的话,存放在堆中的对象将永远不会销毁,而且我们不知道怎么访问这些不会被销毁的对象,因为存放其地址的指针已经被销毁掉了。
不再适用的对象未被释放,即是内存泄漏;小的内存泄漏可能影响不大,但是随着内存占用的增加,你的程序最终会崩溃 。
所以在MRR时代,程序员们这样写代码:
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:@"Jimmy C"];
NSLog(@"%@", array);
[array release];
}
return 0;
}
alloc方法在创建array对象的同时,使其引用计数为1;由于此段代码中局部变量array是指向array对象的唯一指针,所以我们必须在函数返回之前将其释放。
那么问题来了,实例变量应该在什么时候释放呢?以下代码中,ViewController类定义了一个实例变量_array
// ViewController.m
#import "ViewController.h"
@implementation ViewController {
NSMutableArray *_array;
}
//省略代码
@end
(类中的property本质是对实例变量的封装,在此我们不将其单独拿出来讨论)
类被创建以后,实质上是一个OC对象,当这个对象即将被销毁时,runtime会调用NSObject中定义的dealloc方法。
如果ViewController对象被销毁,则其实例变量_array也一并被销毁,这样_array指向的对象则永远不可能被释放;所以程序员们需要重写dealloc方法 ,释放成员变量:
// ViewController.m
- (void)dealloc {
[_array release];
[super dealloc];
}
了解了这些,我们似乎就可以用alloc、retain和release方法对实例变量和成员变量进行内存管理了呢
......
当然不可以
Autorelease对象什么时候释放?
有些方法需要返回对象,故而不能使用release直接释放对象,而是用autorelease延迟对象的释放。
// CarStore.m
+ (CarStore *)carStore {
CarStore *newStore = [[CarStore alloc] init];
return [newStore autorelease];
}
autorelease对象什么时候释放呢?它在最近的@autoreleasepool{}结束时释放。
AutoreleasePool
AutoreleasePool是帮助管理内存对象的好伙伴,它实质上是一个由AutoreleasePoolPage对象连接而成的双向链表,每个Page对象会开辟4096字节内存,除了存储对象的成员变量,剩余的空间全部用来存储Autorelease对象的地址。
我们通过@autoreleasepool{}来使用AutoreleasePool,编译器将会把它改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
objc_autoreleasePoolPush()的作用是向当前的AutoreleasePoolPage对象add进一个哨兵对象(POOL_SENTINEL),值为nil;该哨兵对象传入objc_autoreleasePoolPop方法时,会向AutoreleasePool中的每个autorelease对象发送release消息,直到遇到第一个哨兵;过程如下图:
如图所示,@autoreleasepool{}是可以嵌套使用的。
整个iOS应用都是包含在一个自动释放池中的,如果你打开项目的main.m文件,会看到如下代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
main.m是整个应用程序的入口,@autoreleasepool只包含了一行代码,这行代码将所有的事件、消息全部交给了UIApplication来处理。
当然,仅仅使用一层autoreleasepool是远远达不到优化内存管理的目的的,事实上,系统在每个RunLoop迭代中都加入了自动释放池push和pop。
AutoreleasePool与runloop
RunLoop是OS X/iOS中用于实现EventLoop的机制,一般来讲,线程一次只能执行一个任务,执行完成后线程就退出;而RunLoop提供的函数入口能够使线程始终处于函数内部的“接受消息-等待-处理”的循环当中。
RunLoop被用于实现事件响应、手势识别、界面更新等功能;主线程的RunLoop在所有回调之前调用_objc_autoreleasePoolPush(),在所有回调之后调用_objc_autoreleasePoolPop()释放自动池。
由此可见,AutoreleasePool通过多层嵌套的方式遍布整个应用;故而在不特意写@autoreleasepool{}的情况下,我们也会说Autorelease对象是在当前的RunLoop迭代结束后释放的。
RunLoop和autoreleasepool都与线程一一对应。
RunLoop在AFNetworking中的实际应用,为了能在后台接受NSURLConnection的回调,AFNetworking单独创建了一个线程,并在这个线程启动了一个RunLoop,看源码可知RunLoop的启动代码被放在@autoreleasepool{}中:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
ARC(Automatic Reference Counting)
理解了MRR后,ARC就变得相当简单了;ARC所做的事情是在编译阶段在合适的地方插入retain、release和autorelease,其具体的使用也有很多有趣的地方,读者可以前往唐巧的博客<a href="http://blog.devtang.com/2016/07/30/ios-memory-management/">理解iOS的内存管理</a>一文阅读学习。
相关异常
错误的内存管理主要会造成以下两种异常
<ul>
<li>内存崩溃(memory corruption),释放或覆盖了尚在使用中的数据</li>
<li>内存溢出(memory leaks),未释放不再使用的数据</li>
</ul>
http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
http://draveness.me/autoreleasepool/