可能是史上最全面的内存管理文章

Posted by Liu Peng on August 8, 2016

neicunguanli

iOS内存管理

概述

什么是内存管理

应用程序内存管理是在程序运行时分配内存(比如创建一个对象,会增加内存占用)与清除内存(比如销毁一个对象,会减少内存占用)的过程

为什么要管理内存

目前iPhone手机内存大多为1G,分配给每个应用程序的内存空间极其有限,当应用程序运行过程中所占用的内存较大时,便会收到系统给出的内存警告,如果应用程序所占用的内存超过限制时,便会被系统强制关闭,所以我们需要对应用程序进行内存管理,一个好的程序程序也应该尽可能少地占用内存

内存管理的方式

在OC中提供了两种管理内存的方式

  • MRR(Manual Retain-Release),也被人称作MRC(Manual Reference Counting,手动引用计数)
  • ARC(Automatic Reference Counting,自动引用计数)

注: 在Build Setting中的Objective-C Automatic Reference Counting设置为YES即为ARC

内存管理的范围

任何继承自NSObject的对象都需要管理内存(因存放在堆里面),基本数据类型int、float、double、char、结构体struct以及枚举enum都不需要管理内存(因存放在栈里面)

  • 堆: 一般由程序员分配释放内存,若程序员不释放,程序结束时可能由OS释放,其操作方式类似于数据结构中的链表
  • 栈: 由操作系统自动分配释放,存放函数的参数值,局部变量值等,其操作方式类似于数据结构中的栈(先进后出)
内存管理的规则

内存管理模型基于对象所有权机制,一个对象的所有权可以拥有一个或者多个拥有者,只要一个对象的所有权拥有至少一个拥有者,该对象就会一直存在于内存中.如果一个对象的所有权没有任何拥有者,那么系统便会自动地将该对象所占用的内存释放掉.为了明确何时拥有某个对象的所有权、何时放弃某个对象的所有权,系统设置了如下规则

  • 你拥有你创建的对象的所有权: 你拥有使用alloc,new,copy或者mutableCopy方法创建的新对象的所有权
  • 你可以通过retain来获取一个对象的所有权: 在两种情形中会使用到retain来获取一个对象的所有权
    • 在init方法和setter方法中,使用retain来获取一个想要保存为属性值的对象的所有权
    • 防止一个对象在一些操作中被设置为无效,具体参见”内存管理中需要注意的两个使用场景”
  • 你必须放弃你不再需要拥有其所有权的对象的所有权: 你必须通过release或者autorelease方法来放弃你不再需要拥有其所有权的对象的所有权,在Cocoa术语中”放弃一个对象的所有权”一般被称作”释放一个对象”
  • 你不能放弃一个你不拥有的对象的所有权: 这个规则是以上几点规则的推论

iOS内存管理之MRR&MRC

概述

在iOS5以前,程序员普遍使用MRR&MRC方式来管理内存,程序员需要自己添加retain、release和autorelease等内存管理代码来跟踪自己所拥有的对象以明确地管理对象的内存,在需要使用该对象的时候保证该对象一直存在于内存中,在不需要使用该对象的时候保证该对象所占用的内存被系统正常回收

为了让系统知道何时需要将某个对象所占用的内存清理掉,系统引入了引用计数器的概念

引用计数器

概述

系统为每个OC对象内部都分配了4个字节的存储空间存放自己的引用计数器,引用计数器是一个整数,表示“对象被引用的次数”,当对象的引用计数器为0时,对象所占用的内存空间就会被系统自动回收,当对象的引用计数器不为0时,在程序运行过程中所占用的内存会一直存在,直到整个程序退出时由OS自动释放

操作引用计数器
  • 当使用alloc、new、copy、mutableCopy创建一个新对象时,该新对象的引用计数器为1
  • 当给对象发送一条retain消息时,对象的引用计数器+1(方法返回对象本身)
  • 当给对象发送一条release消息时对象的引用计数器-1(方法无返回值)
  • 当给对象发送一条retainCount消息时,返回对象的当前引用计数器(不要以该数据来判断对象是否被释放)

注: 给对象发送一条release消息,不代表对象会被释放,只有对象的引用计数器为0时才会被释放

示例

下面通过一个小示例,来演示一下如何操作对象的引用计数器

