iOS内存管理

<h5>iOS内存分区</h5>
<ul>
<li>栈区,内存管理由系统负责,一个线程对应一个栈区,服从先进后出原则
</li>
<li>堆区,内存管理由程序员负责,一个应用程序对应一个堆区,作为全局资源在整个程序中共享</li>
<li>全局区/静态区,包括两个部分,未初始化过、初始化过;也就是说,(全局区/静态区)在内存中是放在一起的初始化的全局变量和静态变量放在一块区域,未初始化的全局、静态变量放在另一块区域;eg:int a,未初始化的;int a=10,初始化的</li>
<li>常量区,用于存储常量字符串</li>
<li>代码区,存放App二进制代码</li>
</ul>
<h5>堆栈的简化模型</h5>


stack.gif

<h5>堆与栈的比较</h5>
每个线程对应一个栈区,但是整个应用对应一个堆区,

生命周期

栈,与线程绑定,线程创建时分配,线程结束时回收
堆,应用启动时分配,应用退出时回收

所占内存空间大小

栈的内存大小在线程创建时确定;堆的内存大小在应用程序启动时确定,但是可以根据需求增长

性能

栈的内存块通过栈顶指针移动(或数值改变)来进行分配和回收,同一块内存的重复使用率极高,故而栈的创建速度远高于堆;另外,堆作为全局资源需要考虑多线程安全的问题,堆内存的分配与销毁需要与多个heap access同步

对象的创建

栈对象的创建,只要栈的剩余空间大于栈对象申请的空间 ,操作系统将为程序提供这段空间,否则报栈溢出异常;堆对象的创建,操作系统有一个用于记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找一个空间大于所申请的堆对象的节点,然后将此节点从空闲链表中删除,并将空间分配给程序。

对象的存储

int、float、struct等基本数据类型(值类型)存储在栈中,依次紧密排列,占用一块连续的内存空间;继承自NSObject的所有OC对象(引用类型)存储在堆中,对象和对象之间留有不确定大小的空白,因此会产生很多内存碎片

内存管理

栈区是线程的暂存空间,函数调用时,一个内存块会被压入栈顶,用于存储局部变量和一些bookkeeping data(参数?);函数返回时,内存块被回收,等待下一次的函数调用;堆区用于动态内存分配,不存在严格的内存块分配与回收机制,内存的分配或者回收可以发生在任意时刻
故而我们常说栈的内存分配由系统负责,堆的内存分配由程序猿来管理

为什么Objective-C对象要放在堆中?

有一些博客为了引导读者更好地理解栈与堆的相互关系,会有如下写法:栈存储局部变量,堆用于存储全局变量;这其实是一种错误的说法,所有的Objective-C对象都存储在堆中,但是我们不能直接访问这些变量,而是通过存储在栈中的指针变量访问它们
下图可以帮助你很好的理解栈和堆的关系

demostration.png

为什么要这么存储呢?因为栈遵循先进后出的原则,当存入数据量过大时,存入栈会明显的降低性能;因此将大量的数据放入堆中,然后在栈中存放堆的地址,当须要调用数据时,可以快速地通过栈内的地址找到堆中的数据

MRR&ARC

部分博客将MRR叫做MRC(Manual Reference Count),我在此处采用苹果官方的开发者文档的说法——MRR(Manual Retain Relase);其实两种说法都可以体现内存管理的实质。
Objective-C采用引用计数(Reference Counting)管理对象的生命周期,当对象被持有时,它的引用计数加1;当持有者不再持有这个对象时,引用计数减1;对象的引用计数为0时,意味着它可以被销毁,过程如图:

reference-counting.png

上文中采用较为书面化的“持有”一词可能会让读者有些困惑,对象的持有在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消息,直到遇到第一个哨兵;过程如下图:

pop_release.png

如图所示,@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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容

  • ARC ARC是 Object-C 编译特性, 不是运行时特性也不是垃圾回收机制, ARC 所做的只是在代码编译自...
  • 转自iOS经典面试题总结--内存管理 - CocoaChina_让移动开发更简单 内存管理 1.什么是ARC? A...
    赤洱阅读 219评论 0 0
  • 1. 内总管理原则(引用计数) IOS的对象都继承于NSObject, 该对象有一个方法:retainCount...
    lilinjianshu阅读 2,145评论 0 2
  • 内存管理 简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与a...
    丶逐渐阅读 1,930评论 1 16
  • 1 选择到这个博物馆来工作,不知道算不算明智。 因为这里和我大学的专业完全无关。 我带着简单的行李,站在宫殿一般的...
    紫梦谭阅读 229评论 1 0