obj-c编程11:内存管理和ARC(自动引用计数)

简介:

    乖乖隆地洞,这篇文章内容可是不得了,内存管理哦!首先,这个要是搞不明白,你就等着进程莫名其妙的挂死,或是疯狂申请内存却不释放,结果被OS杀死,不管是“自杀”还是“他杀”,都不是那么好玩的哦。其次要记住这可不是windows 中的内存管理(Win32 api),也不是linux中C like的内存管理方法。这个比他们都“高级”的多啊!但是没有ruby的高级,也没有ruby的简单,如果mac编程用ruby的就好了,这不搞出一个雨燕(SWFIT)来啊!

    在Xcode4.2发布前,内存管理的确是令人恐怖的主题,都把细节推给我们码农了啊。随着4.2的发布,加入了新的自动应用计数特性(Automatic Reference Counting,ARC),码农门不再脱发着思考内存管理问题鸟。

    obj-c基本内存管理模型有以下3种:

1 自动垃圾收集

2 收工引用计数和自动释放池

3 自动引用计数(ARC)

我们依次来看一下吧。


【1】自动垃圾收集

存在于obj-c 2.0中,iOS里不支持垃圾收集(不知现在是否如此哦),仅os x支持。当进程运行到某个低内存的临界点时,自动开始清理垃圾,这是一个计算密集过程,可能导致进程挂起,所以不推荐使用该特性鸟。


【2】手动管理内存计数

一般的当对象创建时,初始引用计数为1,以后每当引用一次该对象需要为该对象的引用计数加1,可以给该对象发送retain消息:

[myobj retain];

当不在需要该对象时,给其发送release消息使其引用计数减1:

[myobj release];

当对象引用计数为0时,理论上该对象不会再被使用,因为没有东西引用到它,他彻底沦为“宅对象”喽。所以可以释放其内存,通过给该对象发送dealloc消息完成这个操作。大多数情况下对象会继承NSObject的dealloc方法,当然如果类覆写了dealloc方法完成自己的析构,则当仁不让的会调用新的方法。

    在手工管理引用计数时,要注意F库中一些方法会隐式的增加对象的引用计数,同样一些方法会隐式减少引用计数。

  引用计数为0的引用称为悬挂指针(dangling pointer)的引用。如果给已释放的对象发送release消息,则会引起过度释放对象,会导致程序崩溃。

2.1自动释放池:设想下这种情况:方法返回一个在其中alloc的对象。这时虽然该方法不再使用这个对象,但不能释放它,因为该对象会作为该方法的返回值。NSAutoreleasePool类创建的目的就是希望能解决这个问题。通过给自动释放池发送drain消息,自动释放池中的对象会被清理和释放。

    通过给对象发送autorelease消息,可以将其手动添加到自动释放池维护的对象列表中:[my_obj autorelease];

并不是所有新创建的对象都会被自动添加到自动释放池中,任何以alloc,copy,mutableCopy和new为前缀的方法创建的对象都不会被自动添加到池中。在这种情况下,我们说你拥有这个对象。当你拥有一个对象时,你必须自己负责这个对象,so你需要在使用完后自己释放这些对象的内存:主动给对象发送release消息或发送autorelease将其加入到自动释放池中,下面上代码:

#import <Foundation/Foundation.h>

int main(int argc,char *argv[])
{
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	NSString *str0 = [[NSString alloc] init];

	//alloc方法不会自动加到池中,所以要手动释放
	[str0 release];

	[pool drain];
	return 0;
}

此代码表明在代码开始位置创建自动释放池,在进程返回前,代码结束位置清理自动释放池。值得注意的是以上代码编译时不可以加 -fobjc-arc选项,也就是不能与ARC机制同时使用哦,否则编译会出错的:

apple@kissAir: objc_src$clang -fobjc-arc -framework Foundation 5.m -o 5

5.m:5:2: error: 'NSAutoreleasePool' is unavailable: not available in automatic

      reference counting mode

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

        ^

/System/Library/Frameworks/Foundation.framework/Headers/NSAutoreleasePool.h:8:12:note: 

      declaration has been explicitly marked unavailable here

@interface NSAutoreleasePool : NSObject {

           ^


在遇到将一个对象作为方法的返回时,我们可以用明确的代码来表示将其放入自动释放池中,确保当起引用计数为0时且pool收到drain消息时能够自动释放:

//in some method:

Some_class *ret = [[Some_class alloc] init] autorelease];

或者在实际返回时再加入:

return [ret autorelease];

2.2 事件循环和内存分配:Cocoa和ios进程运行在事件循环中,类似于windows的事件循环,比如按下一个按钮事件。每当一个新事件发生时,系统会创建一个新的自动释放池,然后可能会调用你代码中的一些方法来处理该事件。当处理完事件,并从你的方法返回后,系统在等待下一个事件发生的间隙,会清理自动释放池哦。这意味着除非对象使用retain否则无法在清空自动释放池的过程中存活下来啦。下面看一个例子:

