IOS内存管理(一)基本概念与原理

文章结构


1.内存管理的基本规则
2.autoreleasePool
3.ARC管理方法

3.1 ARC 引入的四个ownership qualifiers
3.2 Property(属性)与ownership qualifier

4.防止内存泄漏编程注意事项

更新记录


一、内存管理的基本规则

在Objective-C的内存管理中,其实就是引用计数(reference count)的管理。内存管理就是在程序需要时程序员分配一段内存空间,而当使用完之后将它释放。如果程序员对内存资源使用不当,有时不仅会造成内存资源浪费,甚至会导致程序crach。我们将会从引用计数和内存管理规则等基本概念开始,然后讲述有哪些内存管理方法,最后注意有哪些常见内存问题。

About Memory Management from Apple Developer

1.1 引用计数(Reference Count)

为了解释引用计数,我们做一个类比:员工在办公室使用灯的情景。

  • 第一个人进入办公室时,他需要使用灯,于是开灯,引用计数为1
  • 当另一个人进入办公室时,他也需要灯,引用计数为2;每当多一个人进入办公室时,引用计数加1
  • 当有一个人离开办公室时,引用计数减1,当引用计数为0时,也就是最后一个人离开办公室时,他不再需要使用灯,关灯离开办公室。

形象点的可以这样解释,从底层的实现来讲就是一个class 结构体,结构体内部有个值(也就是下图所说的retain count)记录了对象拥有者(ownship)的个数,当计数值为0时,系统将自动释放这个对象占用的内存空间。

官方原文

1.2 内存管理基本规则

从上面员工在办公室使用灯的例子,我们对比一下灯的动作与Objective-C对象的动作有什么相似之处:

灯的动作 Objective-C对象的动作
开灯 创建一个对象并获取它的所有权(ownership)
使用灯 获取对象的所有权
不使用灯 放弃对象的所有权
关灯 释放对象

因为我们是通过引用计数来管理灯,那么我们也可以通过引用计数来管理使用Objective-C对象。


引用Pro Multithreading and Memory Management for iOS and OS X的图

而Objective-C对象的动作对应有哪些方法以及这些方法对引用计数有什么影响?

Objective-C对象的动作 Objective-C对象的方法
1. 创建一个对象并获取它的所有权 alloc/new/copy/mutableCopy (RC = 1)
2. 获取对象的所有权 retain (RC + 1)
3. 放弃对象的所有权 release (RC - 1)
4. 释放对象 dealloc (RC = 0 ,此时会调用该方法)

当你alloc一个对象objc,此时RC=1;在某个地方你又retain这个对象objc,此时RC加1,也就是RC=2;由于调用alloc/retain一次,对应需要调用release一次来释放对象objc,所以你需要release对象objc两次,此时RC=0;而当RC=0时,系统会自动调用dealloc方法释放对象。

除了上面所说的alloc/new/copy/mutableCopy/retain这几种方法可以获取对象的所有权(ownship)外,当对象被添加到集合对象(array, dictionary, set)中时,集合对象会获取集合中所有对象的所有权(RC+1),当集合对象释放时,也会默认向集合中所有对象发送release消息(RC -1),符合谁创建谁释放的原则。

官方原文

注意下以下情况是不会获取对象的所有权的:
(1)不使用alloc/new/copy/mutableCopy方法引用的对象将不会获取对象的拥有权

- (NSString *)fullName {
    NSString *string = [NSString stringWithFormat:@"%@ %@",
                                 self.firstName, self.lastName];
    return string;
}

这个函数调用的是NSStringstringWithFormat并不不满足内存管理基本原则,不会拥有对象的所有权,所以可以放心的返回。而不用调用release方法或者autorelease方法。
(2)引用对象指针的地址的方式(they take an argument of type ClassName ** or id *),不会获取对象的拥有权。
这个比较好理解,比如我们常见的error。

    NSString *fileName = <#Get a file name#>;
    NSError *error;
    NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
                                                       encoding:NSUTF8StringEncoding error:&error];
    if (string == nil) {
        // Deal with error...
    }
    // ...
    [string release];

二、 Autorelease Pool


