Objective-C 内存管理

来源:互联网 发布:js includes 编辑:程序博客网 时间:2024/06/16 00:09

苹果在 2011 年的时候,在 WWDC 大会上提出了自动的引用计数(ARC)。ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,从而彻底解放程序员。

在 ARC 刚刚出来的时候,业界对此黑科技充满了怀疑和观望,加上现有的 MRC 代码要做迁移本来也需要额外的成本,所以 ARC 并没有被很快接受。直到 2013 年左右,苹果认为 ARC 技术足够成熟,直接将 macOS(当时叫 OS X)上的垃圾回收机制废弃,从而使得 ARC 迅速被接受。

2014 年的 WWDC 大会上,苹果推出了 Swift 语言,而该语言仍然使用 ARC 技术,作为其内存管理方式。

所以OC有三种内存管理方式:垃圾回收(Garbage Collection,适用于macOS开发,不能用于iOS开发),MRC(Mannul Reference Counting,手动内存管理),ARC(Automatic Reference Counting,自动引用计数)。

MRC和ARC都是基于引用计数(Reference Count)实现的,只不过MRC是通过开发者手动添加retain/release语句来管理引用计数,ARC则是编译器自动添加retain/release语句,实现引用计数的管理。

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。

当对引用计数为1的对象,进行最后一次release操作时,理论上是将对象引用计数减为0再回收对象,但是实际上release操作之后该对象的引用计数可能为1或0。
这是因为当最后一次执行 release 时,系统知道马上就要回收内存了,就没有必要再将 retainCount 减 1 了,因为不管减不减1,该对象都肯定会被回收,而对象被回收后,它的所有的内存区域,包括 retainCount 值也变得没有意义。不将这个值从 1 变成 0,可以减少一次内存的写操作,加速对象的回收。


内存管理

内存管理的范围:任何继承了NSObject的对象,对其他基本数据类型(int、char、float、double、struct、enum等)无效
其根本原因在于对象和其他数据类型在系统中的存储空间不一样,基本数据类型的变量主要存放于栈中,而对象存储于堆中,系统会自动释放栈中的内存,却不会管理堆中的内存。

当代码块结束时这个代码块中涉及的所有局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。
内存管理就是确保开辟的堆空间得到正确的释放
如果堆空间没有释放,称为内存泄露
使用已释放的堆空间,称为提前释放
重复释放同一个空间,称为重复释放

内存中的五大区域:
栈区,堆区,BBS段,数据段和代码段,其中除了堆区以外,其他区域的内存管理由系统自行回收。
OC对象是存储在堆区的,所以OC的内存管理主要是对“堆区中的OC对象”进行管理。

一些概念

相关博客:

iOS 关于僵尸对象和僵尸指针的那些事儿

野指针与僵尸对象

内存回收的本质:
1. 申请一块空间,实际上是向系统申请一块别人不再使用的空间.
2. 释放一块空间,指的是占用的空间不再使用,这个时候系统可以分配给别人去使用。但是在这个空间分配给别人之前,数据还是存在的.
3. OC对象释放以后,表示OC对象占用的空间可以分配给别人。但是再分配给别人之前,这个空间仍然存在,对象的数据仍然存在

对于代码:
Person *p = [[Person alloc] init];
个人理解:
- alloc 方法是申请了一块要用来存Person类的实例的内存地址A
- init 方法是初始化了一个Person类的实例,也就是有了一个对象O。然后O是存放在了A中
- = 赋值号将O赋值给了*p,所以*p表示的就是内存A中的O对象
- p则是指向这块内存的指针变量,存的是A的地址0X111111…
(这样理解到底对不对啊,有点晕晕的)

接下来考虑一些其他的概念:

僵尸对象,首先僵尸对象是一个对象,也就是A块内存存储的对象O。对象O是有引用计数概念的。当对象O的引用计数变为0,系统会标记对应的内存A不可用,待回收了,这个时候O就成为了僵尸对象

所以僵尸对象就是引用计数已经变为0,对应保存对象的内存已经不可用,但是内部保存的对象数据还没有清除时候的对象。(自我理解,不知道对不对)

