OC内存管理教程之ARC(一)——objective-c内存管理和引用计数

前言:

当前的分类是《objective-c高级编程——ios与os x多线程和内存管理》这本书的读书笔记。
这本书分为了三章,第一讲了自动引用计数(ARC),第二讲了Blocks,第三讲了多线程。所以也准备以这样的分类记录一下自己的收获。写博客也是因为觉得这部分内容挺好的,毕竟输出是更加痛苦的输入,这样可以加深自己的理解和记忆,同时,也是想写一份比较好理解且相对全面的OC下关于内存管理的教程。
现在读完了第一章,也在网上看了很多相关博客,说实话我觉得书上这一章写得并不算好,刚看第一眼跟自己原有的理解感觉好像并不相符一样,各种云里雾里。其实就是描述得不是很好的原因,首当其冲就是第一章关于持有的概念就很模糊,什么叫只能释放自己持有的对象之类的。同时,有一些内容已经不适合当前的开发环境,没有以前开发的经验是没办法一下子理解它里面写的。O__O "…所以,这系列的博客我都会用自己所理解的语言表达出来,整合介绍以前开发的代码写法,以及结合现在开发中一些形成的习惯写法,讲述其背后所蕴含的内存管理的原理。可能有错误,也希望有人如果发现问题可以及时指正不胜感激。

引用计数:

什么是引用计数呢?不管是ARC模式(Automatic Reference Counting自动引用计数,可在xcode中设置是否使用)还是非ARC模式,在objective-c中关于对象的内存管理都采用引用计数的方式。每个创建的对象,它都有一个变量指示着当前这个对象被多少个变量引用。当这个对象被引用的计数为0时,编译器就会把这个对象从内存抹去(把它占用的空间全置为0)。


书中指出,在GNUstep(它是Cocoa的互换框架,因为Foundation框架中NSObject的源代码不公开,而GNUstep是公开的,而且设计思路和Cocoa非常相似,可从这个框架的源码来理解苹果Cocoa的实现)中,从NSObject的alloc方法可以看出:

struct obj_layout {
    NSUInteger retained;
};

+(id)alloc
{
    int size = sizeof(struct obj_layout) + 对象大小;
    struct obj_layout *p = (struct obj_layout *)calloc(1,size);
    return (id)(p+1);
}
alloc类方法用struct obj_layout中的retained整数来保存引用计数,并将其写入内存头部。而且由此也可以看出,OC的对象的内存空间开辟时调用的是calloc函数,对象的内存是在堆中开辟的。可以用下图来战术有关 GNUstep中,alloc类方法返回的对象:


但是,Foundation的实现和GNUstep中有些出入,它用了一个散列表管理各个对象的引用计数:


不管是在内存头部插入结构体,还是使用散列表,都各有各的好处,总之记住一点,对于NSObject或者继承于NSObject的对象,只要没有自己重写各种生成对象的方法,在内存中生成对象的时候,都会有一个变量,专门记录当前对象被引用的个数。而内存管理的本质就是:这个变量一旦为0,编译器就会把这个对象所在的内存空间清0舍弃。


(从这里开始所介绍的只适用于非ARC模式下,即以前的开发模式。如果需要实验,需要把xcode设为非ARC模式,具体方法可以百度)

对象操作的方法

关于对象的操作方法,有以下几种(属于自己的总结,和书上有点区别)

对象操作objective-c方法
生成对象,并使引用计数置1alloc/new/copy/mutableCopy等方法
引用计数+1retain方法
引用计数-1release方法
废弃对象,清空对象所在的内存空间dealloc方法

注意retain方法和release必须成对出现,但调用的次数不限。

生成对象的方法

有了以上的概念,就可以向对象的生成进击了。NSObject中,生成对象的方法大致分为两类,以alloc/new/copy/mutableCopy名称开头;而另一类以对象名称开头,如NSArray中的array方法,也可以概括为类方法。这两类的方法区别在于:

(1)alloc/new/copy/mutableCopy等方法:

假如当前有个person类:

Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject{

    NSString *_name;
    NSString *_age;

}

-(void)setName:(NSString *)name;
-(void)setAge:(NSString *)age;


Person.m

#import "Person.h"

@implementation Person

-(void)setName:(NSString *)name
{
    _name = name;
}
-(void)setAge:(NSString *)age
{
    _age = age;
}

//重写dealloc方法,可以查看编译器什么时候舍弃对象
-(void)dealloc
{
    [super dealloc];
    NSLog(@"person dealloc");
}

@end

当使用如下方法进行生成对象并且初始化时:

Person *p1 = [[Person alloc] init];
由于没有重写alloc和init方法,于是会调用NSObject的alloc和init方法。会在堆区开辟一块内存空间存放Person对象,并在栈区创建一个指针p1,它指向堆区的那个Person对象。并且Person对象的引用计数记为1,可以理解为p1由此持有了Person对象,就不需要再写[p1 retain]这样的语句,当使用结束后直接调用[p1 release]就可以为其引用计数减1,如果此时Person对象的引用计数为0时,编译器就会回收Person的内存空间。