#import <UIKit/UIKit.h>
@interface myView:UIView
	@property (nonatomic,retain)NSMutableArray *data;
@end

假设在实现内部已经用synthesize对data属性进行了同步,再假定viewDidLoad方法会在视图载入内存是被系统调用,则在该方法中有:

data = [NSMutableArray array];

可是这样有个问题,就是前面提到的F库得方法会默认创建自动释放的对象,array创建的是一个自动释放的数组,该数组会在当前事件结束后被立即释放。为了保证数组在事件循环中还能存在,可以使用下面3中方法中的任何一种:

1. data = [[NSMutableArray array] retain]; //用retain后引用不会为0,所以不会被释放掉哦

2. data = [[NSMutableArray alloc] init]; //前面说过的alloc开头的方法不会加入自动释放池中哦

3. self.data = [NSMutableArray array];

注意最后一种方法,没有直接使用实例变量data,而是通过data属性赋值的,而data属性前面使用了retain特性,所以自动释放的数组会被保持。但是不管用3种方法的哪一种,都需要覆盖dealloc方法用于在实际销毁myView对象时释放数组哦:

-(void)dealloc{
	[data release];
	[super dealloc];
}

还是要记得调用父类中的dealloc啊!在手工引用计数环境中,可以为属性添加atomic(默认)或nonatomic特性,也可以添加assign(默认),retain和copy特性。当使用设置方法为属性赋值时我们来看看assign,retain和copy 3种特性实现上的不同:

self.property = new_value;
//assign way:
property = new_value;

//retain way:
if(property != new_value){
	[property release];
	property = [new_value retain];
}

//copy way:
if(property != new_value){
	[property release];
	property = [new_value copy];
}
最后对手工内存管理(即不使用垃圾回收或ARC特性)规则做一个总结:

1 如果需要保持一个对象不被销毁,可以使用retain,但在使用完对象后需要手动release进行释放;

2 给对象发送release不一定会销毁该对象,除非该对象的引用计数等于0,这时系统会发生dealloc消息给该对象;

3 对于使用了retain,copy,mutableCopy,alloc或new方法的任何对象,以及具有retain和copy特性的属性进行释放时,需要覆写dealloc方法;

4 如果在方法中返回一个对象(该方法不需要该对象),则可以给该对象发送autorelease消息标记这个对象延迟释放,autorelease消息不会影响到对象的引用计数;

5 当进程终止时,该进程内存中的所有对象都会被释放(貌似是废话);

6 当开发cocoa或iOS程序时,自动释放池会随着每次事件发送而创建和清空,在此情况下,如果要使自动释放池被清空后自动释放的对象还能够存在,对象需要使用retain方法。只要对象的引用计数大于发送autorelease消息的数量,就能够在池清理后生存下来。


【3】ARC

    自动引用计数ARC可以避免收工引用计数的一些潜在陷阱,但原来的引用计数仍然被维护和跟踪。然而系统会检测出何时需要保持对象,合适需要释放对象,这些你都不用担心鸟。你也不必担心返回了方法内创建的对象,编译器会管理好对戏的内存,编译器会通过生成正确的代码去自动释放或保持返回的对象(对于其他对象也是类似)。我们首先要引出强变量和弱变量2个概念:

强变量:通常在ARC中所有指针变量都是强变量。将对象引用obj_new赋值给obj_old会使obj_new对象自动保持,同时旧对象obj_old会在被赋值千被释放。强变量默认会被初始化为0,无论他是实例变量、局部变量还是全局变量这都成立。我们看以下代码:

Some_class *obj0 = [[Some_class alloc] init];
Some_class *obj1 = [[Some_class alloc] init];

obj0 = obj1;

当手工管理内存时上述代码会导致obj0对象的引用丢失,随后他的值被覆盖,从而产生内存泄露,即一个变量不再被引用,但又不能够释放。如果使用ARC,obj0和obj1都是强变量,前面赋值其实会是这样:

[obj1 retain];
[obj0 release];
obj0 = obj1;
但你不会在代码中实际看到上述代码,编译器在后台帮你做了这些,你只要写赋值语句就行啦。

因为所有对象默认都是强变量,所以不需要先声明,但你仍然可以使用关键字__strong:

__strong Some_class *obj;

但是默认属性不是strong,而是unsafe_unretained(相当于assign),所以如果有必要你需要这样为属性声明strong特性:

@property(strong,nonatomic) Some_class *obj;

编译器会保证事件循环中通过强属性对赋值执行保持操作,从而属性对象可以存活下来。带有unsafe_unretained(相当于assign)或weak的属性不会执行这些操作。