Person *p = [[Person alloc] init]; // 使用alloc创建一个新对象,对象引用计数器 = 1
[p retain]; // 给对象发送一条retain消息,对象引用计数器 + 1 = 2
[p release]; // 给对象发送一条release消息,对象引用计数器 - 1 = 1
[p release]; // 给对象发送一条release消息,对象引用计数器 - 1 = 0,指针所指向的对象的内存被释放

僵尸对象、野指针与空指针

概述

下面介绍几个关于内存管理方面常常提到的术语

  • 僵尸对象: 所占用的内存已经被回收的对象,僵尸对象不能再使用
  • 野指针: 指向僵尸对象的指针,给野指针发送消息会报错EXC_BAD_ACCESS错误:访问了一块已经被回收的内存
  • 空指针: 没有指向任何对象的指针(存储的东西是nil,NULL,0),给空指针发送消息不会报错,系统什么也不会做,所以在对象被释放时将指针设置为nil可以避免野指针错误

注: 默认情况下,Xcode是不会监听僵尸对象的,所以需要我们自己手动开启,开启监听僵尸对象步骤为: Edit Scheme ->; Run ->; Diagnostics ->; Objective-C的Enable Zombie Objects打钩,这样便可以在因为僵尸对象报错的时候给出更多错误信息

示例
Person *p = [[Person alloc] init]; // 引用计数器 = 1
[p release]; // 引用计数器 - 1 = 0,指针所指向的对象的内存被释放
[p release]; // 这句给野指针发送消息,会报野指针错误,开启监听僵尸对象会给出错误信息**- -[Person release]: message sent to deallocated instance 0x100206fd0

注: 如果在第一次给对象发送release消息后,立刻将指针置空,便不会出现野指针错误,因为给空指针发送消息不会报错,系统什么也不会做,所以在对象被释放时将指针设置为nil可以避免野指针错误

单对象内存管理

概述

单对象的内存管理比较简单,当使用alloc、new、copy、mutableCopy创建一个新对象时,该新对象的引用计数器为1,我们只需要在对象不再使用的时候给其发送一次release消息,让对象的引用计数器-1变为0即可正常释放

示例

下面通过一个小示例,来演示一下如何对单对象进行内存管理

// 在堆中开辟一块内存存放Person对象,在栈中开辟一块内存存储局部变量p,p中存放的是Person对象的地址
// 当出了大括号,局部变量p出了作用域便会被系统自动回收,而Person对象引用计数器为1仍然存在,这样便造成内存的泄漏,所以我们需要在局部变量p被系统回收之前,给p所指向的Person对象发送一条release消息让对象引用计数器-1变为0
Person *p = [[Person alloc] init];
[p release];

多对象内存管理

概述

相对于单对象内存管理而言,多对象内存管理显得比较复杂,但是只要我们遵从基本的内存管理规则,即可避免大部分内存管理相关的问题

  • 当A对象想要拥有B对象的所有权时,一定要对B对象进行一次retain操作,这样才能保证A对象存在期间B对象一直存在
  • 当A对象想要放弃对B对象的所有权时,一定要对B对象进行一次release操作,这样才能保证A对象释放了,B对象也能正常释放
存取器方法

在涉及到多对象内存管理时,平时我们使用的存取器方法便需要针对内存管理进行一些调整

  • getter方法规范: 直接返回实例变量
// 针对基本数据类型
- (int)age
{
    return _age;
}

// 针对OC对象
- (Room *)room
{
    return _room;
}
  • setter方法规范:
// 针对基本数据类型,直接将新值赋值给实例变量
- (void)setAge:(int)age
{
    _age = age;
}

// 针对OC对象,方式一: 判断两次传入的对象是否相同,如果不同便release旧对象retain新对象,并将新对象赋值给实例变量
- (void)setRoom:(Room *)room
{
    if (_room != room)
    {
        [_room release];
        _room = [room retain];
    }
}

// 针对OC对象,方式二: retain新对象,release旧对象,并将新对象赋值给实例变量
- (void)setRoom:(Room *)room
{
    [room retain];
    [_room release];
    _room = room;
}

注1: release旧对象retain新对象原因: 避免设置两次不同的room,在Person对象释放时,第一次设置的对象无法被释放,造成内存泄露

注2: 判断当前对象与传入对象是否相同原因: 避免设置两次相同的room,在设置第二次时,将room已经释放了,再调用retain造成野指针错误

dealloc方法

