iOS 内存管理

# 前言

反复地复习iOS基础知识和原理,打磨知识体系是非常重要的,本篇就是重新温习iOS的内存管理。

内存管理是管理对象生命周期,在对象不需要时进行内存释放的编程规范。

# 目录

  • 前言

  • 目录

  • MRC时代

    • 概要
    • Memory Management Policy 内存管理策略
    • Practical Memory Management 实际内存管理
    • 内存管理实践
      • 使用访问器方法使内存管理更轻松
      • 使用访问器方法设置属性
      • 不要在初始化和dealloc中使用访问器方法
      • 使用弱引用来避免循环引用
      • 避免正在使用的对象被释放
      • Collections类拥有它们所包含的对象所有权
      • 通过引用计数实现所有所有权策略
  • ARC时代

    • 概要
    • ARC 强制新规则
    • 内存泄漏
    • block使用中出现循环引用
    • NSTimer循环引用
  • 参考资料

# MRC时代

概要

Objective-C内存管理使用使用引用计数(Reference Counting)来管理内存。

在OS X 10.8以后也不再使用垃圾回收机制,iOS则从来都没有支持垃圾回收机制。

create或者copy对象时,会计数为1,其他对象需要retain时,会增加引用计数。持有对象的所有者也可以放弃所有权,放弃所有权时减少计数,当计数为0时就会释放对象。
如图:

memory_management 图片来自官方文档

Memory Management Policy 内存管理策略

  • 通过分配内存或copy来创建任何对象
  • 使用方法 alloc, allocWithZone:, copy, copyWithZone:, mutableCopy , mutableCopyWithZone:创建对象
  • 通过retain来获取不是自己创建对象的所有权。以下两种情况使用retain:
  1. accessor method或者init method方法获取所需要的对象所有权为属性property
  2. 需要操作对象时,避免对象被释放而导致错误,需要retain持有对象。
  • 发送release, autorelease消息来释放不需要的对象。
  • 不要不是你创建的对象和没有所有权的对象发送release消息。

Practical Memory Management 实际内存管理

  • Autorelease pools
  • 向对象发送autorelease消息,会将对象标记为延迟释放,当对象超出当前作用域时,释放对象。
  • AppKit frameworksUIKit frameworks在事件循环的每个周期开始时,在主线程上创建一个自动释放池,并在此次时间循环结束时,释放它,从而释放在处理时生成的所有自动释放的对象。因此,通常不需要自己创建autoreleasePool,当然,以下情况你需要自己创建和销毁autoreleasePool
  1. 如果你编写的代码不是基于UI framework的程序,如command-line tool命令行工具。
  2. 如果你需要写一个循环,创建许多临时对象,如读入大量的铜像同时改变图片尺寸,图像读入到NSData对象,并从中生成UIImage对象,改变该对象尺寸生成新的UIImage对象。
  3. 如果你创建一个长期存在线程并且可能产生大量的autorelease对象。

autoreleasePool推荐使用以下方法:

@autoreleasepool {
 //do something
}
  • dealloc
    NSObject对象的引用计数为0时,销毁该对象前会调用dealloc方法,用来释放该对象拥有的所有资源,包裹实例变量指向的对象。
    例子:
 // MRC
 - (void)dealloc{
    [_firstName release];
    [_lastName release];
    [super dealloc];
 }

Important: Never invoke another object’s dealloc method directly.You must invoke the superclass’s implementation at the end of your implementation.You should not tie management of system resources to object lifetimes; see Don’t Use dealloc to Manage Scarce Resources.When an application terminates, objects may not be sent a dealloc message. Because the process’s memory is automatically cleared on exit, it is more efficient simply to allow the operating system to clean up resources than to invoke all the memory management methods.
不要直接调用另一个对象的dealloc方法。你必须在类使用结束时调用父类的实现。你不应该把系统资源与对象的生命周期绑定。
因为进程的内存退出时,对象可能无法发送dealloc消息,该方法的内存被自动退出清零,所以让操作系统清理资源比调用所有的内存管理方法更有效。

内存管理实践

使用访问器方法使内存管理更轻松

如果类有一个属性是一个对象,你必须确保使用该对象时,它不会被释放。因此在设置时,必须声明对象的所有权。还必须保证持有这些对象所有权的放弃。

  • 使用setget方法来实现,更方便管理内存(主要是省写很多retainrelease)。
    例子如下:
@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;

Counter类有一个属性是NSNumber对象,属性声明了setget两个访问器方法,在get中就是返回synthesized实例变量,所以没必要retain或者release

- (NSNumber *)count {
 return _count;
}

set方法:

- (void)setCount:(NSNumber *)newCount {
    [newCount retain]; // 先`retain`确保新数据不被释放
    [_count release];  // 释放旧对象所有权
    // Make the new assignment.
    _count = newCount;  // 将新值赋给_count
}

retain确保新数据不被释放,释放旧的对象所有权(Objective-C允许向nil发送消息)。你必须在[newCount retain]之后再[_count release]确保外部不会被dealloc

使用访问器方法设置属性

// 方法一
- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}
// 方法二
- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}

方法二没有对count属性赋新值时没有使用set访问方法,也不会触发KVO,可能在特殊情况导致错误(比如忘记了 retain或者release,或者如果实例变量的内存管理发生了变化)。除了第一种方法,或者直接使用self.count = zero;

不要在初始化和dealloc中使用访问器方法

不应该使用setget方法在initdealloc。应该使用_直接访问成员变量进行初始化和dealloc。如下:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}
// 由于Counter类具有对象实例变量,因此还必须实现dealloc方法。
// 它应该通过向任何实例变量发送一个释放消息来放弃它的所有权,最终它应该调用super的实现
- (void)dealloc {
    [_count release];
    [super dealloc];
}

使用弱引用来避免循环引用

  • retain对象,实际是对对象的强引用(strong reference),一个对象在所有强引用都没有被释放之前,不能释放对象。因此,如果有两个对象互相持有对方或者间接互相引用,会导致循环引用。这时候就需要弱引用对方来打破这个循环。

如父亲强引用儿子,儿子强引用孙子,那么倒过来孙子只能弱引用儿子,儿子也只能弱引用父亲。Cocoa建立了一个约定,副对象应该强引用子对象,并且子对象应该只对父对象弱引用。
Cocoa中常见的例子包括代理方法delegatedata source,observer,target等等

必须小心将消息发送到持有只是一个弱引用的对象。当发送消息给一个被dealloc的弱引用对象时,你的应用程序会崩溃(这是在MRC时期的代理delegate会出现,因为当时对代理弱引用的修饰符是assign,assign弱引用并不会在对象dealloc时,把对象置为nil。而ARC时代使用weak则会在对象dealloc时置为nil)。

避免正在使用的对象被释放

  • Cocoa的所有权策略规定接收的对象通常在整个调用方法的范围内保证有效。还应该是在当前方法范围内,而不必担心它被释放。对象的getter方法返回一个缓存的实例变量或者一个计算的值,这不重要,重要的是,对象在需要的使用时还是有效的。
  • 有两类例外情况:
  • 当一个对象从基本的集合类删除时
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject 现在可能无效
  • n从集合array删除时也会向n发送release(而不是autorelease)消息。如果array集合时被删除n对象的唯一拥有者,被移除的对象n是立即被释放的。heisenObject并没有对n进行retain,所以当narray删除时同时被释放。

正确的做法

heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// Use heisenObject...
[heisenObject release];
  • 当一个父对象被释放时
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject 现在可能无效
  • 在某些情况下,从另一个对象获取的对象,然后直接或者间接的释放负对象。如果释放父对象导致它被释放,并且父对象是子对象唯一所有者,那么子对象heisenObject将被同一时间释放。所以正确的做法还是子对象heisenObject获取的时候先retain一次。

Collections类拥有它们所包含的对象所有权

  • 添加一个对象到一个collection中,如(数组、字典、集合)时,collection会得到该对象所有权。当对象从collection删除或者collection自己被释放时,collection将释放它拥有的所有权。
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
    NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
    [array addObject:allocedNumber];
    [allocedNumber release];
}

通过引用计数实现所有所有权策略

  • 所有圈策略是通过引用计数实现的,通常retain方法后被称为retain count。每个对象都有一个引用计数。

  • 当你创建一个对象,它的引用计数为1

  • 当你给对象发送retain消息,引用计数+1

  • 当你给对象发送release消息,引用计数-1

  • 当你给对象发送一个autorelease消息,它的引用计数器将在当前的自动释放池结束后-1

  • 当对象的引用计数为0时将被释放

# ARC时代

概要

iOS5后出现了ARC。那么ARC是什么呢?
自动引用计数ARC是一种编译器的功能,为Objective-C对象提供了自动化的内存管理。
ARC不需要开发者考虑保留或者释放的操作,就是不用自己手动retainreleaseautorelease(😄开心),让开发者可以专注写有趣的代码。
当然ARC依然是基于引用计数管理内存。

ARC 强制新规则

