移动app开发中,由于移动设备内存的限制,内存管理是一个非常重要的话题。objective-c的内存管理,不仅是面试当中老生常谈的一个必问话题,也是日常项目开发中,特别需要重视的环节。对于笔者这种以java语言入门编程世界的开发者来说,习惯了垃圾收集器的自动化管理,对于oc的引用计数器管理方式,还是需要花功夫来学习和运用。

1. ARC 和 非ARC

oc的内存管理方式,分为ARC(automatic reference counting自动引用计数)和非ARC模式。Apple 在 Xcode 4.2 中发布了 Automatic Reference Counting (ARC),该功能为开发人员消除了手动执行引用计数的负担。

目前xcode新建项目,都会推荐默认的ARC方式(ARC的确有很大的优势)。当然,如果必须要使用非ARC,可以在build setting中修改automatic reference counting选项为NO。如果在ARC项目中引入非ARC的代码或者静态库,需要在build phases设置相应资源为-fno-objc-ar;相反,非arc项目设置arc使用-fobjc-arc。

ARC与非ARC,从字面上来看,在于是否auto,即是由编译器来自动实现reference counting,还是由开发者手动完成引用计数的加减。作为现在经常使用arc模式来开发的我们来说,ARC大大减少了我们对内存管理的工作,甚至,很多入门开发者完全像开发java应用一样,没有管object的释放。然而对oc来说,从mac os x10.8开始,garbage collector也已废弃,iOS上压根就没有出现过这个概念。iOS上oc的内存管理,本质上是引用计数的管理,理解引用计数,是iOS内存管理的核心。

2. 引用计数

如果一个对象没有被其他对象所引用,则表明该对象已不需要,即可以释放。这就好比人们在一张饭桌上吃饭,如果饭桌上还有人,就表明该饭桌还不可以收拾;只有当所有人都吃好饭离开,使用人数小于1,才表明这张饭桌已使用完,可以清理收拾。 所以对象释放的时机,即是该内存的引用计数小于1. oc中的内存管理方式,就是基于对引用计数的管理。

ARC模式下,编译器完成了引用计数的管理,开发者不需要手动添加引用计数管理的代码(实际上也不允许),所以以下主要通过非ARC模式的代码方式,解释引用计数管理方式。

1》对象的初始化和释放

oc中的object需要继承自统一的父类NSObject。对于一个NSObject的生命周期来说,关注以下几个方法:

  •  alloc
  • new
  • copy
  • mutableCopy
  • dealloc

当调用上述前四种方法时,都会生成新的对象,object的引用计数都会+1,归调用者所有(即需要调用者释放)。系统对象以及库中所有的对象都会遵循这个规则,即所有以上述四种方法名开头的方法,都会使引用计数+1;反之,所有不以该命名开头的返回对象方法,都不会使引用计数+1.对于ARC来说,这种规则被确立为硬性规定。当我们自己自定义对象时,也应遵循这种规范,虽然通过命名规则来体现内存管理方式有些让人奇怪。

dealloc是对象内部的实现方法,当object的引用计数为0,即没有被其他对象引用时,该方法会调用,释放object。需要注意的是,在每个对象的生命周期内,dealloc只会调用一次。然而当引用计数为0时,并不能保证系统何时执行该方法。运行期系统会在何时时机调用dealloc,所以决不应该手动调用dealloc,一旦调用后对象就不可用。

在非ARC对象的dealloc方法中,主要就是释放对象所拥有的引用,除此之外,通常还需要把原来配置的观测行为清理掉,如remove掉kvo和notification的观察者。对于ARC模式来说,并不允许我们直接复写dealloc这些内存管理相关的操作。编译器会利用Objective-c++的特性,为C++对象的.cxx_destruct方法生成代码,释放对象。但是,对于那些通过malloc()生成的内存或者比如CoreFoundation中的对象,并非属于oc对象,开发者需要按照需要自己释放。

在非ARC模式中,我们可以手动控制引用计数的增减。通过以下两个方法:

  • retain
  • release