简单的来说,僵尸对象是已经被释放的对象。

野指针,野指针就是指向不可用内存的指针(当对象O的一些操作,导致引用计数变成0之后,这块内存A已经不可用了,但是这个时候指针p还是指向这块内存的,此时指针p就是一个野指针)

使用野指针访问僵尸对象.有的时候会出问题,有的时候不会出问题:

  • 当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候,这个时候其实是可以访问的,因为对象的数据还在并未清空

  • 当野指针指向的对象所占用的空间分配给了别人的时候,这个时候访问僵尸对象就会出问题.

所以,不要通过一个野指针去访问一个僵尸对象。虽然偶尔能通过野指针访问到已经被释放(引用计数为0)的对象,但是我们不允许这么做。

如何避免僵尸对象报错,当1个指针变为野指针以后. 就把这个指针的值设置为nil(OC中对nil指针发消息是不会报错的)

MRC内存管理

MRC中涉及引用计数的方法的基本使用

  1. retain 方法:让引用计数加1,并返回对象本身
  2. release 方法:让引用计数减1,没有返回值
  3. retainCount 方法:返回对象当前的引用计数(当前有多少指针指着这块内存)
  4. dealloc 方法:
    • 当一个对象要被回收的时候就会调用,相当于对象的临终遗言(我当年添加了哪些观察者,现在我要挂了,哪些观察者也给挂掉吧,之类的)
    • 重写dealloc方法时,一定要调用[super dealloc];,并且这句调用要放在最后。

MRC的精华,Setter方法

现在很多代码规范里面都会有一段,规定必须是用@property命令来声明成员变量,不单单是因为使用点语法书写起来特别方便,更多的是在MRC时代所带来的巨大便利,那就是自动生成和调用Setter方法(原子和非原子暂且不谈)。

先来段代码看看Setter方法的书写方式

先在@interface里面声明一个成员变量,并写上Setter方法

这里写图片描述

再在@implementation里面实现Setter方法

这里写图片描述

会不会感觉很恐怖,在没有@property命令的时候每一个成员变量都需要编写Setter方法,如果是一个有着一堆成员变量的Model类,写起来太多重复工作。

我们来把Setter方法拆解开来,用例子来讲为什么要这么写

先写出一个只有赋值操作的的setter方法

这里写图片描述

这样我们在为这个成员变量赋值时调用这个方法就可以了,但是

这么写是有问题的,这种是浅拷贝,只是把指针地址赋值过去了,它的引用计数并没有变化,这样两个指针指向了一块引用计数为1的内存空间,有引起野指针的潜在风险

,那么为了避免这个问题,我们给newValue调用一下retain方法,让它每次赋值操作都能增加一次引用计数。

这里写图片描述

但是光是这么写还是不可以,依然存在着很大的隐患,想象一下,如果是这个对象赋值给自己会是什么样子,一般不会直接发生这种情况,但换种思路 A赋值给B,B又赋值给A,这种是不是就比较常见了,如果B也实现了这种setter方法,那问题就比较大了,我们来分析一下

当A初始化的时候,引用计数为1,赋值给B后,引用计数为2,B再赋值给A时引用计数再加1,成了3,抛开内存管理原则不谈,单单你在做项目的时候,真的能记好或者分的清要释放几次吗?那么我们来解决这个问题,我们知道A和B都是指向的同一个内存空间,那么A和B这两个指针中存储的内存地址一定是一样的,指针可以直接使用运算符来操作,我们直接使用一个!=就可以判断两个指针是不是指向同一块内存空间了,于是我们把代码改成这样

这里写图片描述

还有最后一个问题要解决,假如,A和B都指向了一个内存,引用计数为2,这时,我将C赋值给了A,那么此时,A和C指向了一个内存,引用计数为2,B独自指向一块内存,引用计数为2,这样最终可能会导致B内存泄漏,那么我们就应该在A改变其内存指向之前将A release一次,这样分析得出最终的Setter方法