当系统回收对象的内存时,系统会自动给该对象发送一条dealloc消息,我们一般会重写dealloc方法,在这里给当前对象所拥有的资源(包括实例变量)发送一条release消息(基本数据类型不用),保证自身所拥有的资源也可以正常释放(因为在使用该资源的时候,采用retain获取了该资源的所有权,在自身释放的同时,也应该放弃对该资源的所有权)

- (void)dealloc
{
    NSLog(@"Person dealloc");

    // release对象所拥有资源
    [_room release];
    // 设置为nil可以避免野指针错误(其实可以不设置,只是写了显得有逼格)
    _room = nil;

    [super dealloc];
}

注1: 不要直接调用对象的dealloc方法

注2: 重写dealloc方法时,一定要调用[super dealloc]方法,且放在代码的最后

注3: 当应用程序被关掉,dealloc方法不一定会被调用,因为由系统OS直接来释放内存比调用dealloc释放内存效率得多

注4: 不要在dealloc方法中管理稀缺资源(比如网络连接,文件资源,DOS命令等),因为dealloc并不一定都是立即调用,有可能会延迟调用,也可能根本不会被调用

属性@property

概述

属性可以帮助我们免除重复地写存取器代码的麻烦,并且合理使用属性修饰符也可以帮助我们生成符合内存管理规范的代码

两个编译器指令: @property和@synthesize

在Xcode4.4之前

  • @property用于替代setter和getter方法的声明
@property double weight;

- (void)setWeight:(double)weight;
- (double)weight;
  • @synthesize用于替代setter和getter方法的实现
@synthesize weight = _weight;

- (void)setWeight:(double)weight
{
    _weight = weight;
}

- (double)weight
{
    return _weight;
}

注1: 在@synthesize后面告诉编译器,将要实现哪个@property的实现,在等号后边告诉编译器,将setter传进来的值赋值给谁和getter返回谁的值给调用者

注2: 在@synthesize中如果不写等号后面的,系统会默认将setter传进来的值赋值给与@synthesize后面的变量同名的成员变量,且getter返回与@synthesize后面的变量同名的成员变量的值给调用者,即weight而不是_weight

在Xcode4.4之后

  • @property可以直接替代setter和getter方法的声明和实现,同时会生成以下划线开头的”私有”成员变量(即在.m文件@implementation下面{}中生成的,在其他类中不能直接访问)

注: 不用写@synthesize了,系统会默认为将setter传进来的值赋值以下划线开头的成员变量和getter返回以下划线开头的成员变量给调用者

  • @property只会生成最简单的setter和getter方法的声明和实现,并不会对传入的数据进行过滤,如果想对传入的数据进行过滤,可以自己重写setter和getter方法的实现
    • 如果重写了setter方法的实现,那么@property只会帮忙生成getter方法的实现
    • 如果重写了getter方法的实现,那么@property只会帮忙生成setter方法的实现
    • 如果同时重写了setter方法和getter方法的实现,那么@property就不会帮忙生成以下划线开头的”私有”成员变量,需要自己生成

注: 使用@property并不会帮忙实现dealloc中的[_name release];所以仍需要自己释放对象所拥有的资源

属性修饰符

属性修饰符用来修饰@property,给@property添加特定功能,以NSNumber对象为例

  • 如果不写修饰符(即默认assign),自动生成的setter是直接赋值
@property NSNumber *number;

- (void)setNumber:(NSNumber *)number
{
    _number = number;
}
  • 如果写了retain修饰符,自动生成的setter便是符合内存管理规则的存取器方法(判断两次传入的对象是否相同,如果不同便release旧对象retain新对象,并将新对象赋值给实例变量)
@property (retain) NSNumber *number;

- (void)setNumber:(NSNumber *)number
{
    if (_number != number)
    {
        [_number release];
        _number = [number retain];
    }
}

在OC中有如下几类属性修饰符

  • retain, assign, copy
    • retain自动生成的setter是判断当前对象与传入对象是否相同,如果不同release旧对象retain新对象(适用于OC对象)
    • assign自动生成的setter是直接赋值(默认,适用于基本数据类型)
    • copy:自动生成的setter是判断当前对象与传入对象是否相同,如果不同release旧对象copy新对象(适用于NSString *,详见”iOS内存管理之Copy”)
  • readonly, readwrite
    • readonly只生成getter的声明和实现
    • readwrite同时生成setter和getter的声明和实现
  • atomic, nonatomic
    • atomic性能低(默认)
    • nonatomic性能高(iOS开发中99.99%使用这个)
  • setter, getter
    • setter给生成的setter方法取一个名字,一定要有冒号
    • getter给生成的getter方法取一个名字,一般用于BOOL类型中
