一. 初识MRC
- Automatic Reference Counting:ARC
Manual Reference Counting:MRC - 在iOS中,使用引用计数来管理OC对象的内存
- 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
- 内存管理的经验总结:
① 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
② 想拥有某个对象,就让它的引用计数+1,不想再拥有某个对象,就让它的引用计数-1 - 可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
首先搜索“Automatic Reference Counting”,关闭ARC,将项目改成手动内存管理。
//new方式和alloc init方式一样,只不过我们习惯了分两步走。
//MJPerson *person = [MJPerson new];
MJPerson *person = [[MJPerson alloc] init]; //1
//中间写我们想要的代码
[person release]; // 0
或者:
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease]; //1
//中间写我们想要的代码
}
我们可以手动进行release,也可以使用autoreleasepool,如果一个对象调用了autorelease,那么在@autoreleasepool{}结束之后,会对{}内部调用过autorelease的对象进行一次release操作。
二. 逐步完善MRC的setter方法
MJPerson里面拥有MJDog,在MJPerson里面重写setDog方法。
MRC的标准setter方法如下,一步都不能少,下面解释为什么这么写?
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
1. 为什么要retain新值
当setDog方法的实现是如下这样:
- (void)setDog:(MJDog *)dog
{
_dog = dog
}
执行以下代码:
void test2()
{
MJDog *dog = [[MJDog alloc] init]; // dog:1
MJPerson *person = [[MJPerson alloc] init]; // person:1
[person setDog:dog]; // dog:1
[dog release]; // dog:0
[[person dog] run]; //报错
[person release];
}
报错:
-[MJDog run]: message sent to deallocated instance 0x10064dd40
意思是run消息发送给一个已经释放掉的对象了(僵尸对象)。
上面代码,当[[person dog] run]的时候person还存在,但是dog却死了,这样肯定不合理啊,所以我们要让person拥有dog,所以给dog先retain一次再赋值。
setter方法修改如下:
- (void)setDog:(MJDog *)dog
{
_dog = [dog retain];
}
基于谁使用谁负责的原则,既然person拥有了dog,那么在person死掉的时候也要把dog再release一次,所以person的dealloc方法要这么写:
- (void)dealloc
{
[_dog release];
_dog = nil;
NSLog(@"%s", __func__);
// 父类的dealloc放到最后,一般都是先释放子类对象再释放父类对象
[super dealloc];
}
运行如下代码:
void test2()
{
MJDog *dog = [[MJDog alloc] init]; // dog:1
MJPerson *person1 = [[MJPerson alloc] init]; // person1:1
[person1 setDog:dog]; // dog:2
MJPerson *person2 = [[MJPerson alloc] init]; // person2:1
[person2 setDog:dog]; // dog:3
[dog release]; // dog:2
[person1 release]; // person1:0 dog:1
[[person2 dog] run];
[person2 release]; // person2:0 dog:0
}
打印:
-[MJPerson dealloc]
-[MJDog run]
-[MJDog dealloc]
-[MJPerson dealloc]
根据引用计数器分析和打印可知,两个person和一个dog都被释放了。
达到的效果:只要有人使用狗,狗就不会销毁,这也是我们想要的效果。
2. 为什么要release旧值
执行以下代码:
void test3()
{
MJDog *dog1 = [[MJDog alloc] init]; // dog1 : 1
MJDog *dog2 = [[MJDog alloc] init]; // dog2 : 1
MJPerson *person = [[MJPerson alloc] init]; // person : 1
[person setDog:dog1]; // dog1 : 2
[person setDog:dog2]; // dog2 : 2
[dog1 release]; // dog1 : 1
[dog2 release]; // dog2 : 1
[person release]; //person : 0 dog2 : 0 对最后传进来的dog2进行release
}
打印:
-[MJDog dealloc]
-[MJPerson dealloc]
可以发现,有一条狗没被释放,根据上面注释的引用计数器分析,可以知道是dog1没被释放,这样就造成了内存泄漏(该释放的对象没有释放)。
因为dog1是旧值,所以赋新值之前要对旧值进行release操作,setter方法修改如下:
- (void)setDog:(MJDog *)dog
{
[_dog release]
_dog = [dog retain];
}
重新执行以下代码:
void test3()
{
MJDog *dog1 = [[MJDog alloc] init]; // dog1 : 1
MJDog *dog2 = [[MJDog alloc] init]; // dog2 : 1
MJPerson *person = [[MJPerson alloc] init]; // person : 1
[person setDog:dog1]; // dog1 : 2
[person setDog:dog2]; // dog2 : 2, dog1 : 1
[dog1 release]; // dog1 : 0
[dog2 release]; // dog2 : 1
[person release]; //person : 0 dog2 : 0
}
调用 [person setDog:dog2] 的时候把dog1先release一次,最后person和两个dog都被释放了,也可参考上面的引用计数器分析进行理解。
3. 为什么要判断新值旧值是否相等
首先,打开僵尸对象检测,Edit scheme -> Run -> Diagnostics,勾选Zombie Objects。
执行以下代码:
void test4()
{
MJDog *dog = [[MJDog alloc] init]; // dog:1
MJPerson *person = [[MJPerson alloc] init]; // person : 1
[person setDog:dog]; // dog:2
[dog release]; // dog:1
[person setDog:dog]; // dog 0
[person setDog:dog];
[person setDog:dog];
[person release];
报错:
-[MJDog retain]: message sent to deallocated instance 0x10058cdf0
错误意思就是,retain消息发送给一个已经释放掉的对象了(僵尸对象)。
上面代码,执行[person setDog:dog]就是调用setter方法:
- (void)setDog:(MJDog *)dog
{
[_dog release]
_dog = [dog retain];
}
先release旧值后retain新值,旧值和新值是一样的,由于旧值release之后引用计数器就为0,dog被释放了,这时候再[dog retain]就会报上面的错。
在setter方法里面加个判断,如下:
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
重新执行代码:
void test4()
{
MJDog *dog = [[MJDog alloc] init]; // dog:1
MJPerson *person = [[MJPerson alloc] init]; // person : 1
[person setDog:dog]; // dog:2
[dog release]; // dog:1
[person setDog:dog];//旧值新值一样,不做任何事
[person setDog:dog];
[person setDog:dog];
[person release]; // person : 0 dog : 0
}
这时候,如果旧值和新值一样,就什么都不做了,根据上面引用计数器的分析,这样写就没问题了。
4. 完善
在dealloc里面我们经常见到别人这么写:
- (void)dealloc
{
self.dog = nil;
[super dealloc];
}
其实,self.dog = nil就相当于:
- (void)setDog:(MJDog *)dog
{
if (_dog !=nil) { // 如果旧值不为nil
[_dog release]; //就把旧值release
//_dog = [nil retain];
_dog = nil; //并且将指针置为nil
}
}
可以看出,和上面我们写的dealloc是一样的,所以推荐使用self.dog = nil这种方式,更简洁。
5. 总结
- 为什么要retain新值?
只要有一个人使用新值,就要保证新值不被销毁,所以自然要retain一下了。 - 为什么要release旧值?
旧值使用的时候retain过了,现在你不使用它了,肯定要把旧值release一下,否则旧值会一直在内存中,这样就造成了内存泄漏(该释放的对象没有释放)。 - 为什么要判断新值旧值是否相等?
先release旧值后retain新值,旧值和新值是一样的,由于旧值release之后引用计数器就为0,dog被释放了,这时候再[dog retain]就会报如下错:
-[MJDog retain]: message sent to deallocated instance 0x10058cdf0
错误意思就是,retain消息发送给一个已经释放掉的对象了(僵尸对象)。
如果是基本数据类型,不用进行内存管理,就很简单了,直接赋值就可以了。
如果是OC对象,那么它的setter方法要这样写:
MJPerson.h
#import <Foundation/Foundation.h>
#import "MJDog.h"
@interface MJPerson : NSObject
{
int _age;
MJDog *_dog;
}
- (void)setAge:(int)age;
- (int)age;
- (void)setDog:(MJDog *)dog;
- (MJDog *)dog;
@end
MJPerson.m
#import "MJPerson.h"
@implementation MJPerson
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
- (MJDog *)dog
{
return _dog;
}
- (void)dealloc
{
self.dog = nil;
[super dealloc];
}
@end
上面是MRC中最原始的也是最麻烦的写法了,随着Xcode编译器的发展,也出现了@synthesize关键字的用法,具体可参考:@synthesize和@dynamic,这里就不详细说了。
直到现在,ARC下,使用一个简单的@property就可以生成_age成员变量,setter、getter方法声明,setter、getter方法实现。
三. MRC下的@property
如果刚才你看了@synthesize和@dynamic,你就会知道,MRC下的@property只会生成setter、getter方法的声明,如果想生成_age成员变量和setter、getter方法的实现还要使用@synthesize关键字。
下面就看看使用不同的关键字修饰@property并且使用了@synthesize关键字,生成的setter、getter方法实现有什么不同。
1. 如果使用assign修饰
@property (nonatomic, assign) int age;
那么生成的就是:
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
可以发现,使用assign生成的setter方法没有内存管理相关的东西,所以assign一般用来修饰基本数据类型。
2. 如果使用retain修饰
@property (nonatomic, retain) MJDog *dog;
那么生成的就是:
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
- (MJDog *)dog
{
return _dog;
}
可以发现,使用retain修饰,生成的setter方法有内存管理相关的东西,所以retain一般用来修饰对象类型。
3. 使用@synthesize
就算在MRC环境下,我们也不会像我上面总结的那样,写那么一大串又原始又复杂的代码,MRC环境下,一般我们这么写:
#import <Foundation/Foundation.h>
#import "MJDog.h"
@interface MJPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, retain) MJDog *dog;
+ (instancetype)person; //工厂方法
@end
上面代码,使用@property生成setter、getter方法的声明
#import "MJPerson.h"
@implementation MJPerson
// 自动生成_开头的成员变量和setter、getter方法的实现
@synthesize age = _age, dog = _dog;
+ (instancetype)person
{
return [[[self alloc] init] autorelease]; //自动释放
}
- (void)dealloc
{
self.dog = nil;
[super dealloc];
}
@end
上面代码:
使用@synthesize自动生成_开头的成员变量和setter、getter方法的实现(如果是使用assign修饰的,就直接赋值,如果是使用retain修饰的,就生成相应的带内存管理的setter方法)。
在MRC里面,就算你使用retain自动生成了内存管理相关的setter方法,在dealloc里面还是要你自己去释放的(因为那两个关键字没帮我们自动生成)。
MRC环境下,我们也会给类添加一个工厂方法,可以自动释放对象,如上代码。使用起来也很简单,这样就不用我们每次手动release了,如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [MJPerson person];
}
return 0;
}
四. 体验MRC如何写代码
#import "ViewController.h"
@interface ViewController ()
@property (retain, nonatomic) NSMutableArray *data;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//刚开始我们这么写
NSMutableArray *data = [[NSMutableArray alloc] init];
self.data = data;
[data release];
//后来简化
self.data = [[NSMutableArray alloc] init];
[self.data release];
//后来又简化
self.data = [[[NSMutableArray alloc] init] autorelease];
//最后还可以这样写
//Foundation框架,一般使用类方法创建的对象,内部都已经调用了autorelease
//也可以这样想:array方法没看到alloc,所以不用release
self.data = [NSMutableArray array];
}
//一定要释放
- (void)dealloc {
self.data = nil;
[super dealloc];
}
@end
Demo地址:MRC