当调用[object retain],引用计数+1;调用[object release]时,引用计数-1.当object不在使用,需要释放时,调用release使引用计数清0,而不要直接调用dealloc。

NSObject协议中,存在retainCount这个方法,用于查询对象的当前保留计数:

- (NSUInteger)retainCount

 然而它并没有卵用。然而它并没有卵用(重要的事要说两次)。在ARC模式中,内存管理相关的方法都无法通过编译,而在非ARC中,retainCount很多时候并不能反映出对象正常的保留计数。甚至当对象已被释放的情况下,再次调用到retainCount会直接导致crash。

    NSString *str = @"12456";
NSLog(@"str retainCount: %lu",(unsigned long)[str retainCount]);
NSNumber *num = @1;
NSLog(@"num retainCount: %lu",(unsigned long)[num retainCount]);
NSNumber *numF = @3.1415f;
NSLog(@"numF retainCount: %lu",(unsigned long)[numF retainCount]);

  以上代码的结果可能会让你大跌眼镜:

2015-07-24 14:37:19.238 TestMemory[10987:3418823] str retainCount: 4294967295
2015-07-24 14:37:19.241 TestMemory[10987:3418823] num retainCount: 18
2015-07-24 14:37:19.241 TestMemory[10987:3418823] numF retainCount: 1

  str和num对象的retainCount出现了让人不解的数字。实际上编译器对这些对象都做了优化,这种优化只在某些场合才会发生,所以numF对象的retainCount是正常的。所以再次强调,不要试图利用retainCount来做任何操作。

iOS的内存管理,说白了也就是对于引用计数,调用retain和release来告知系统对内存的释放。我们举个栗子,以下代码在非ARC模式中运行:

/*!
* 饭桌
*/
@interface Table : NSObject @end
@implementation Table - (void)dealloc{
NSLog(@"%s",__func__);
[super dealloc];
}
@end /*!
* 客人
*/
@interface Guest : NSObject @property (nonatomic, copy) NSString *name;
@property (nonatomic, retain) Table *table; - (instancetype)initWithName:(NSString *)name;
@end
@implementation Guest - (instancetype)initWithName:(NSString *)name{
self = [self init];
if (self) {
self.name = name;
}
return self;
} - (void)dealloc{
NSLog(@"%@ dealloc",self.name); [self.name release];
[self.table release];
[super dealloc];
} - (void)setTable:(Table *)table{
[table retain];
[_table release];
_table = table;
}
@end

 

//两位客人到一张饭桌上吃饭的过程再现
Table *table = [[Table alloc] init];
NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); Guest *guest_1 = [[Guest alloc] initWithName:@"guest_1"];
NSLog(@"%@ retain count: %lu",guest_1.name,(unsigned long)[guest_1 retainCount]);
guest_1.table = table;
NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); Guest *guest_2 = [[Guest alloc] initWithName:@"guest_2"];
NSLog(@"%@ retain count: %lu",guest_2.name,(unsigned long)[guest_2 retainCount]);
guest_2.table = table;
NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [guest_1 release];
NSLog(@"%@ retain count: %lu",guest_1.name,(unsigned long)[guest_1 retainCount]);
NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [guest_2 release];
NSLog(@"%@ retain count: %lu",guest_2.name,(unsigned long)[guest_2 retainCount]);
NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [table release];
NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]);

    以上实现了一个简单的客人到饭桌吃饭的场景:首先有准备出一张饭桌,来了一位客人1,做到饭桌吃饭,又来了一位客人2,到饭桌吃饭,客人1吃好饭走人,客人2走人,饭桌使用完毕,收拾饭桌。