#import <Cocoa/Cocoa.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    Person *p1 = [[Person alloc] init];
    NSLog(@"Person retaincount: %ld", (unsigned long)[p1 retainCount]);//结果是1
    
    
    Person *p2 = p1;
    NSLog(@"Person retaincount: %ld", (unsigned long)[p2 retainCount]);//结果还是1
    
    //用p2指向了p1所指向的对象,但却不为它的引用计数+1 这是危险且不合规范的
    //编译器虽然看到p2也指向了这个Person对象,但并不会自动为其引用计数+1,需要程序员手动为其引用计数+1
    [p2 retain];
    NSLog(@"Person retaincount: %ld", (unsigned long)[p2 retainCount]);//结果为2

    //既然有了[p2 retain],那么p2用完之后也要有[p2 release],retain和release方法需要成对出现。
    [p2 release];
    //此时,仍然可以通过p2指针使用到Person对象,但这是不合规范且危险的
    NSLog(@"Person p2:%@", p2);
    
    [p1 release];
    //到这里,可以看到重写的dealloc方法被调用了,因此Person对象已经被废弃了
    //此时如果再访问Person对象就会报错
    //NSLog(@"Person retaincount: %ld", (unsigned long)[p2 retainCount]);
    //所以这时候p1和p2都成为野指针了,这个问题会在ARC中得到解决。也可以手动设置p1=nil,p2=nil解决
    return 0;
}

由此,其实可以理解retain和release方法了,它们只是让引用计数+1 和 -1 ,并不会消除指针的指向效果。而且编译器在非ARC模式下并不会自动为引用计数+1和-1。因此在早期开发的时候,程序员必须时时刻刻关注引用计数的问题,并且需要的时候手动调用retain和release方法。只要使用就retain,只要用完就release,这样才是规范且安全的。一旦没有及时release,在超出指针的作用域后,就容易出问题。例如上面的return语句之后,p1和p2指针由于是栈区,超出作用域后就回收,当没有及时release的话,Person对象的引用计数不为0,编译器不回收它的内存,就造成内存泄漏了。

同时,从上面的例子可以反映出一个问题,当Person对象被废弃时,在return语句之前,p1指针和p2指针其实还在生命周期内,它们却指向了一块内容未知的内存区域。所以p1和p2成了野指针,容易引发不安全的问题。有经验的程序员此时都会给p1和p2赋值nil。


(2)类方法:

这类方法以对象名称开头,如NSString的string方法,NSArray的array方法……按照书上讲的话,意思是这样子生成的对象,自己不持有,如果需要持有的话需要再次调用retain方法。不过由于NSString和NSArray是常量,编译器不对它使用引用计数。而我在使用NSMutableString在xcdoe 8.3.2试验的时候,不管retain多少次,引用计数都为1。很多说法就是retaincount获取的数据不准确。加上苹果框架并不开源,不能很清楚的看到它的内存机制。因此,对于这类型的方法,只要记住两点,要么创建完后retain,使用完后要release(这样可以保证你想要的作用域),要么拿来就用,不需要手动retain或者release,这样的作用域就是nsrunloop

    NSMutableString* s1 = [NSMutableString stringWithFormat:@"abc%d",123];
    [s1 retain];
    //使用过程
    [s1 release];

autorelease和NSAutoreleasePool

看名字可以理解为自动释放和自动释放池,看着很像ARC,其实不是,它实际上 更类似与C语言中自动变量的特性。而c语言的自动变量特性可以理解为:

{
        int a;
        //在当前作用域里a都可以使用
    }
        /*
         这里超出了a的作用于
         自动变量a被废弃,不再可以访问
         */
autorelease和NSAutoreleasePool的使用可以总结为:

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    
    //obj的使用过程
    
    [pool drain];
对obj调用autorelease方法就是把obj指向的对象注册到自动释放池pool中。此时,程序员不再需要手动调用[obj release]方法,在调用[pool drain]时,pool会对它里面的所有对象统一调用它们的release方法。由此可以看出,创建pool一直到pool被drain这个范围内就是obj的作用域。 所以一句话理解就是,autorelease就是把指向的对象放进autoreleasepool,drain把autoreleasepool里的所有对象引用计数-1。这时候同样,哪个对象引用计数减为0了,废弃,还不为0的,继续保留。

那为什么要有autorelease呢?其实只要掌握了retain和release不是也能应付吗?其实autorelease和NSAutoreleasePool的使用场景在于,当我们有多个对象的时候,使用它们就可以免于管理多个对象的麻烦,同时避免频繁申请/释放内存也有利于执行效率的提高。

但在autolease使用的过程中还是有问题需要注意的。如果大量产生autorelease对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能为释放,因此有时候会产生内存不足的现象。典型例子就是读入大量图像同时改变尺寸。图像文件读入到NSData对象,并从中生成UIImage对象,改变该对象后生成新的UIImage对象。这种情况下,就会大量产生autorelease对象。

    for(int i =0; i< 图像数; i++)
    {
        /*
         读入图像
         大量产生autorelease对象
         由于没有废弃NSAutoreleasePool对象
         最终导致内存不足
         */
    }

在此情况下,有必要在适当的地方生成、持有或者废弃NSAutoreleasePool。