在开发中,我们常常都会使用到局部变量,局部变量一个特点就是当它超过作用域时,就会自动释放。而autorelease pool跟局部变量类似,当执行代码超过autorelease pool块时,所有放在autorelease pool的对象都会自动调用release。它的工作原理如下:

  • 创建一个NSAutoreleasePool对象

  • 在autorelease pool块的对象调用autorelease方法

  • 释放NSAutoreleasePool对象

引用Pro Multithreading and Memory Management for iOS and OS X的图

iOS 5/OS X Lion前的(等下会介绍引入ARC的写法)实例代码如下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// put object into pool
id obj = [[NSObject alloc] init];
[obj autorelease];

[pool drain];

/* 超过autorelease pool作用域范围时,obj会自动调用release方法 */

由于放在autorelease pool的对象并不会马上释放,如果有大量图片数据放在这里的话,将会导致内存不足。

for (int i = 0; i < numberOfImages; i++)
{
      /*   处理图片,例如加载
       *   太多autoreleased objects存在
       *   由于NSAutoreleasePool对象没有被释放
       *   在某个时刻,会导致内存不足 
       */
}

像上面这种情况你就可以这么写:

for (int i = 0; i < numberOfImages; i++)
{
       @autoreleasepool {
      /*   
       *这样临时的autoreleased objects就会在autoreleasepool 结束时释放达到最少的内存占用。
       */
       }
}

二、 ARC管理方法


iOS/OS X内存管理方法有两种:手动引用计数(Manual Reference Counting)和自动引用计数(Automatic Reference Counting)。从OS X Lion和iOS 5开始,不再需要程序员手动调用retainrelease方法来管理Objective-C对象的内存,而是引入一种新的内存管理机制Automatic Reference Counting(ARC),简单来说,它让编译器来代替程序员来自动加入retainrelease方法来持有和放弃对象的所有权。

MRC与ARC

在ARC内存管理机制中,id和其他对象类型变量必须是以下四个ownership qualifiers其中一个来修饰:

  • __strong(默认,如果不指定其他,编译器就默认加入)
  • __weak
  • __unsafe_unretained
  • __autoreleasing

比方说下面这段程序

NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
    // Report the error.
    // ...

开启ARC经过编译器处理后将会变成下面这样:

NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
    // Report the error.
    // ...

接下来看几个例子:

2.1 __strong ownership qualifier

如果变量var被__strong修饰,当变量var指向某个对象objc,那么变量var持有某个对象objc的所有权

如果我想创建一个字符串,使用完之后将它释放调用,使用MRC管理内存的写法应该是这样:

{
    NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"];   //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);
    [text release];                      //@"Hello, world"对象的RC=0
}

而如果是使用ARC方式的话,就text对象无需调用release方法,而是当text变量超过作用域时,编译器来自动加入[text release]方法来释放内存

{
    NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"];    //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);
}
/*
 *  当text超过作用域时,@"Hello, world"对象会自动释放,RC=0
 */

而当你将text赋值给其他变量anotherText时,MRC需要retain一下来持有所有权,当text和anotherText使用完之后,各个调用release方法来释放。

{
    NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"];    //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);

    NSString *anotherText = text;        //@"Hello, world"对象的RC=1
    [anotherText retain];                //@"Hello, world"对象的RC=2
    NSLog(@"%@", anotherText);

    [text release];                      //@"Hello, world"对象的RC=1
    [anotherText release];               //@"Hello, world"对象的RC=0
}

而使用ARC的话,并不需要调用retain和release方法来持有跟释放对象。

{
    NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"];   //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);

    NSString *anotherText = text;        //@"Hello, world"对象的RC=2
    NSLog(@"%@", anotherText);
}
/*
 *  当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法, @"Hello, world"对象的RC=0
 */

除了当__strong变量超过作用域时,编译器会自动加入release语句来释放内存,如果你将__strong变量重新赋给它其他值,那么编译器也会自动加入release语句来释放变量指向之前的对象。例如:

{
    NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"];    //@"Hello, world"对象的RC=1
    NSString *anotherText = text;        //@"Hello, world"对象的RC=2
    NSString *anotherText = [[NSString alloc] initWithFormat:@"Sam Lau"];  // 由于anotherText对象引用另一个对象@"Sam Lau",那么就会自动调用[anotherText release]方法,使得@"Hello, world"对象的RC=1, @"Sam Lau"对象的RC=1
}
/*
 *  当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法,
 *  @"Hello, world"对象的RC=0和@"Sam Lau"对象的RC=0
 */

前面已经提过内存管理的四条规则:

Objective-C对象的动作 Objective-C对象的方法
1. 创建一个对象并获取它的所有权 alloc/new/copy/mutableCopy (RC = 1)
2. 获取对象的所有权 retain (RC + 1)
3. 放弃对象的所有权 release (RC - 1)
4. 释放对象 dealloc (RC = 0 ,此时会调用该方法)

我们总结一下编译器是按以下方法来实现的:

  • 对于规则1和规则2,是通过__strong变量来实现,
  • 对于规则3来说,当变量超过它的作用域或被赋值或成员变量被丢弃时就能实现
  • 对于规则4,当RC=0时,系统就会自动调用

2.2 __weak ownership qualifier

其实编译器根据__strong修饰符来管理对象内存。但是__strong并不能解决引用循环(Reference Cycle)问题:对象A持有对象B,反过来,对象B持有对象A;这样会导致不能释放内存造成内存泄露问题。

引用Pro Multithreading and Memory Management for iOS and OS X的图

举一个简单的例子,有一个类Test有个属性objc,有两个对象test1和test2的属性objc互相引用test1和test2:

@interface Test : NSObject

@property (strong, nonatomic) id objc;

@end


{
    Test *test1 = [Test new];        /* 对象a */
    /* test1有一个强引用到对象a */

    Test *test2 = [Test new];        /* 对象b */
    /* test2有一个强引用到对象b */

    test1.objc = test2;              /* 对象a的成员变量objc有一个强引用到对象b */
    test2.objc = test1;              /* 对象b的成员变量objc有一个强引用到对象a */
}
/*   当变量test1超过它作用域时,它指向a对象会自动release
 *   当变量test2超过它作用域时,它指向b对象会自动release
 *   
 *   此时,b对象的objc成员变量仍持有一个强引用到对象a
 *   此时,a对象的objc成员变量仍持有一个强引用到对象b
 *   于是发生内存泄露
 */

如何解决?于是我们引用一个__weakownership qualifier,被它修饰的变量都不持有对象的所有权,而且当变量指向的对象的RC为0时,变量设置为nil。例如:

__weak NSString *text = [[NSString alloc] initWithFormat:@"Sam Lau"];
NSLog(@"%@", text);

由于text变量被__weak修饰,text并不持有@"Sam Lau"对象的所有权,@"Sam Lau"对象一创建就马上被释放,并且编译器给出警告⚠️,所以打印结果为(null)。

所以,针对刚才的引用循环问题,只需要将Test类的属性objc设置weak修饰符,那么就能解决。

@interface Test : NSObject

@property (weak, nonatomic) id objc;//修改成weak修饰符

@end

以及我们常用的block防止内存泄漏也可以使用__weak 修饰符,引用官方给的例子如下:

MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController) {
        // ...
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
        // ...
    }
    else {
        // Probably nothing...
    }
};

2.3 __unsafe_unretained ownership qualifier

__unsafe_unretained ownership qualifier,正如名字所示,它是不安全的。它跟__weak相似,被它修饰的变量都不持有对象的所有权,但当变量指向的对象的RC为0时,变量并不设置为nil,而是继续保存对象的地址;这样的话,对象有可能已经释放,但继续访问,就会造成非法访问(Invalid Access)。例子如下:

__unsafe_unretained id obj0 = nil;
    
{
    id obj1 = [[NSObject alloc] init];     // 对象A
    /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */

    obj0 = obj1;
    /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */
        
    NSLog(@"A: %@", obj0);
}
/* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */
 
NSLog(@"B: %@", obj0);
/* 由于obj0是__unsafe_unretained,当它指向的对象RC=0时,它会继续保存对象的地址,所以两个地址相同 */