弱变量:在一些情况下,2个对象都持有彼此的强引用时,会产生循环保持(retain cycle),这样2个对象都不可以被销毁,即使其中一个已经不再被使用了。解决这个问题可以通过在2个对象间建立弱引用的方法解决。我们可以在父对象到子对象间使用强引用,而子对象到父对象间使用弱引用,这样就没有循环保持,弱变量也不能阻止引用的对象被销毁,在这里即是子对象无法阻止父对象被销毁。此时子对象会被系统自动设置为nil,这也避免了无意给它发送消息引起崩溃的问题。因为给nil对象发送消息不会有反应哦。

    可以使用__weak关键字声明一个弱变量:

__weak Some_class *obj;

或者为属性指定weak特性:

@property(weak,nonatomic) Some_class *obj;

需要注意的是,在ios4和os x 10.6中不支持弱变量,此时你仍然可以为属性使用unsafe_unretained(或assign)特性,或者将变量声明为__unsafe_unretained,然而这时当引用的对象被销毁时,变量不再被清零喽(即被置为nil)。

细心地看客或许已经注意到,本系列到目前为止几乎所有代码都会在main中有@autoreleasepool指令,该指令围住的语句块定义了自动释放池的上下文,在自动释放池块结束的时候,任何在这个上下文中创建的对象都会被自动销毁。(除非编译器在自动释放块结束后还需要保证这个对象存在)我们可以在产生大量临时对象的地方使用这一机制:

for(i = 0;i < n;++i){
  @autoreleasepool{
    //处理大量临时变量
  }
}

在本文开始处提到,cocoa和ios应用运行在事件循环中,为了处理新的事件,系统会创建一个新的自动释放池上下文,在事件结束的时候,自动释放池上下文已经结束,意味着自动释放对象可能被销毁。使用ARC,这些都会在“底层”发生,你无需为此担心。

    ARC很好,但其与ARC之前的代码兼容性如何呢?我们说只要非ARC代码与标准的cocoa命名规则一致,都会运行良好。当ARC遇到方法调用时会检查方法名,如果名字以alloc、new、copy、mutableCopy或init开头时,它会假定这些方法返回对象的所有者给方法的调用者。(即意思为这些返回的对象需要自己手动释放?ARC不会自动释放这些对象?)除非你使用的方法不符合标准的命名规则,此时,需要使用隐性通知编译器该方法会返回对象的拥有者(以便让编译器不会自动释放返回的方法?)。最后要注意的的是,如果你试图合成属性,而属性的名字是以上面提到的特殊词开头的话,编译器会提示一些错误哦。

    


相关文章
|
1月前
|
存储 Linux 编译器
Linux C/C++ 编程 内存管理之道:探寻编程世界中的思维乐趣
Linux C/C++ 编程 内存管理之道:探寻编程世界中的思维乐趣
50 0
|
18天前
|
缓存 安全 Java
Java并发编程进阶:深入理解Java内存模型
【4月更文挑战第6天】Java内存模型(JMM)是多线程编程的关键,定义了线程间共享变量读写的规则,确保数据一致性和可见性。主要包括原子性、可见性和有序性三大特性。Happens-Before原则规定操作顺序,内存屏障和锁则保障这些原则的实施。理解JMM和相关机制对于编写线程安全、高性能的Java并发程序至关重要。
|
1月前
|
存储 编解码 Linux
深入解析Linux C/C++ 编程中的内存泄漏问题
深入解析Linux C/C++ 编程中的内存泄漏问题
112 1
|
1月前
|
消息中间件 Linux
Linux进程间通信(IPC)教程 Linux共享内存介绍:介绍POSIX共享内存的基本概念、用途和编程实践
Linux进程间通信(IPC)教程 Linux共享内存介绍:介绍POSIX共享内存的基本概念、用途和编程实践
22 2
|
2月前
|
存储 编译器 程序员
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
106 0
|
2月前
|
程序员 编译器 C++
C++核心编程一:内存分区模型(持续更新)
C++核心编程一:内存分区模型(持续更新)
|
2月前
|
存储 缓存 并行计算
DP读书:鲲鹏处理器 架构与编程(四)内存顺序模型与内存屏障
DP读书:鲲鹏处理器 架构与编程(四)内存顺序模型与内存屏障
36 1
|
6月前
|
存储 缓存 分布式数据库
[笔记]Windows核心编程《十七》内存映射文件(二)
[笔记]Windows核心编程《十七》内存映射文件(二)
|
6月前
|
缓存 Java 编译器
[笔记]Windows核心编程《十七》内存映射文件(一)
[笔记]Windows核心编程《十七》内存映射文件
|
6月前
|
存储 Windows 内存技术
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存

热门文章

最新文章