要改变A指向的内存地址之前,先把原始指向的内存地址中的对象引用计数减去(意味着我已经不持有你了,你的拥有者减少了我一个,你赶紧把你的引用计数减1吧),再指向新的内存地址,同时新内存中对象的引用计数加1

这里写图片描述

另外,一个项目中适量的内存泄漏是允许的,MRC的项目你很难做到每个都是完美释放的,但一定要确保,占用内存较大的一定要准确释放,像VIewController,这个要是泄漏了,轻则用一段时间就崩溃,重则根本无法使用。

最后总结一句,MRC不管是三大原则还是其他什么,总之就是一句话,你要真正确保这个对象的引用计数最终为0!

文/戴上耳机世界与我无关(简书作者)
原文链接:http://www.jianshu.com/p/6ceffec74a4d
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

手动内存管理的原则

引用来自书本《Objective-C基础教程(第二版)》,9.2节 Cocoa的内存管理规则:

Cocoa的内存管理规则只有如下三条:
1. 当你使用new、alloc、或copy方法创建一个对象时,该对象的保留计数器的值为1.当不再使用该对象时,你应该向该对象发送一条release或autorelease消息。这样,该对象将在其使用寿命结束时自动被销毁。
2. 当你通过其他方法获得一个对象时,假设该对象的保留计数器的值为1,而且已经被设为为自动释放,那么你不需要执行任何操作来确保该对象得到清理。如果你打算在一段时间内拥有该对象,则需要保留它并确保在操作完成时释放它(也就是记得要retain和release)。
3. 如果你保留了某个对象,就需要(最终)释放或自动释放该对象。必须保持retain方法和release方法的使用次数相等。


临时对象:
你正在代码中使用某个对象,但是并未 打算长期拥有该对象。如果你是用new,alloc,或copy方法获得的这个对象,就需要安排好该对象的内存释放,通常用release来实现:

NSMutableArray *array;
array = [NAMutableArray alloc] init]; // count = 1;
// 在这之间使用array
[array release]; // 使用完之后记得release,让count = 0

如果你使用其他方法获得一个对象,比如arrayWithCapacity:方法,这不需要关心如何销毁该对象。

NSMutableArray *array;
array = [NAMutableArray arrayWithCapactiy:17];
// array指向的对象现在的count是1,但是是autoreleased的
// 直接使用array,后面不用管释放

arrayWithCapacity:方法与alloc,new和copy方法不一样,因此可以假设该对象被返回时保留计数器的值为1且已经被设置为自动释放。当自动释放池被销毁时,向array对象发送release消息,该对象的保留计数器的值归0,其占用的内存被回收。
使用NSColor对象的部分代码如下:

NSColor *color;
color = [NSColor blueColor];
// 直接使用color,后面不用管释放

blueColor方法也不属于alloc,new和copy这三个方法,因此可以假设该对象的保留计数器的值为1并且已经设置为自动释放。blueColor方法返回一个全局单例(singleton)对象–每个需要访问它的程序都可以共享的单一对象。这个对象永远不会被销毁,不过你不需要关心其实现细节,只需要知道不需要自己手动来释放color。


拥有对象
通常,你可能希望在多段代码中一直拥有某个对象。典型的方法是,把他们加入到诸如NSArray或NSDictionary等集合中,作为其他对象的实例变量来使用,或作为全局变量来使用(少见)。
如果你使用了new、alloc、或copy方法获得了一个对象,则不需要执行任何其他操作。该对象的保留计数器值为1,因此它将一直存在着,你只需要确保在拥有该对象的dealloc方法中释放它即可。
- (void)doStuff {
// flonkArray是一个实例变量
flonkArray = {[NSMutableArray alloc] new]; // 引用计数为1
}
// 重写dealloc方法中,释放
- (void)dealloc {
[flonkArray release]; // 让count变为0
[super dealloc];
}

如果你使用除alloc,new或copy之外的方法获得了一个对象,需要记得保留该对象。如果你编写的是GUI程序,要考虑到事件循环。你需要保留自动释放的对象,以便这些对象在当前的事件循环结束以后仍能继续存在。