// 枚举
typedef NS_ENUM(NSInteger, Sex)
{
    SexMan,
    SexWoman
};

// 结构体
typedef struct
{
    int year;
    int month;
    int day;
} Date;

@interface Person : NSObject

@property (nonatomic, assign) int age;
@property (nonatomic, assign) Sex sex;
@property (nonatomic, assign) Date birthday;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain, readonly) NSString *phone;
@property (nonatomic, assign, getter = isRich) BOOL rich;

@end

@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");

    [_name release];
    [_phone release];

    [super dealloc];
}

@end
使用存取器方法时应该注意以下使用场景
  • 如果在类中声明了一个方法用于设置实例变量,需要使用存取器方法来设置
@property (nonatomic, retain) NSNumber *count;

- (void)reset
{
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}

// 虽然下述方法也能实现,但是这种方式避开了存取器方法,可能会导致一些错误(比如实例变量内存管理规则发生了变化,并且这种方法不符合KVO规则)
- (void)reset
{
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}
  • 不要在init方法和dealloc方法中使用存取器方法来访问和修改实例变量
- - init 
{
    self = [super init];
    if (self) 
    {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}

- initWithCount:(NSNumber *)startingCount 
{
    self = [super init];
    if (self) 
    {
        _count = [startingCount copy];
    }
    return self;
}

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

循环import

#import概述

import是一个预编译指令,他会将”“中的文件拷贝到import所在的位置,不过一旦”“中的文件发生变化,那么import就会重新拷贝一次,从而降低编译效率

在开发中经常会出现循环import的情况,表现形式为在A.h文件中import了B.h文件,在B.h文件中import了A.h文件,那么A.h和B.h两个文件会相互拷贝,造成循环import报错

@class概述

@class ClassName;仅仅是告诉编译器,ClassName是一个类,不会做任何拷贝操作,不过由于@class仅仅是告诉编译器ClassName是一个类,并不知道ClassName类中到底有什么方法,所以在.m文件中使用该类时还是要#import该类

使用@class避免循环import

在开发中我们可以使用@class来避免循环import的问题,即在想要使用一个类的时候,可以在.h中使用@class ClassName;来声明一个类,真正使用这个类的时候在.m中#import “ClassName”即可

注: 在一个ClassName发生变化时,一般只有import了它的.m类才需要重新拷贝,并不会让其他类间接受到影响(因为其他类不会引用.m文件)

循环引用

如果A对象要拥有B对象的所有权,同时B对象也要拥有A对象的所有权,那么就会产生循环引用,导致两者都无法被正常释放

@interface Person : NSObject
@property (nonatomic, retain) Car *car;
@end

@interface Car : NSObject
@property (nonatomic, retain) Person *person;
@end

针对循环引用的问题,Cocoa建立了一种规则,就是在出现循环引用的情形时,不要让A对象和B对象互相拥有对方的所有权,而是让一方(parent)拥有另一方(children)的”强”引用,另一方(children)拥有这一方(parent)的”弱”引用,即将A retain B, B retain A中一方的retain改为assign即可

@interface Person : NSObject
@property (nonatomic, retain) Car *car;
@end

@interface Car : NSObject
@property (nonatomic, assign) Person *person;
@end

注: 使用assign之后,dealloc中无需再释放该资源

在Cocoa中使用到”弱”引用的例子包括但不局限于table data sources, outline view items, notification observers, and miscellaneous targets and delegates,在处理”弱”引用的对象时一定要小心处理,因为parent并不拥有children的所有权,也就是并不能保证在使用children的时候其一定存在(可能已经被释放),所以在parent自身释放时一定要告知children一下自身已经释放,不要再继续给自己发消息,否则会造成应用程序的闪退(野指针错误).处理办法针对通过中心/观察者而言,便是移除观察者;针对delegate而言,便是setDelegate:为nil,这个操作一般都是在dealloc中进行

自动释放池

概述

自动释放池提供了延迟放弃一个对象的所有权的机制,比如想要在一个方法中返回一个对象,如果先使用release放弃了该对象的所有权,那么return返回的对象便是一个僵尸对象,如果先进行return返回,那么便无法放弃该对象的所有权,导致了内存泄漏

自动释放池的创建

iOS5.0之前创建自动释放池方法(现在也可使用)

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// do something...
[pool release];

iOS5.0之后创建自动释放池方法

@autoreleasepool
{
    // do something...
}
autorelease方法

autorelease是一种支持引用计数的内存管理方式,只要在自动释放池中给对象发送一条autorelease消息,就会将对象放到自动释放池中,当自动释放池被销毁时,会对池中的所有对象发送一条release消息

  • autorelease方法会返回对象本身
  • autorelease方法不会修改对象的引用计数器
  • autorelease方法可以让开发者不用实时关心什么时候发送release消息

注1: 自动释放池被销毁时,只是给池中所有对象发送一条release消息,不代表对象一定会被释放

注2: 对象在自动释放池中每收到一条autorelease消息,在自动释放池被销毁时,对象都会收到一次release消息

autorelease方法使用注意事项
  • 一定要在自动释放池中调用autorelease方法,才会将对象放入自动释放池中
  • 即使在自动释放池内创建对象,只要不调用了autorelease方法,就不会将对象放入自动释放池中
  • 即使在自动释放池外创建对象,只要在自动释放池中调用了autorelease方法,就会将对象放入自动释放池中
  • 一个程序中可以创建N个自动释放池,且自动释放池可以嵌套,这些自动释放池以栈结构存在(先进后出),当一个对象调用autorelease方法时,会将这个对象放到栈顶的自动释放池中
  • autorelease不能精准地释放内存(延迟释放),因为要将池中的所有内容都执行完才会释放自动释放池,所以占用内存比较大的东西还是使用release为宜
创建自己的自动释放池

Cocoa希望代码在自动释放池中执行,否则发送了autorelease消息的对象便不能收到release消息,从而导致内存泄漏.UIKit库会在自动释放池中处理每一个事件循环,所以我们并不需要显式地创建自动释放池,不过如下三种情况下,我们仍然需要创建自己的自动释放池

  1. 如果不是基于UI库(UI framework)进行开发,而是采用命令行工具(command-line tool)进行开发
  2. 在使用循环时,尤其是循环次数多,且非常占用内存的情况下,可以在循环内部创建自己的自动释放池,在每次循环时都将临时变量销毁,以免大量临时变量堆积,导致内存短时占用过高
  3. 如果大量产生第二线程,线程一被执行就必须创建自己的自动释放池,否则程序会内存泄漏(这里未来需要进行详细补充)
for (int i = 0; i < 999999; i++)
{
    @autoreleasepool
    {
        Person *p = [[[Person alloc] init] autorelease];
        // do something...
    }
}
类工厂方法内存管理

在开发中,我们经常使用Foundation框架中的类,在调用其类工厂方法创建一个对象时,因为并不是使用alloc,new,copy或者mutableCopy方法创建的,所以并不需要我们自己在给该对象发送release或者autorelease消息,这是因为类工厂方法内部都已经在返回对象前进行过延迟释放

我们在自己书写类工厂方法时,也应该与系统处理方式相同,快速返回一个autorelease对象的方式具体如下

+ (instancetype)person
{
    // 使用self而不是使用Person是因为这样可以在子类调用该方法时会返回子类的对象
    return [[[self alloc] init] autorelease];
}

快速返回一个带有参数的autorelease对象的方式具体如下

+ (instancetype)personWithName:(NSString *)name
{
    Person *person = [[[self alloc] init] autorelease];
    person.name = name;
    return person;
}

注: Foundation框架中的类,所有通过类工厂方法创建的对象都是autorelease的,所以不用自己进行释放

集合中对象的内存管理

集合在iOS中包括NSArray、NSDictionary、NSSet,接下来以数组为例讲解一下集合中的内存管理规则

  • 将一个对象添加到一个数组中,数组会对该对象进行一次retain操作
  • 当数组释放后,会给数组中每个对象发送一条release消息
  • 当数组移除一个对象后,会给这个对象发送一条release消息
NSMutableArray *array = [NSMutableArray array];
for (NSUInteger i = 0; i < 10; i++) 
{
    NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
    // allocedNumber对象被加入数组,数组会对该对象进行一次retain操作
    [array addObject:allocedNumber];
    [allocedNumber release];
}

// 当array对象被释放,系统会给数组中的每个allocedNumber对象发送一条release消息,保证数组中的每个元素都能正常释放

内存管理中需要注意的两个使用场景

  • 当一个指针所指向的对象为数组中的某一个元素时,如果数组将该元素移除或者数组对象自身被释放,此时该指针指向的对象已经被释放了
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject被从数组中移除,会收到一条release消息,此时heisenObject所指向的对象已经被释放了

// 在实际开发中,我们并不希望这样的情况发生,所以需要使用retain拥有该对象的所有权
heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// do something...
[heisenObject release];
  • 当一个指针所指向的对象是通过另一个对象获得的,如果另一个对象自身被释放,此时该指针指向的对象已经被释放了
id parent = <#create a parent object#>;
heisenObject = [parent child];
[parent release];
// heisenObject所指向的对象是通过另一个对象(例子中的parent对象)获得的,另一个对象是该对象的唯一拥有者,如果另一个对象被释放了,此时heisenObject所指向的对象已经被释放了

// 在实际开发中,我们并不希望这样的情况发生,所以需要使用retain拥有该对象的所有权
id parent = <#create a parent object#>;
heisenObject = [[parent child] retain];
[parent release];
// do something...
[heisenObject release];

iOS内存管理之ARC

概述

ARC(Automatic Reference Counting,自动引用计数),是iOS4引入的一项新技术(从iOS5开始支持弱引用),其使用与MRR&MRC相同的内存管理规则来管理内存,不过编译器会在编译阶段自动地在适当的位置插入retain、release和autorelease等内存管理代码来管理内存(属于编译器特性,不是运行时特性),不再需要程序人员手动管理.官方强烈建议使用ARC方式来管理内存

注: OC中的ARC和Java中的垃圾回收机制不一样,Java中的垃圾回收是系统做的,而OC中的ARC是编译器做的

展示ARC与MRC使用区别的小示例
// MRC
@interface Person : NSObject

@property (retain) NSNumber *number;

@end

@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
    
    [_number release];
    
    [super dealloc];
}

