iOS成长之路基础篇 – (4) OC 内存管理MRC和ARC

iOS成长之路基础篇 – (4) OC 内存管理MRC和ARC

最近一直在坚持写博客,这些学习笔记主要是自己以前学习OC的过程中记下的笔记,现在整理的过程中就想,把这个写成博客。大牛同志写博客一年好几百篇,我的几年了才30篇不到,心里也鄙视了自己一把。最近却写的停不下来了,准备把这个系列的博客写完,一般两天基本出一篇,虽然比较基础,但是一篇下来大概也是需要两个小时左右整理。现在才知道写文章的人有多么辛苦。后面也希望自己能一直坚持写下去。

今天我总结的是Objective-C的手动内存管理和现在Apple已经全面支持的自动引用计数(自动内存管理)。好,下面我们进入正题。

准备工作

刚才说到,现在的iOS项目已经全面支持ARC,而且,Apple一向的强制原则就是,我推荐的,你就只能按着我的来。所以我们现在在Xcode7上建立的新项目,已经看不到原来的可以勾选的ARC选项,换句话说就是,我们现在只能开发ARC的项目,像Apple的新语言Swift根本就不支持MRC,那么是不是说我们已经彻底不能看到MRC的项目了?答案是NO。那么如何建立一个MRC项目?

首先,我们按着一般建立项目的步骤建立一个新项目,然后查看项目是否为ARC项目:

      TARGETS—项目名称——Build Settings—-Objective-C A.. R.. C..YES

如果我们将YES修改为NO,那么项目就会变成MRC项目,这时候我们就要自己管理引用计数了。

还有就是,我们以前的项目可能是MRC,现在,MRC已经一去不复返,我们想将我们的项目转换成ARC项目,这个怎么办?难道一个文件一个文件修改?答案也是NO。Xcode提供一键将非ARC项目改为ARC项目:

      XcodeEdit—>Refactor—->Convert to Objective-C ARC…

那么,如果我们需要在我们的项目中引入第三方框架,而我们的项目是ARC,第三方框架因为太老而使用的MRC,这时候我们应该怎么办?答案是,Xcode提供对项目中的不同文件使用不同的编译策略。就是我们常说的MRC和ARC混合编程。这里使用到一个编译指令。项目中单文件ARC或者MRC设置位置:

      TARGETS—项目名称——Build Phases—-Compile Sources

ARC项目不需要ARC的文件设置参数:-fno-objc-arc,如下图(图片来自网络);

no-objc-arc

对非ARC项目需要ARC的文件设置参数:-fobjc-arc。如下图(图片来自网络):

objc-arc

接下来,我们首先总结一下手动引用计数(MRC)的知识。

MRC内存管理 {.p1}

我们说的引用计数管理,其实就是对对象的内存管理。那么我们的内存管理的对象是哪些呢?任何继承了NSObject的对象。在OC中,每一个OC对象都有自己的引用计数器,是一个整数,对象被引用的次数。存在于对象的内部,占四个字节

当使用alloc,new和copy创建一个新对象时,新对象的引用计数就默认为1。当引用计数为0,就回收内存,对象被销毁。

  1. 给对象发送一条retain消息,引用计数+1。
  2. 给对象发送一条release消息,引用计数-1。
  3. 可以通过retainCount方法获取引用计数次数。

当对象引用计数为0时,对象被销毁时,系统发送一条dealloc消息。

一般开发中我们将重写dealloc方法,释放相关资源。同时一旦调用了dealloc方法,就必须在最后调用[super dealloc];

注意:

release为0的对象称为僵尸对象,指向僵尸对象的指针成为野指针。

一个对象引用计数为0后不能再调用release方法。会出现野指针错误(EXC_BAD_ACCESS)。OC中没有空指针错误,一个空指针调用任何方法,都会被忽略,不会出错。

retain方法有返回值,返回其对象本身。

在OC中,局部变量和动态产生的对象在内存中是这样的。局部对象存储在栈中,动态产生的对象,首先在堆中申请内存,然后初始化。蓝色的线,相当于指针指向OC对象。此时的堆对象中会有一个引用计数器,它的数值为1。

stackdeam

下面是一段MRC代码:

#import "People.h"

int main()
{
    People *p = [[People alloc] init];    //假设有一个People对象,创建对象,
                                          //引用计数+1,变为1

    [p release]; //在使用完p对象之后要release一次,引用计数-1,变为0
    return 0;
}
  • MRC内存原则

1>需要引用时,+1;不需要引用了,-1;

2>谁创建,谁release;

3>谁retain,谁release;

4>只要调用了alloc,必须有release(autorelease)

5>成员变量的set方法中需要注意的:基本数据不用管,对象数据需要注意。

//虽然@property可以省略setter和getter方法,但是在MRC中,成员变量如果是对象
//我们应该在setter方法中将原来的对象进行一次release
//将新赋值的对象进行一次retain
- (void)setBook: (Book)book        
{
    if(_book != book)           //如果原来的_book和新赋值的不是同一对象,就需要更新引用计数
    {
        [_book release];       //如果第一次赋值,_book为nil,此句代码将没有人和执行效果
        _book = [_book retain];//新的对象计数+1
    }
}

上面的代码如果没有学习过OC的可能看不懂,建议在看书系统学习一下。

当然,上面的setter方法中,成员变量引用计数+1,那么我们是不是在释放People对象时,将_book变量也销毁了,此时应该将_book所指的对象引用计数-1。那么在dealloc方法中需要注意对成员对象计数-1。

- (void)dealloc
{
    [_book release];

    [super release];
}
  • @property的参数