另外,Cocoa框架中也有很多类方法用于返回autorelease对象。比如NSMutableArray类的arrayWithCapacity类方法。

id array = [NSMutableArray arrayWithCapacity:1];
这个方法等同于下面的调用:

id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

结语:

到这里,书上第一章关于非ARC下面的内存管理基本介绍完毕了,对于那些retain、release等函数的实现,书上也给了一些参考的源码。但这里就不细究了,主要还是以应用为主导,我认为一般情况下,理解它们背后的原理更重要。系列的第二篇,将真正介绍ARC了,有了这篇博客的基础,相信看第二篇会理解得更好。

2017.5.23补充:

autorelease和函数返回值

这天看了一下autorelease和函数返回值方面的内容,更加清楚了上面说的两种生成对象的方法的区别。也更加理解了autorelease的作用。这里记录一下。同样,本篇只记录关于MRC下这方面的内容,对于autorelease和函数返回值在ARC下的体现,留在下一篇介绍。

如果一个函数的返回值是指向一个对象的指针,那么这个对象肯定不能在函数返回之前进行 release(引用计数-1),这样调用者在调用这个函数时得到的就有可能是野指针了,在函数返回之后也不能立刻就release(引用计数-1),因为我们不知道调用者是不是retain(引用计数+1)了这个对象,如果我们直接 release 了,可能导致后面在使用这个对象时它已经成为 nil 了。
为了解决这个纠结的问题, Objective-C 中对对象指针的返回值进行了区分,一种叫做 retained return value ,另一种叫做 unretained return value 。前者表示调用者拥有这个返回值,后者表示调用者不拥有这个返回值,按照“谁拥有谁释放”的原则,对于前者调用者是要负责释放的,对于后者就不需要了。
按照苹果的命名 convention,以 alloc , copy , init , mutableCopy 和 new 这些方法打头的方法,返回的都是 retained return value,例如 [[NSString alloc] initWithFormat:] ,而其他的则是 unretained return value,例如 [NSString stringWithFormat:] 。我们在编写代码时也应该遵守这个 convention。


在 MRC 中我们需要关注这两种函数返回类型的区别,否则可能会导致内存泄露。

对于 retained return value(alloc/new/copy/mutableCopy方法的返回值),需要负责释放 

假设我们有一个 property 定义如下:

@property (nonatomic, retain) NSObject *property;
在对其赋值的时候,我们应该使用:

self.property = [[[NSObject alloc] init] autorelease]; //对象会在runloop结束时被自动释放一次(引用计数-1)
然后在 dealloc 方法中加入:

[_property release];
_property = nil;
这样内存的情况大体是这样的:

  • init 把引用计数增加到 1
  • 赋值给 self.property ,把引用计数增加到 2
  • 当 runloop circle 结束时,autorelease pool 执行 drain,把引用计数减为 1
  • 当整个对象执行 dealloc 时, release 把引用计数减为 0,对象被释放
可以看到没有内存泄露发生。

如果我们只是使用:
self.property = [[NSObject alloc] init];
这一条语句会导致引用计数增加到 2,而我们少执行了一次 release,就会导致引用计数不能被减为 0 。
当然也可以通过手动执行两次release来解决,但这样的代码是很奇怪的,可读性也很不好。

另外,我们也可以使用临时变量:
NSObject * a = [[NSObject alloc] init];
self.property = a;
[a release];
这种情况,因为对 a 执行了一次 release,所有不会出现上面那种引用计数不能减为 0 的情况。

注意:现在大家基本都是 ARC 写的比较多,会忽略这一点,但是根据上面的内容,我们看到在 MRC 中直接对 self.proprety 赋值和先赋给临时变量,再赋值给 self.property,确实是有区别的!当然,如果property中没有retain属性,就没有这么麻烦在delloc里加上再释放一次的代码了。

我们在编写自己的代码时,也应该遵守上面的原则,同样是使用 autorelease:

// 注意函数名的区别
+ (MyCustomClass *) myCustomClass
{
return [[[MyCustomClass alloc] init] autorelease]; // 注册到autoreleasepool,调用者可以不考虑释放问题(会自动释放)
}


- (MyCustomClass *) initWithName:(NSString *) name

{
return [[MyCustomClass alloc] init]; // 不注册到autoreleasepool,使用者需要注意释放
}


对于 unretained return value(类方法的返回值),不需要负责释放

当我们调用非 alloc,init 系的方法来初始化对象时(通常是工厂方法),我们不需要负责变量的释放,可以当成普通的临时变量来使用:

NSString *name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
self.name = name
// 不需要执行 [name release],这类方法的返回值一般已经注册到autoreleasepoo了,调用者不需要自己执行release操作

这里补充一个说明,虽然我们使用unretained return value可以不考虑release,拿来就用。但还是不建议这样做,最好还是先retain一下,用完再老老实实release,因为这样,使用者才能完完全全掌握这个对象的生命周期。不然,作为放到autoreleasepool的对象,编译器可能在某个时候为它release了一下,而此时它引用计数为0了,被编译器废弃了。假如你之后还需要再用到它,就找不到这个对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值