@end

Person *person = [[Person alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2];
person.number = number;

[number release];
[person release];
// perosn和number正常被释放
// ARC
@interface Person : NSObject

@property (strong) NSNumber *number;

@end

@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end

Person *person = [[Person alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2];
person.number = number;

// perosn和number出了作用域正常被释放
ARC管理内存的优势

消除了手动管理内存的烦恼,不再需要手动调用retain、release和autorelease等方法来管理内存(实际上在ARC模式下已经不能调用出来该方法了),编译器在编译阶段会自动地在适当的位置插入这些代码,这样可以极大概率地避免出现内存问题.同时编译器也会在一些地方执行某些优化,让代码性能更高,官方提供了一个能够体现出来这种优势的插图,如下

ARC

ARC与MRC的混合开发
  • 如果想在ARC项目中使用MRC文件,可以在Build Phases中的Compile Sources中对应文件加入编译标记-fno-objc-arc
  • 如果想在MRC项目中使用ARC文件,可以在Build Phases中的Compile Sources中对应文件加入编译标记-fobjc-arc

ARC引入的新规则

为了使ARC能够正常工作,在ARC中引入了一些区别于当前编译模式的新的规则,如果你违反了这些规则,在编译阶段编译器会给出一个警告

  • 不能实现或者调用retain、release、autorelease和retainCount方法,甚至不能使用@selector(retain)、@selector(release)等方式调用
  • 不能调用dealloc方法
    • 可以实现dealloc方法,用于释放除了实例变量以外的其他资源
    • 不需要在这里释放实例变量(实际上也不能在这里给实例变量发送release消息)
    • 可以在这里调用[systemClassInstance setDelegate:nil],以便处理不是用ARC编译的systemClass(在MRC下delegate使用assign修饰,如果自身被释放,delegate会变成野指针,所以需要在dealloc中将其置空;在ARC下delegate使用weak修饰,如果自身被释放,delegate会自动置空)
    • 不需要调用[super dealloc],编译器会自动调用
  • 不能使用NSAutoreleasePool来创建自动释放池,而是需要使用@autoreleasepool来代替
  • 为了与MRC之间进行互相操作,ARC中不允许给存取器命名为以new开头(即不能声明以new开头的属性),除非为该属性定义一个新的getter名称
// 错误
@property NSString *newTitle;
 
// 正确
@property (getter=theNewTitle) NSString *newTitle;

ARC引入的新特性

两个属性修饰符: strong和weak

在ARC中新增了两个属性修饰符: strong和weak,其中strong是默认修饰符,下面介绍一下这两个属性修饰符与retain和assign的区别

// 下面这句对于strong的示例,与此同义: @property(retain) MyClass *myObject;
@property(strong) MyClass *myObject;

// 下面这句对于weak的示例,与此相似: @property(assign) MyClass *myObject;
// 使用assign修饰的指针所指向的对象如果被释放,该指针会变成野指针;使用weak修饰的指针所指向的对象如果被释放,该指针会变成空指针
@property(weak) MyClass *myObject;

针对于ARC中属性修饰符的使用,要进行如下变化

  • strong用于OC对象,相当于MRC中的retain
  • weak用于OC对象,相当于MRC中的assign
  • assign用于基本数据类型,相当于MRC中的assign

注: 其实就是将MRC中的assign分成了两个部分,分别用于修饰OC对象与基本数据类型

四个变量修饰符

在ARC中新增了四个变量修饰符: 双下划线strong、双下划线weak、双下划线unsafe_unretained和双下划线autoreleasing,其中双下划线strong是默认修饰符,下面介绍一下这四个变量修饰符

  • 双下划线strong: 强引用,只要有强指针指向该变量,该变量便会一直存在
  • 双下划线weak: 弱引用,只要没有强指针指向该变量,该变量便会被置空(即设置为nil)
  • 双下划线unsafe_unretained: 不安全的弱引用,只要没有强指针指向该变量,该变量不会被置空(即设置为nil),而会变成野指针
  • 双下划线autoreleasing: 用于标示自动释放的变量

官方提醒,在为变量添加修饰符时,最正确的方式如下

// 规则
ClassName * qualifier variableName;

// 正确示例
MyClass * __weak myWeakReference;
MyClass * __unsafe_unretained myUnsafeReference;

// 错误示例(虽然错误,但是编译器会默认为正确,官方说法为"forgiven")
__weak MyClass * myWeakReference;
__unsafe_unretained MyClass * myUnsafeReference;

注: 在直接使用__weak修饰变量指向一个刚创建的对象时,需要注意对象刚刚创建出来就会释放的情况

NSString * __weak string = [[NSString alloc] initWithFormat:@"loly"];
// 因为没有强指针指向该对象,该对象会立即被释放

ARC中多对象内存管理

概述

ARC中判断对象是否应该被释放,不再观察引用计数器是否为0,思想应该转变为是否有强指针指向该对象,只要有一个强指针指向该对象,该对象就会一直保持在内存中不会被释放

  • 当A对象想要拥有B对象的所有权时,直接使用一个强指针指向它即可
  • 当A对象想要放弃对B对象的所有权时,什么都不需要做,编译器会自动处理

在涉及到多对象内存管理时,在MRC中使用的存取器方法也需要进行一些调整

存取器方法
  • getter方法规范: 直接返回实例变量
// 针对基本数据类型
- (int)age
{
    return _age;
}

// 针对OC对象
- (Room *)room
{
    return _room;
}
  • setter方法规范:
// 针对基本数据类型,直接将新值赋值给实例变量
- (void)setAge:(int)age
{
    _age = age;
}

// 针对OC对象,直接将新值赋值给实例变量
- (void)setRoom:(Room *)room
{
    _room = room;
}

循环引用

因为ARC使用与MRC相同的引用计数规则,所以同样也会出现循环引用问题

在MRC中的解决办法是让parent持有children的”强”引用,而children持有parent的”弱”引用,即将A retain B, B retain A中一方的retain改为assign即可

在ARC中的解决办法与在MRC中原理相同,即将A strong B, B strong A中一方的strong改为weak即可

@interface Person : NSObject
@property (nonatomic, strong) Car *car;
@end

@interface Car : NSObject
@property (nonatomic, weak) Person *person;
@end

注: 当使用Block时,会出现不易察觉的循环引用现象,关于这点我在另一篇文章中进行过介绍,这里不再赘述

ARC中使用新的方式创建自动释放池

关于自动释放池在上文已经详细介绍过,这里不再赘述

// MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// do something...
[pool release];

// ARC
@autoreleasepool
{
    // do something...
}

栈变量在ARC中默认初始化为nil

在ARC中,栈变量会被初始化为nil,即使不进行赋值,程序也不会造成崩溃,示例如下

- (void)myMethod 
{
    NSString *name;
    NSLog(@"name: %@", name);
    // 打印结果为nil,不会崩溃
}

iOS内存管理之Copy

概述

copy(复制、拷贝)是产生一个副本对象的过程,只要是通过拷贝产生的副本对象,副本对象中的内容与源对象中的内容就完全一致,下面介绍几个copy相关的知识点

copy的特点
  • 修改源对象的属性和行为,不会影响副本对象
  • 修改副本对象的属性和行为,不会影响源对象
copy与mutableCopy
  • 使用copy产生的副本对象是不可变的(如NSString,NSArray)
  • 使用mutableCopy产生的副本对象是可变的(如NSMutableString,NSMutableArray)
通过拷贝是否会产生新的对象

通过拷贝是否会产生新对象,就要看源对象与副本对象是否满足拷贝的特点

  • 可变对象通过mutableCopy,会生成新的对象
  • 可变对象通过copy,会生成新的对象
  • 不可变对象通过mutableCopy,会生成新的对象
  • 不可变对象通过copy,不会生成新的对象(因为源对象与副本对象都是不可变的,已经满足拷贝的特点)
深拷贝(内容拷贝)与浅拷贝(指针拷贝)
  • 深拷贝: 如果通过拷贝生成了新对象,就称为深拷贝(内容拷贝)
  • 浅拷贝: 如果通过拷贝没生成新对象,就称为浅拷贝(指针拷贝)

注1: 通过深拷贝,源对象的引用计数器不变,副本对象的引用计数器为1,所以需要对副本对象进行一次release操作

注2: 通过浅拷贝,源对象的引用计数器+1,所以需要对源对象再进行一次release操作

copy与retain作为属性修饰符的不同

  • 通过copy作为属性修饰符来修饰对象的属性,是对传入变量进行了一次copy操作,在外界变量进行修改之后,对象的属性不会随之发生变化
  • 通过retain作为属性修饰符来修饰对象的属性,是对传入变量进行了一次retain操作,在外界变量进行修改之后,对象的属性会随之发生了变化

注: 字符串的属性都应该使用copy修饰符进行修饰

示例
@interface Person : NSObject

@property (nonatomic, copy) NSString *theCopyName;
@property (nonatomic, strong) NSString *theStrongName;

@end

Person *p = [[Person alloc] init];
NSMutableString *mStr = [NSMutableString stringWithString:@"loly"];
p.theStrongName = mStr;
p.theCopyName = mStr;
[mStr appendString:@"y"];
NSLog(@"theStrongName = %@, theCopyName = %@", p.theStrongName, p.theCopyName);
// 打印结果: theStrongName = loly, theCopyName = lol

自定义类实现copy与mutableCopy功能

概述

一个类的对象想拥有copy与mutableCopy功能,都需要拥有一个前提

  • 使用copy的前提: 对应类需要遵守NSCopying协议,并实现copyWithZone:方法
  • 使用mutableCopy的前提: 对应类需要遵守NSMutableCopying协议,并实现mutableCopyWithZone:方法

注: OC中大部分类已经遵守了NSCopying,NSMutableCopying协议

下面我们将通过两个示例来介绍一下如何让自定义类拥有copy与mutableCopy功能

实现Person类的copy与mutableCopy功能
@interface Person : NSObject <NSCopying, NSMutableCopying>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

@end

@implementation Person

- (id)copyWithZone:(NSZone *)zone
{
    // 1.创建一个新对象
    Person *p = [[[self class] allocWithZone:zone] init];
    // 2.设置当前对象的内容给新的对象
    p.name = _name;
    p.age = _age;
    // 3.返回新的对象
    return p;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    Person *p = [[[self class] allocWithZone:zone] init];
    p.name = _name;
    p.age = _age;
    return p;
}

@end
实现Student类(继承自Person类)的copy与mutableCopy功能
@interface Student : Person

@property (nonatomic, assign) float height;

@end

@implementation Student

- (id)copyWithZone:(NSZone *)zone
{
    // 1.创建一个新对象
    id obj = [super copyWithZone:zone];
    // 2.设置当前对象的内容给新的对象
    [obj setHeight:_height];
    // 3.返回新的对象
    return obj;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    id obj = [super mutableCopyWithZone:zone];
    [obj setHeight:_height];
    return obj;
}

@end

参考文献

  1. Advanced Memory Management Programming Guide
  2. Transitioning to ARC Release Notes
  3. 互联网相关技术博客及视频

//