首先看两个类,Table很简单,类中没有保留的其他对象,dealloc方法调用[super dealloc]保证父类对象的释放。Guest类中,添加了两个属性(@property),属性的修饰符是copy和retain。iOS会对property自动创建get和set方法,而copy/retain/assign等这一类修饰符,表明了set方法中对属性值不同内存管理的方式,这一点后文详述。这里只要知道,copy和retain都会导致set方法中将属性值保留,即retainCount+1。Guest类中加入了自定义的init方法,注意init开头的方法,必须按照oc的代码规范要求才能实现init时调用的目的,如例子中initWithName:这样的camel-case。dealloc方法中实现了对保留对象的释放,最后调用[super dealloc]。setTable:方法复写了property的set方法,实际上只是将retain修饰符属性值的默认set方法用代码再现了一下。可以看到,对于retain的属性值,会先保留新值,后释放旧值,然后将新值赋给属性,实现对象的保留。而set方法的调用者,需要管理所赋值的释放。

以上代码的log如下:

2015-07-24 10:14:07.623 TestMemory[10956:3397055] table retain count: 1
2015-07-24 10:14:07.628 TestMemory[10956:3397055] guest_1 retain count: 1
2015-07-24 10:14:07.629 TestMemory[10956:3397055] table retain count: 2 -> table被guest对象保留,引用计数+1
2015-07-24 10:14:07.630 TestMemory[10956:3397055] guest_2 retain count: 1
2015-07-24 10:14:07.631 TestMemory[10956:3397055] table retain count: 3 -> table被guest对象保留,引用计数+1
2015-07-24 10:14:07.632 TestMemory[10956:3397055] guest_1 dealloc -> guest_1 引用计数为为0,触发dealloc
2015-07-24 10:14:07.632 TestMemory[10956:3397055] guest_1 retain count: 1
2015-07-24 10:14:07.633 TestMemory[10956:3397055] table retain count: 2
2015-07-24 10:14:07.634 TestMemory[10956:3397055] guest_2 dealloc -> guest_2 引用计数为为0,触发dealloc
2015-07-24 10:14:07.634 TestMemory[10956:3397055] guest_2 retain count: 1
2015-07-24 10:14:07.635 TestMemory[10956:3397055] table retain count: 1
2015-07-24 10:14:07.636 TestMemory[10956:3397055] -[Table dealloc] -> table 引用计数为0,触发dealloc
2015-07-24 10:14:07.636 TestMemory[10956:3397055] table retain count: 1

  可以看到,对象通过alloc方法create后,retainCount都会+1。table对象在alloc,又分别被guest_1和guest_2保留,导致retainCount为3。guest调用release方法后,引用计数为0,都会触发guest对象的dealloc方法,导致table对象引用计数-1.最后table调用release,引用计数为0,触发table对象dealloc。

然而,正如上所说,retainCount在这里并没有很好地起到说明引用计数的作用。虽然引用计数都以为0,最后的retainCount都没能显示为0.这可能是运行期内部对retainCount方法做出了处理,或者是对象内存并没有被立即释放,否则对象dealloc后再次调用已释放对象的方法都会导致EXC_BAD_ACCESS的crash发生。

2》属性的所有权语义(ownership semantic)

我们经常看到这样的属性申明:

@property (strong, nonatomic) UIWindow *window;

  strong, nonatomic这些属性修饰语义,表征了属性的特性。oc的编译器会为属性自动生成get和set方法,而这两个方法会涉及对象所有权的保留与释放,诸如strong/weak这些不同的属性修饰符会造成不同的内存操作。ARC和非ARC模式下修饰符是不同的。

非ARC
语义 说明
retain 保留新值,释放旧值,将新值赋给对象。引用计数+1
assign 简单赋值,共享同一块内存地址。引用计数不变
copy 建立一个新的引用计数为1的对象,然后释放旧对象。
ARC
语义 说明
strong 表明拥有对象,同retain。
weak 表明非拥有对象,同assign类似,但当对象被摧毁后,对象会被清空(变为nil)
assign 同非ARC中assign,设置方法只会针对“纯量类型”,如NSInteger,CGFloat等
copy 同非ARC中copy
unsafe_unretained 同assign,但适用于“对象类型”,表明非拥有对象,但当对象摧毁后,对象不会自动清空,即可能会EXC_BAD_ACCESS。

要注意的几个问题:

1.retain/strong 和 copy

本质上,retain/strong是指针拷贝,copy是内容拷贝。举个栗子:

.h
@property (nonatomic, retain) NSString *str1;
@property (nonatomic, copy) NSString *str2; .m
NSMutableString *str0 = [NSMutableString stringWithString:@"str"];
NSLog(@"str0:%lu",(unsigned long)[str0 retainCount]);
self.str1 = str0;
NSLog(@"str0:%lu",(unsigned long)[str0 retainCount]);
self.str2 = str0;
NSLog(@"str0:%@, str1:%@, str2:%@",str0,self.str1,self.str2);
NSLog(@"str0:%lu",(unsigned long)[str0 retainCount]);
NSLog(@"str1:%lu",(unsigned long)[self.str1 retainCount]);
NSLog(@"str2:%lu",(unsigned long)[self.str2 retainCount]);
[str0 appendString:@"+1"];
NSLog(@"str0:%@, str1:%@, str2:%@",str0,self.str1,self.str2);
NSLog(@"str0:%lu",(unsigned long)[str0 retainCount]);
NSLog(@"str1:%lu",(unsigned long)[self.str1 retainCount]);
NSLog(@"str2:%lu",(unsigned long)[self.str2 retainCount]);
[str0 release];
NSLog(@"str0:%@, str1:%@, str2:%@",str0,self.str1,self.str2);
NSLog(@"str0:%lu",(unsigned long)[str0 retainCount]);
NSLog(@"str1:%lu",(unsigned long)[self.str1 retainCount]);
NSLog(@"str2:%lu",(unsigned long)[self.str2 retainCount]);
str0 = nil;
NSLog(@"str0:%@, str1:%@, str2:%@",str0,self.str1,self.str2);
NSLog(@"str0:%lu",(unsigned long)[str0 retainCount]);
NSLog(@"str1:%lu",(unsigned long)[self.str1 retainCount]);
NSLog(@"str2:%lu",(unsigned long)[self.str2 retainCount]);
[self.str1 release];
NSLog(@"str0:%@, str1:%@, str2:%@",str0,self.str1,self.str2);
NSLog(@"str0:%lu",(unsigned long)[str0 retainCount]);
NSLog(@"str1:%lu",(unsigned long)[self.str1 retainCount]);
NSLog(@"str2:%lu",(unsigned long)[self.str2 retainCount]); log:
2015-07-30 16:21:18.936 TestMemory[35070:2648291] str0:1
2015-07-30 16:21:18.936 TestMemory[35070:2648291] str0:2
2015-07-30 16:21:18.936 TestMemory[35070:2648291] str0:str, str1:str, str2:str
2015-07-30 16:21:18.936 TestMemory[35070:2648291] str0:2
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str1:2
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str2:1
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str0:str+1, str1:str+1, str2:str
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str0:2
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str1:2
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str2:1
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str0:str+1, str1:str+1, str2:str
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str0:1
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str1:1
2015-07-30 16:21:18.937 TestMemory[35070:2648291] str2:1
2015-07-30 16:21:20.804 TestMemory[35070:2648291] str0:(null), str1:str+1, str2:str
2015-07-30 16:21:21.939 TestMemory[35070:2648291] str0:0
2015-07-30 16:21:21.939 TestMemory[35070:2648291] str1:1
2015-07-30 16:21:21.939 TestMemory[35070:2648291] str2:1
2015-07-30 16:21:21.940 TestMemory[35070:2648291] str0:(null), str1:
2015-07-30 16:21:21.940 TestMemory[35070:2648291] str0:0
2015-07-30 16:21:21.940 TestMemory[35070:2648291] str1:1152921504606846975
2015-07-30 16:21:21.940 TestMemory[35070:2648291] str2:1

  str0是一个可变string。str1和str2分别是retain和copy的属性。参考引用计数,当str1被赋值后,str0/str1引用计数+1。str2赋值时,由于是copy,stro/str1不会+1,而str2会生成新的引用计数为1的对象。str0值变化后,由于str1指向同一内存,str1和str0的值都会变为"str+1",而str2是单独的内存地址,所以并不变化。str0 release后,要注意的是,str0的引用计数会-1,变为1,这时仍旧可以通过str0来访问到对象内存,即下一行log会打印出str0为"str+1"。一般我们要释放一个对象,不仅需要release,还需要将其赋值为nil,保证下一次访问到该对象时,即便内存被释放,仍旧可以获得nil而不至于野指针crash。当str0赋值为nil后,str0的引用计数也清0.当str1也被release后,会发现log开始出现异常。因为str1的引用计数为0,被系统运行期释放,导致无法保证获取正确的str1值和retainCount。并且会出现EXC_BAD_ACCESS的crash。

