在iOS5之前,iOS开发者需要自己管理对象的内存,很是繁琐.直到ARC出来后这些工作才由编译器和Objective-C运行时库共同完成.
虽然现在已经是ARC环境,内存管理相关的代码已经不需要开发者手动去写了,但是ARC的底层依然是MRC那一套,只不过这些手动管理内存的代码由编译器帮我们写了而已.所以要想更加深刻的理解OC的内存管理机制,就很有必要搞清楚MRC环境下的内存管理.
- 在MRC环境下,当调用
alloc , new , copy , mutableCopy
返回的对象在不需要这个对象时,需要调用release 或者 autorelease
来释放它.
比如下面这样:
Person *person = [[Person alloc]init];
//不用的时候要记得销毁
[person release];
或者调动autorelease
:
Person *person = [[[Person alloc]init]autorelease];
如果调用autorelease
,那么对象就会在适当的时候自动调用 release
释放,比如说:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[[Person alloc]init]autorelease];
}//执行大括号结束的的时候释放 [person release]
return 0;
}
如果该释放的对象没有释放就会造成内存泄漏.
下面代码运行会发生什么:
@interface Person : NSObject
{
Dog *_dog;
}
- (void)setDog:(Dog *)dog;
- (Dog *)dog;
@end
@implementation Person
- (void)setDog:(Dog *)dog{
_dog = dog;
}
- (Dog *)dog{
return _dog;
}
- (void)dealloc{
[super dealloc];
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc]init];
Person *person = [[Person alloc]init];
[person setDog:dog];
[dog release];
[[person dog] run];
//不用的时候要记得销毁
[person release];
}//如果调用 autorelease
return 0;
}
上面代码一运行就出现了坏内存访问,dog
已经release
了,所以我们在执行[[person dog] run];
的时候就崩溃了.但是这样显然不符合我们的要求,因为person
还没有销毁,正常来讲,我们调用person.dog
的方法应该是没有问题的.我们要做到如果person
还在,dog
也要保证存在.person
要对dog
保持持有关系.那应该怎么做呢?
只需在setter
方法中对dog
对象retain
即可:
- (void)setDog:(Dog *)dog{
_dog = [dog retain];
}
- (Dog *)dog{
return _dog;
}
- (void)dealloc{
//person对象一旦释放,就要放开对dog的持有关系
[_dog release];
_dog = nil;
[super dealloc];
NSLog(@"%s",__func__);
}
即使我们调用多遍也没有问题:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc]init];// dog 引用计数 1
Person *person = [[Person alloc]init];
[person setDog:dog];// dog 引用计数 2
[dog release];// dog 引用计数 1
Person *person2 = [[Person alloc]init];
[person2 setDog:dog];// dog 引用计数 2
[[person dog] run];
//不用的时候要记得销毁
[person release];// dog 引用计数 1
[[person2 dog]run];
[person2 release];// dog 引用计数 0
}//如果调用 autorelease
return 0;
}
这样还是会出现问题,比如像下面这样,Dog
对象多次赋值给Person
:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc]init];// dog 引用计数 1
Person *person = [[Person alloc]init];
[person setDog:dog];// dog 引用计数 2
Dog *dog2 = [[Dog alloc]init];// dog2 引用计数 1
[person setDog:dog2];//// dog2 引用计数 2
[dog release];// dog 引用计数 1
[dog2 release]; //dog2 引用计数 1
// person 最后释放的引用计数是最后一个赋值给它的,也就是 dog2
//所以dog2最后成功释放了,但是dog1还没有释放
[person release];//dog2 引用计数 0
}//如果调用 autorelease
return 0;
}
上述代码的结果就是dog2
会释放,但是dog
不会被释放,这样就造成了内存泄漏.解决的办法就是每次再给Person
内部的_dog
赋值之前,都要先把旧对象释放掉:
- (void)setDog:(Dog *)dog{
[_dog release];
_dog = [dog retain];
}
这样虽然不会报错了,但是还是会有别的问题,比如下面这样,重复给_dog
赋值:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc]init]; //dog 引用计数 1
Person *person = [[Person alloc]init];
[person setDog:dog];//dog 引用计数 2
[dog release];//执行这一行时,dog 引用计数 1
[person setDog:dog];
[person setDog:dog];
[person release];
}//如果调用 autorelease
return 0;
}
一运行一样报坏内存访问.这是因为[dog release]
会对dog
的引用计数减1,因为此时person
内部还引用了dog
,person
此时也还没有释放,所以此时dog
的引用计数还是1.再执行[person setDog:dog]
时,在setter
方法内部会先调用[_dog release];
.再调用_dog = [dog retain];
.而[_dog release]
执行完毕后dog
已经释放了.此时我们再去dog retain
就报坏内存访问了.已经释放的内存,再去retain
肯定不可以.那我们怎么解决呢?
解决办法就是在setter
方法内部判断一下,如果_dog
和传进来的dog
相同,就不进行任何操作:
- (void)setDog:(Dog *)dog{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
这样我们就解决了这个问题,并且dealloc
也可以直接写成self.dog = nil;
- (void)dealloc{
//person对象一旦释放,就要放开对dog的持有关系
self.dog = nil;
[super dealloc];
NSLog(@"%s",__func__);
}
self.dog = nil
就好比给setter
方法直接传入nil
:
- (void)setDog:(Dog *)nil{
if (_dog != nil) {
[_dog release];
// _dog = [nil retain]; [nil retain]还是nil
_dog = nil;
}
}
@synthesize
关键字
随着时间的推移,xcode
越来越智能化.比如我们这样写@property (nonatomic,assign)int age;
,Xcode
会自动给我们生成setter
,getter
方法的声明.@synthesize
会自动生成下划线开头的成员变量和setter,getter方法的实现:
//自动生成下划线开头的成员变量和setter,getter方法的实现
@synthesize age = _age123;
- (void)setAge:(int)age{
_age123 = age;
}
再到后来连@ synthesize
都不用写,@Property
会自动生成成员变量,setter,getter
方法的声明和实现.并且如果发现是assign
修饰就直接赋值,没有任何内存管理相关的操作.发现是retain
就生成内存管理相关的代码:
- (void)setDog:(Dog *)dog{
if (_dog != dog) {
[_dog release];
_dog = [dog retain]; //如果是 copy 这里就是 copy
}
}
虽然Xcode
越来越智能,但是在MRC年代还是需要在dealloc
手动release
的.
- 小提示:在MRC环境下一些类方法开头的初始化方法不需要调用
release
或者autorelease
,因为它的内部已经调用过release
,比如说NSArray
:
+ (instancetype)array{
return [[[self alloc]init]autorelease];
}
copy 关键字
我们在创建属性的时候有时候会用到copy
关键字.copy
的目的就是:产生一个副本对象跟源对象互不影响.也就是修改了源对象不会影响副本对象;修改了副本对象也不会影响源对象.
iOS中提供了两个copy
方法:
1: copy
:不可变拷贝,产生不可变副本
2: mutableCopy
:可变拷贝,产生可变副本.
举例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"123"];
NSMutableString *str1 = [str copy];
NSMutableString *str2 = [str mutableCopy];
// [str1 appendString:@"456"]; //报错
[str2 appendString:@"456"];
NSLog(@"%@,%@",str1,str2);
}//如果调用 autorelease
return 0;
}
结果:
2019-12-18 21:28:42.476911+0800 OC内存管理MRC[1728:970673] 123,123456
copy
和mutableCopy
有没有什么区别呢?
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str1 = [NSString stringWithFormat:@"123"];
NSMutableString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
NSLog(@"str1:%p,str2:%p,str3:%p",str1,str2,str3);
}//如果调用 autorelease
return 0;
}
结果:
OC内存管理MRC[1768:976040] str1:0x9a20e77157d44f17,str2:0x9a20e77157d44f17,str3:0x100706070
从打印结果可以看到str1
和str2
的内存地址是一样的,str3
的内存地址不一样.它们的内存关系如下:
为什么会这样呢?大家想想copy
的目的是什么?就是产生一个副本对象跟原对象互不影响
.我们只要达到这个目的就行了.既然str1
是不可变字符串,那我们就不必再开辟一块内存存放一个副本,因为它本身就是不可变的,我们只需对它retain
一份即可,何必再浪费内存空间呢.而mutableCopy
是产生一个可变对象,所以需要开辟一块新的内存存放可变str2
.让str2
的修改不影响到str1
.
我们对[str1 copy]
;进行copy
操作其实就相当于retain
:
我们得出结论
不可变对象调用 copy 返回的是他本身,只是相当于引用计数加1;可变对象调用 copy , 返回的是一个新的可变对象
由此我们就引申出两个概念:深拷贝,浅拷贝深拷贝:内容拷贝,产生新对象
浅拷贝:指针拷贝,没有产生新对象
练习一下,下面的代码是深拷贝还是浅拷贝?
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableString *str1 = [NSMutableString stringWithFormat:@"他日若遂凌云志"];
NSMutableString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
NSLog(@"str1:%p,str2:%p,str3:%p",str1,str2,str3);
}//如果调用 autorelease
return 0;
}
打印结果:
OC内存管理MRC[1809:987734] str1:0x103009860,str2:0x100705870,str3:0x1007859a0
答案全部都是深拷贝,原因如图:
NSArray
,NSMutableArray
,NSDictionary
,NSMutableDictionary
都和NSString
,NSMutableString
同样原理.我就不一一举例了,现在我们总结一下深拷贝
,浅拷贝
规则:拷贝规则
练习一:
下面代码的运行结果:
@interface Person : NSObject
@property (nonatomic,copy)NSMutableArray *mutableArray;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
person.mutableArray = [NSMutableArray array];
[person.mutableArray addObject:@"a"];
}//如果调用 autorelease
return 0;
}
已运行就会发现报错:
为什么会这样呢?因为我们的
mutableArray
使用copy
修饰的,所以我们在给它赋值的时候就像下面这样:
- (void)setMutableArray:(NSMutableArray *)mutableArray{
if (_mutableArray != mutableArray) {
[_mutableArray release];
_mutableArray = [mutableArray copy];
}
}
调用[mutableArray copy]
返回的是一个不可变数组,既然是一个不可变数组肯定没有addObject
方法,就会报错.所以我们以后在项目中可变类型的对象不要用copy修饰,因为用copy修饰会得到一个不可变的对象.
小提示:NSString
一般都用copy
修饰,因为用copy
修饰就能保证肯定是个不可变字符串.要是想改变这个字符串就直接赋值一个新的字符串就好了.避免在外面使用appendString
修改字符串影响到其他地方.
自定义对象的copy
mutableCopy
主要是给Foundation
框架的NSMutableString
,NSMutableArray
,NSMutableDictionary
提供的.所以自定义对象只要考虑好copy
就可以了.
我们试一下自定义对象的copy
:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person1 = [[Person alloc]init];
Person *person2 = [person1 copy];
}//如果调用 autorelease
return 0;
}
直接报错:
自定义的类要想实现
copy
操作,需要实现<NSCopying>
协议的copyWithZone
方法:
- (id)copyWithZone:(NSZone *)zone{
Person *person = [[Person alloc]init];
person.dog = self.dog;
return person;
}
网友评论