打印结果是内存地址相同

如果将__unsafe_unretained改为weak的话,两个打印结果将不同

__weak id obj0 = nil;
    
{
    id obj1 = [[NSObject alloc] init];     // 对象A
    /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */

    obj0 = obj1;
    /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */
        
    NSLog(@"A: %@", obj0);
}
/* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */
 
NSLog(@"B: %@", obj0);
/* 由于obj0是__weak, 当它指向的对象RC=0时,它会自动设置为nil,所以两个打印结果将不同*/

2.4 __autoreleasing ownership qualifier

引入ARC之后,让我们看看autorelease pool有哪些变化。没有ARC之前的写法如下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// put object into pool
id obj = [[NSObject alloc] init];
[obj autorelease];

[pool drain];

/* 超过autorelease pool作用域范围时,obj会自动调用release方法 */

引入ARC之后,写法比之前更加简洁:

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

相比之前的创建、使用和释放NSAutoreleasePool对象,现在你只需要将代码放在@autoreleasepool块即可。你也不需要调用autorelease方法了,只需要用__autoreleasing修饰变量即可。

引用Pro Multithreading and Memory Management for iOS and OS X的图

但是我们很少或基本上不使用autorelease pool。当我们使用XCode创建工程后,有一个app的入口文件main.m使用了它:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

2.4 Property(属性)与ownership qualifier


有了ARC之后,新的property modifier也被引入到Objective-C类的property,例如:

@property (strong, nonatomic) NSString *text;

下面有张表来展示property modifier与ownership qualifier的对应关系

Property modifier Ownership qualifier
strong __strong
retain __strong
copy __strong
weak __weak
assign __unsafe_unretained
unsafe_unretained __unsafe_unretained

我们先看下当我们命名一个属性为retain 时,然后调用@synthesize时编译器将会做什么。引用官方原文如下:



setter方法:

 - (void)setCount:(NSNumber *)newCount {
        [newCount retain];
        [_count release];
        // Make the new assignment.
        _count = newCount;
    }

也就是这里会首先retain新值,然后释放旧值(旧值RC -1),然后再赋值新值。

相同的当命名一个属性为copy类型时
setter方法:

 - (void)setCount:(NSNumber *)newCount {
        [_count release];
        // Make the new assignment.
        _count = [newCount copy];
    }

retain与copy的区别在于,一个不产生新的对象只是对对象的RC + 1,另一个产生新的对象。

除了上面提到的属性修饰符外还有atomic (default)nonatomicreadonlyreadwrite


只能一个线程访问,线程安全的(相当于线程锁的概念,一个时间段只有一个线程访问不容易出错,所以线程安全),低性能 等等

参考文章:
官方文档Advanced Memory Management Programming Guide
官方文档Transitioning to ARC Release Notes
官方文档Memory Management Programming Guide for Core Foundation
官方文档Transitioning to ARC Release Notes

iOS/OS X内存管理(一):基本概念与原理

iOS/OS X内存管理(二):借助工具解决内存问题

IOS 出现内存泄漏的几种原因

assign、copy 、retain等关键字的含义

Variable property attributes or Modifiers in iOS

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

推荐阅读更多精彩内容

  • 对于OC的使用者来说,最会被问到的就是iOS开发中的内存管理。而只要涉及到内存管理,就肯定会涉及到property...
    zkhCreator阅读 1,792评论 4 18
  • 最近在拉钩看到一个面试评论,有个哥们在优酷面完后评论--“ARC下有谁会用autorelease,问我aut...
    Cstars阅读 411评论 2 0
  • 总结自 美团点评技术沙龙 Online 第4期 内存管理的几种方式 显式内存释放(C:-free、C++:-del...
    Mokyz阅读 1,054评论 0 1
  • # 前言 反复地复习iOS基础知识和原理,打磨知识体系是非常重要的,本篇就是重新温习iOS的内存管理。 内存管理是...
    Vein_阅读 777评论 0 2
  • 自动引用计数,又称ARC(Automatic Reference Counting)是苹果在iOS5中引入的重要特...
    Cyyyyyyyy阅读 1,173评论 1 10