2. strong weak 和 assign

经常在ARC模式下见到这个属性的修饰语义。正如字面意思,strong和weak表明了该对象拥有关系的强弱表现。strong就比如你牵着你的宠物狗,狗的行动必须受到你的控制,绳子在你手上,放不放手由你决定。weak就像你和你朋友的狗,你可以跟逗她玩耍,它的跑走是它的自由。strong的属性,赋值时,会保留新值,释放旧值,然后把新值赋给对象。这跟非ARC中的retain是一致的,在将内存地址赋值的同时,将引用计数也相应+1.而weak则不同,它不会产生保留新值和释放旧值的操作,只是将内存地址赋给属性,并没有将引用计数增加,这一点上跟assign类似。不同的是,ARC模式下通常针对非对象的纯量类型用assign,而对象用weak。当weak特性的属性被释放内存后,属性值会被自动赋nil,这样在之后访问该属性时,只会获得空值,而不会出现野指针的情况。assign通常只会针对纯量类型,执行简单的赋值操作。如果用于对象,则有可能出现,当该对象的内存已被释放,则属性指针指向的内存地址已不是想要的内容,出现野指针crash。顺便提一下unsafe_unretained,它跟assign相同,但适用于对象,当对象释放后,不同于weak,属性值不会被赋nil。据说,在iOS5之前的ARC时代,没有weak,通常会用到unsafe_unretained;weak出现之后被weak取代。

Pro.考虑一下经常用到的deleage模式

我们经常会自己自定义一些delegate,而delegate这个属性该如何设置内存修饰语义?参考一下iOS SDK中的UITableView:

@property (nonatomic, assign)   id <UITableViewDelegate>   delegate;

  这里将delegate设置为assign。即在UITableView这个对象中,不关系delegate的内存管理。因为在delegate模式中,UITableView的使用者需要保留UITableView对象,将UITableView的delegate属性设置为self自身。这样一来,如果delegate也是retain/strong等强拥有关系,则会造成保留环(retain cycle)的出现,导致memory leak。当ARC模式出现后,自定义delegate推荐使用weak。在某些iOS5前的工程中,因为没有weak,会使用assign类似的unsafe_unretained。

