在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
我们得出结论
不可变对象调用 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;
}
直接报错:
copyWithZone
自定义的类要想实现
copy操作,需要实现<NSCopying>协议的copyWithZone方法:
- (id)copyWithZone:(NSZone *)zone{
Person *person = [[Person alloc]init];
person.dog = self.dog;
return person;
}











网友评论