以下部分来自博客:
http://www.cnblogs.com/decomfor/p/5324125.html

  • 一旦创建一个对象,这个对象的引用计数器的值就为1,所以必须要匹配1个release
  • 只有在多1个人使用这个对象的时候才retain 只有在少1个人使用这个对象的时候才release
  • retain的次数要和release次数相匹配
  • 永远不要手动调用对象的dealloc方法,而是让系统自动调用(当对象的引用计数变为0时,就会自动调用对象的dealloc方法)

手动内存管理中内存泄露的几种情况
1. retain和release不匹配,retain多于release导致的内存泄露;
2. 对象使用过程中,没有先被release,而直接被被赋值为nil
3. 在方法中不当的使用了retain;

手动内存管理的关键就是防止内存泄露!!!!!
防止内存泄露要记住
1.谁创建”alloc”,”new”,谁”release”;
2. 谁”retain”,谁”release”;

多对象的手动内存管理:

当B类作为A类属性时,要防止内存泄露,则A类的setter方法应为:

这里写图片描述

其中setter方法实现还可写成:

这里写图片描述

以上是标准的MRC内存管理代码.

MRC下的循环retain问题:

  • 遇到的问题:
    当两个对象相互引用时,即A对象的属性指向B对象, B对象的属性指向A对象时,如果两边都使用retain.就会出现内存泄露. 都回收不了.

  • 解决方案:
    一端使用retain,一端使用assign.(请查看@property带参数用法的相关内容)
    需要注意的是: 使用assign的那一段,dealloc中不需要再去release这个对象了(因为并没有retain).

自动释放池(autorelease)

原理

存储在自动释放池的对象,在自动释放池销毁时,会自动调用该对象的release方法,故将对象存储在自动释放池中,就不需要再写release

创建方法

@autorelease{} //大括弧表示自动释放池的范围

将对象放入的方法

在自动释放池的范围中调用对象的autorelease方法。
注:autorelease的返回值是对象本身,所以我们可以这样创建对象:

@autorelease{类型 *对象 = [类名 alloc] init] autorelease];}

使用注意

1.只有在自动释放池中调用了对象的autorelease方法,这个对象才会被存储到这个自动释放池之中
2.对象的创建可以在自动释放池的外面,在自动释放池之中调用对象的autorelease方法,就可以将这个对象存储到这个自动释放池之中.
3.当自动释放池结束的时候.仅仅是对存储在自动释放池中的对象发送1条release消息 而不是销毁对象.
4. 如果在自动释放池中,调用同1个对象的autorelease方法多次.就会将对象存储多次到自动释放池之中.在自动释放池结束的时候.会为对象发送多条release消息.那么这个时候就会出现僵尸对象错误.
5. 自动释放池可以嵌套.调用对象的autorelease方法,会讲对象加入到当前自动释放池之中,只有在当前自动释放池结束的时候才会像对象发送release消息.

使用规范

我们一般情况下写一个类,会为我们的类写一个同名的类方法,用来让外界调用类方法来快速的得到一个对象。
应遵守规范:
使用类方法创建的对象,要求这个对象在方法中就已经被autorelease过了.这样,我们只要在自动释放池中, 调用类方法来创建对象, 那么创建的对象就会被自动的加入到自动释放中.

ARC内存管理

自动内存管理原则

ARC是自iOS 5之后增加的新特性,完全消除了手动管理内存的烦琐,编译器会自动在适当的地方插入适当的retain、release 、autorelease语句,为对象做引用计数。开发者完全不需要担心内存管理 ,因为编译器为你处理了一切(除非你错用了ARC)。

ARC 是编译器特性,而不是 iOS 运行时特性,它也不是类似于其它语言中的垃圾收集器。因此 ARC 和手动内存管理性能是一样的,有时还能更加快速,因为编译器还可以执行某些优化

ARC原理:ARC的规则非常简单:只要还有一个强指针变量指向对象,对象就会保持在内存中

ARC的判断准则:只要没有强指针指向对象,就会释放对象