iOS内存管理(objective-c)的更多相关文章

  1. iOS内存管理

    iOS内存管理的方式是引用计数机制.分为MRC(人式引用计数)和ARC(自动引用计数). 为什么要学习内存管理? 内存管理方式是引用计数机制,通过控制对象的引用计数来实现操作对象的功能.一个对象的生命 ...

  2. 【Bugly干货分享】iOS内存管理:从MRC到ARC实践

    Bugly 技术干货系列内容主要涉及移动开发方向,是由Bugly邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处. 对于iOS程序员来说,内存管理是入门的 ...

  3. iOS内存管理个人总结

    一.变量,本质代表一段可以操作的内存,她使用方式无非就是内存符号化+数据类型 1.保存变量有三个区域: 1>静态存储区 2>stack 3>heap 2.变量又根据声明的位置有两种称 ...

  4. IOS内存管理学习笔记

    内存管理作为iOS中非常重要的部分,每一个iOS开发者都应该深入了解iOS内存管理,最近在学习iOS中整理出了一些知识点,先从MRC开始说起. 1.当一个对象在创建之后它的引用计数器为1,当调用这个对 ...

  5. iOS内存管理编程指南

    iOS 内存管理 目录[-] 一:基本原则 二:成员变量的内存管理 三:容器对象与内存管理 四:稀缺资源的管理 五:AutoRelease 六:其他注意事项 iOS下内存管理的基本思想就是引用计数,通 ...

  6. iOS内存管理(一)

    最近有时间,正好把iOS相关的基础知识好好的梳理了一下,记录一下内存相关方面的知识. 在理解内存管理之前我觉得先对堆区和栈区有一定的了解是非常有必要的. 栈区:就是由编译器自动管理内存分配,释放过程的 ...

  7. iOS内存管理 ARC与MRC

    想驾驭一门语言,首先要掌握它的内存管理特性.iOS开发经历了MRC到ARC的过程,下面就记录一下本人对iOS内存管理方面的一些理解. 说到iOS开发,肯定离不开objective-c语言(以下简称OC ...

  8. 75.iOS内存管理

    堆区和栈区 1.栈区:由编译器自动分配释放,函数的参数值,局部变量等值 2.堆区:一般由开发人员分配释放,若不释放,则可能会引起内存泄漏 NSString *string = @"abcd& ...

  9. iOS 内存管理-copy、 retain、 assign 、readonly 、 readwrite、nonatomic、@property、@synthesize、@dynamic、IB_DESIGNABLE 、 IBInspectable、IBOutletCollection

    浅谈iOS内存管理机制 alloc,retain,copy,release,autorelease 1)使用@property配合@synthesize可以让编译器自动实现getter/setter方 ...

  10. iOS内存管理策略和实践

    转:http://www.cocoachina.com/applenews/devnews/2013/1126/7418.html 内存管理策略(memory Management Policy) N ...

随机推荐

  1. rsync | scp文件同步命令使用

    现在有一台服务器A,目录/data2/abc下存在若干文件夹和文件,需要复制到服务器B中.这时,可以在服务器A上执行rsync或者scp命令,将文件夹或文件复制到服务器B中. SCP: scp /da ...

  2. POJ2159 ancient cipher - 思维题

    2017-08-31 20:11:39 writer:pprp 一开始说好这个是个水题,就按照水题的想法来看,唉~ 最后还是懵逼了,感觉太复杂了,一开始想要排序两串字符,然后移动之类的,但是看了看 好 ...

  3. jQuery获取属性值的方法

    1.利用绑定事件:     $(".callback").on("click","#knbh",function(){      ***** ...

  4. 那些年java MD5加密字符编码的坑

    相信做过MD5加密的童鞋都遇到过字符编码的坑,一般加密出来的结果和其他人不一样都是字符编码不一致导致的,比如类文件的字符编码.浏览器的字符编码等和对方不一致,所以就需要转码统一字符. 以下是笔者转码过 ...

  5. 配置servlet支持文件上传

    Servlet3.0为Servlet添加了multipart配置选项,并为HttpServletRequest添加了getPart和getParts方法获取上传文件.为了使Servlet支付文件上传需 ...

  6. hibernate:inverse、cascade,一对多、多对多详解

    1.到底在哪用cascade="..."? cascade属性并不是多对多关系一定要用的,有了它只是让我们在插入或删除对像时更方便一些,只要在cascade的源头上插入或是删除,所 ...

  7. CountDownLatch await可能存在的问题

    执行countdown的某个子线程可能会因为某些原因无法执行countdown,这样就会导致await线程一直阻塞下去. 在线程池中多次调用await方法,因为await方法会阻塞一段时间,有可能导致 ...

  8. git branch 新建,推送与删除

    在开发的许多时候我们都需要使用git提供的分支管理功能. 1.新建本地分支:git checkout -b test  新建一个名为:test 的本地分支. 2.提交本地分支:git push ori ...

  9. PHP中用下划线开头的含义

    命名的规则 加一个为私有的 加两个一般都是系统默认的,系统预定义的,即所谓:=====================“魔术方法”与“魔术常量”=====================★PHP起止为 ...

  10. 设计模式--装饰模式C++实现

    装饰模式C++实现 1定义 动态地给一个对象添加一些额外的职责.就增加功能来说,装饰模式比生成子类更加灵活.可作为继承的替代 2类图 3实现 //构件 class Component { protec ...