ARC相对于MRC强制加了一些新的规则。

  • 你不能主动调用dealloc、或者调用retain,release, retainCount,autorelease就是这些都不用你写了。也不能@selector(retain), @selector(release)这样子调用。
  • 你可以实现一个dealloc方法,如果你需要管理资源而不是释放实例变量(比如解除监听、释放引用、socket close等等)。在重写dealloc后需要[super dealloc](在手动管理引用计数时才需要)。
  • 仍然可以使用CFRetainCFRelease等其它对象。
  • 你不能使用NSAllocateObject或者NSDeallocateObject
  • 你不能使用C结构体,可以创建一个Objective-C类去管理数据而不是一个结构体。
  • idvoid没有转换关系,你必须使用cast特殊方式,以便在作为函数参数传递的Objective-C对象和Core Foundation类型之间进行转换。
  • 你不能使用NSAutoreleasePool,使用@autoreleasepool
  • 没必要使用NSZone

ARC 使用新修饰符

  • __strong 强引用,用来保证对象不会被释放。
  • __weak弱引用 释放时会置为nil
  • __unsafe_unretained弱引用 可能不安全,因为释放时不置为nil
  • __autoreleasing对象被注册到autorelease pool中方法在返回时自动释放。

内存泄漏

ARC还是基于引用计数的管理机制所以依然会出现循环引用。

block使用中出现循环引用

  • 常见的有情况在block使用中出现循环引用
// 情况一
self.myBlock = ^{
self.objc = ...;
};
// 情况二
Dog *dog = [[Dog alloc] init];
dog.myBlock = ^{
  // do something
};
self.dog = dog;
  • 解决方法
__weak typeof (self) weakSelf = self;
self.myBlock = ^{
weakSelf.objc = ...;
};
  • 那么如果block内使用了self这个时候如果某一个时刻self被释放就会导致出现问题。
  • 解决方法
__weak typeof (self) weakSelf = self;
self.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
strongSelf.objc1 = ...;
strongSelf.objc2 = ...;
strongSelf.objc3 = ...;
};
  • 使用__weak打破循环引用。__strong用来避免在使用self过程中self被释放,__strongblock后会调用objc_release(obj)释放对象。
id __strong obj = [[NSObject alloc] init];

// clang 编译后
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);

两次调用objc_msgSend并在变量作用域结束时调用objc_release释放对象,不会出现循环引用问题。

NSTimer循环引用

为什么NSTimer会导致循环引用呢?

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti 
           target:(id)aTarget 
           selector:(SEL)aSelector 
           userInfo:(nullable id)userInfo 
           repeats:(BOOL)yesOrNo;
           
           
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti 
                  target:(id)aTarget 
                  selector:(SEL)aSelector 
                  userInfo:(nullable id)userInfo 
                  repeats:(BOOL)yesOrNo;
  • 主要是因为NSRunloop运行循环保持了对NSTimer的强引用,并且NSTimertarger也使用了强引用。

  • 来自文档NSTimer

Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
target
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.

举个🌰:


@interface ViewController ()<viewControllerDelegate>

@property (strong, nonatomic) NSTimer *timer;

@end

 - (void)viewDidLoad
 {
     [super viewDidLoad];
     self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                               target:self 
                                             selector:@selector(onTimeOut:) 
                                             userInfo:nil 
                                              repeats:NO];
 }
  • 这里控制器强引用了timer,而timer也强引用了控制器,这个时候就是循环引用了,引用关系如下图:
retain cycle
  • 那么如果控制器对timer使用了weak呢?
    使用weak是打破了循环引用,但是run loop还是强引用着timer,timer又强引用着控制器,所以还是会导致内存泄漏。引用关系如下图:
leak

如果我们把timer加入主线程的runloop,主线程中的runloop生命周期只有主线程结束才会销毁,所以我们不主动调用[timer invalidate],runloop会一直持有timertimer又持有控制器,那么就一直不会释放控制器。

  • 解决方法:手动调用[timer invalidate]来解除持有关系,释放内存。可能会想到在dealloc方法中来手动调用,但是因为timer持有控制器,所以控制器的dealloc方法永远不会调用,因为dealloc是在控制器要被释放前调用的。在Timer Programming Topics中有特别说明。所以一般我们可以在下面这些方法中手动调用[timer invalidate]然后置为nil
- (void)viewWillDisappear:(BOOL)animated; // Called when the view is dismissed, covered or otherwise hidden. Default does nothing
- (void)viewDidDisappear:(BOOL)animated;  // Called after the view was dismissed, covered or otherwise hidden. Default does nothing

A timer maintains a strong reference to its target. This means that as long as a timer remains valid, its target will not be deallocated. As a corollary, this means that it does not make sense for a timer’s target to try to invalidate the timer in its dealloc method—the dealloc method will not be invoked as long as the timer is valid.

# 参考资料

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

推荐阅读更多精彩内容