ARC特点

  1. 不允许调用release、retain、retainCount
  2. 允许重写dealloc,但是不允许调用[super dealloc]
  3. @property的参数
    • strong :成员变量是强指针(适用于OC对象类型)
    • weak :成员变量是弱指针(适用于OC对象类型)
    • assign : 适用于非OC对象类型
  4. 以前的retain改为用strong

ARC只对可保留(Retainable)的对象指针(ROPS)有效,ROPS主要有三种:
1. 代码块指针;
2. Objective-C对象指针
3. 通过attribute((NSObject))类型定义的指针
所有其他的指针类型,比如char *和CF对象(例如CFStringRef)都不支持ARC特性。如果你使用的指针不支持ARC,那么你将不得不亲自手动管理它们。这样也没有问题,因为ARC可以与手动内存管理共同发挥作用。

强指针与弱指针

  • 强指针:默认情况下,我们声明的指针都为强指针,也可以使用__strong来显示的声明指针为强指针.
  • 弱指针:使用__weak关键字修饰的指针,例如 __weak Person *p;

作用与区别:

在ARC模式下,强指针与弱指针用来作为回收对象的标准,当一个对象即使用弱指针指向,但没有任何强指针指向时就会被立即回收,此时该弱指针会被自动设置为nil.

ARC模式下的循环引用

在ARC机制下,如果出现了循环引用,既A对象中有1个属性是B对象. B对象中有1个属性是A对象.此时如果两边都为strong.就会发生内存泄露.

解决方案:一端使用strong 另外一端使用weak

最后补充记录下ARC的新规则(ARC Enforces New Rules)

以下来自博客:
https://my.oschina.net/wealpan/blog/182720

为了正常运转,ARC使用了一些在使用其它编译模式下没有的新规则。这些规则意在提供完全可靠的内存管理模型;在某些情况下,它们仅使用最佳实践方法,在其它情况下,它们仅简化你的代码或明显的做出推论告知你不需要处理内存管理。如果你违反了这些规则,你会马上得到一个编译期错误,而不是在运行期可能会显现的一个狡猾的bug。

1.不能显示的调用dealloc,实现或调用 retain, release, retainCount,或 autorelease。
同样也不要能使用 @selector(retain), @selector(release), 等等类似的选择器。

如果你需要管理资源而不是释放实例变量,那你可以实现 dealloc方法。你不需要(事实上你不能)释放实例变量,但你可能需要在系统类和其它的未使用ARC代码中调用 [systemClassInstance setDelegate:nil] 方法。

在ARC中自定义的 dealloc方法不要调用 [super dealloc]方法(它实际上会导致编译器错误)。到super的链式调用是自动的并且是编译器强制执行的。你仍可以在Core Foundation样式的的对象上,使用CFRetain, CFRelease,和其它相关的函数。

2.你不能使用 NSAllocateObject 或 NSDeallocateObject
你使用 alloc来创建对象;运行时系统会注意释放这些对象。

3.你不能在C语言结构体中使用对象指针。
与其使用一个结构体( struct),不如创建一个Objective-C类来管理数据。

4.id与 void*之间不能随意转换
你必须使用特定的类型转换来告诉编译器对象的生命周期。你需要在Objective-C对象和以函数参数传入的Core Foundation类型值之间进行这样的转换。有关详情,参见“Managing Toll-Free Bridging”。

5.你不能使用 NSAutoreleasePool对象
ARC 提供了 @autoreleasepool来代替。这比 NSAutoreleasePool更高效。

6.你不能使用内存区(memory zones)。
再也没有使用 NSZone的必要了——现代的Obj-C运行时会永远忽略它。

为了允许与自动retain-release的代码进行交互,ARC在方法命名上加上了一个约束:
你不能以 new为开头命名一个访问器的名字。这反过来意味着你不必声明一个以new开头的属性,除非你指定一个不同名称的getter方法:

// Won't work: @property NSString *newTitle; // Works: @property (getter=theNewTitle) NSString *newTitle;

附几篇博客

OC–内存管理

OC-内存管理(跟上面差不多)

理解 iOS 的内存管理–唐巧

http://blog.sina.com.cn/s/blog_a2c098b50101gtu4.html

0 0