上面我们讲到@property可以将基本数据类型的成员变量的getter和setter方法生成,我们不需要人工干预,但是对于对象类型的成员变量,我们必须自己写setter方法,而且对于一个对象类型的变量,代码几乎一样,此时,Apple给@property有添加了一些编译器特性。就是@property的参数。

@property(retain) Book *book; //retain:生成set代码中的release旧值,retain新值

//不能代替dealloc方法,还是需要在dealloc中release对象

其实@property的参数还有很多,主要分为四类:

1、内存管理

  • retain : release旧值,retain新值
  • assign :直接复制(缺省值,适用于非OC对象类型)
  • copy : release旧值,copy新值

2、读写属性,是否要生成set方法

  • readaonly :只读,只生成getter的声明和实现
  • readwrite :可读可写,生成setter和getter的生命和实现(缺省值)

3、多线程管理

  • nonatomic :性能高,不加锁,线程不安全(建议使用,开发常写)
  • atomic:性能低,加锁,线程安全(缺省值)

4、setter和getter方法的名称

  • @property (setter = setXxx: ,getter = xxx) int weight;
  • 一般使用在BOOL类型的成员变量 getter = isXxx

四种参数可以组合使用,只要不是冲突的参数,就可以组合,一般我们开发中的组合为:

// 对象类型
@property(nonatomic, retain) Book *book;
// 基本类型
@property(nonatomic, assign) int age;

// 如果设置读写属性,也可以加上
@property(nonatomic, assign, readonly) double weight;
  • @class

如果A文件头文件包含了B文件的头文件,同时B文件有包含了A文件的头文件,此时就会出现无法编译的情况,两个头文件循环导入,最后进入死循环。此时,我们就要用到@class关键字。

@class Car;

仅仅是告诉编译器这是一个类。能解决循环引用时不能import的问题。

一般的开发规范:

  1. 一般情况下,在.h文件中我们使用@class Xxx;
  2. 在.m文件中import “Car.h”,提高编译效率。
  • autorelease

autorelease关键字,字面意思好像是自动释放,其实它不是自动释放,它的正确理解是:半自动的。就是在自动释放池销毁的时候release一次

autorelease准确的说是一个函数,返回对象本身id。调用autorelease方法后,将对象放入自动释放池,对象计数不变,当自动释放池销毁时,池中的对象会自动调用release一次。

  • 自动释放池

ios程序运行过程中会创建无数多的池子,这些池子都是栈这种数据结构存在(后进先出),当一个对象调用autorelease方法时,会将这个对象放到栈顶的释放池。

自动释放池有两种创建方式。

// ios5以前的方式:
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];

//中间是自动释放池代码

[pool release];

//下面的方式是ios5.0以后的方式
@autoreleasepool {

    //创建自动释放池
    //使用方式:
    Person *p = [[[Person alloc] init] autorelease];

}   //销毁自动释放池,池中所有对象进行release一次。

 

自动释放池的一些特点:

  1. 优点:可以延迟对象释放时间,不用关心对象释放时间。
  2. 缺点:不能准确控制对象释放时间,占用内存较大的对象不要使用autorelease。

注意:

1.自动释放池在内存中的存放数据结构是栈,后进先出。同时自动释放池可以嵌套使用。

2.不能在调用autorelease之后再次调用release,其实autorelease就是在销毁自动释放池时,池中对象进行release一次。

3.不能多次调用autorelease方法,每一个自动释放池对应一个autorelease。

4.一般情况我们自己创建的对象放入栈顶的池子。

  • 开发中的技巧
  1. 调用系统中的对象,系统的对象实例化中没有alloc,new,copy等关键字,说明返回的对象都是autorelease,不需要关心它的内存管理。
  2. 可以在模型类中设计一些类方法,返回一个带有autorelease方法的对象。

注意:在设计自己的带有autorelease方法的对象的类方法时,方法中的类名使用self代替直接写类名。

ARC内存管理 {.p1}

ARC属于编译器特性。自动生成引用计数的代码,并插入到特定位置,不需要我们关心其中的细节,大大提高程序员的工作效率。那么它是如何判断的呢?

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

 

  • 指针分为两类分类:

 

A:强指针:默认情况下,所有的指针都是强指针, 可以使用关键字__strong 进行修饰一个变量。

B:弱指针:__weak 关键字修饰的变量。

在ARC中,弱指针指向的对象如果被销毁,此时弱指针值自动修改为nil

注意:

所有关于内存的代码releaseautoreleaseretainretainClount等不能调用。

允许重写dealloc方法,但是不能调用[super dealloc];

在ARC中,我们需要将@property的参数retain改为strong,意味着成员变量为强指针;

 此时,weak代替assign,相当于assign,意味着成员变量为弱指针。

循环引用 {.p1}

在内存管理中,我们要注意的就是循环引用,循环引用是两个对象循环强引用。此时,两个对象都不能release,就不会被释放。

我们俩可以将其中一个对象的@property参数设置为weak。

如何检测循环引用,其实也是一个开发中的技巧。已经有很多人介绍过循环引用这儿的技巧和解决方案。Apple也是将view中的控件都默认为weak弱引用。

总结 {.p2}

今天总结的内容相对较多,同时内存管理也是iOS开发中很重要的一节内容,在这篇文章中,我并没有展开写,只是总结归纳了一下。如果堆内存管理不是很清楚的同学,建议多在此话费一点时间。在面试中,绝对不会逃过内存管理不问。下一篇准备总结OC中的又一个特色——Block代码块。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注