1.2 iOS中内存分配的实现
话说我昨天写了一篇告诉大家怎么泡妹子的文章,但是果然程序员都是不缺妹子的啊!!!好吧,既然不缺,那我们还是继续研究技术吧!还是同一句话,如果有交流技术的同学可以加我的qq:1583042987。
上一节的例子讲述了objective-c的内存管理方式,什么是引用计数,以及如何施行。对此有了一个粗略的概念之后,这一节,将使用代码来对此做具体的阐述。
1.2.1 alloc/retain/release/dealloc/autorelease的实现
通过之前的例子,可以清楚的了解到内存的管理步骤,但是具体在程序中如何实现呢?在objective-c中使用alloc/retain/release/dealloc/autorelease这些方法为基础,通过需求进行操作,接下来通过代码来进一步了解。
/*
* 活动开始,并已经有一名学生持有图书资源
*/
id student = [[NSObject alloc]init];
/**
* 另一名学生持有图书资源
*/
[student retain];
/**
* 打印学生持有资源总数
*/
NSLog(@"How many books is %lu.",(unsigned long)[student retainCount]);
/*
* 一名学生释放资源
*/
[student release];
/**
* 打印release后,学生持有资源总数
*/
NSLog(@"After release,How many books is %lu.",(unsigned long)[student retainCount]);
/**
* 将资源注册到自动释放池,当释放池结束时自动调用release释放内存
*/
[student autorelease];
/**
* 打印autorelease后,学生持有资源总数
*/
NSLog(@"After autorelease,How many books is %lu.",(unsigned long)[student retainCount]);
打印结果为:How many books is 2.
After release,How many books is 1.
After autorelease,How many books is 1.
从代码中可以看出,当调用alloc时对象向系统请求一块内存,生成并持有这块资源,再调用retain方法的时候,持有数量加1,引用计数数量打印结果为2,而调用release则减1,引用计数数量打印结果为2,在调用autorelease 时将此次操作记录到一个名为自动释放池的管理器中,暂时并不释放,引用计数不变。当释放池结束运行时,会将注册在里面的内存释放,如图1.2所示。
图1.2 自动释放池流程
在引用计数技术下,每一个对象都存有一个计数器,纪录当下对象持有资源数量,当数量为0时,调用dealloc彻底释放这块内存,为避免出现“dangling pointer”(悬挂指针,也称为迷途指针)现象,意为指针指向无效或者已被释放的内存地址,为避免这样的事情发生通常会在计数为0时将对象至为nil确保其安全释放。
如代码所示,对象的引用计数可以通过retaincount实例方法获得,但这个方法并不实用,原因有三点,一,虽然它保留了对象的计数,但保留计数的绝对值,且永远不会返回0,这就给开发者带来极大的不便,二,它只保留了对象在某一时间点的计数,当使用autorelease时并不能考虑到自动释放池运行结果,那就给开发者带来一个错误的信息,容易导致开发者过度释放,三,在处理如NSString,NSNumber这样基本数据类型时,返回值往往会很大,如下代码。
/**
* 为一个字符串赋值,并打印其引用计数
*/
NSString *str = @"iOS性能优化";
NSLog(@"str is retainCount = %lu.",(unsigned long)[str retainCount]);
/**
* 为一个数字类型的对象复制,并打印其引用技术
*/
NSNumber *num = @1;
NSLog(@"num is retainCount = %lu.",(unsigned long)[num retainCount]);
打印结果为:str is retainCount = 18446744073709551615.
num is retainCount = 9223372036854775807.
此时如果使用retainCount方法查看计数就会非常尴尬,另外在ARC中,retainCount方法已经被禁用。
在objective-c中还有两种方法可以增加引用计数copy(浅拷贝)和mutablecopy(深拷贝)。使用copy的类必须遵守NSCopying协议规定,该协议只有一个方法:
/*************** Basic protocols ***************/
@protocol NSCopying
- (id)copyWithZone:(NSZone *)zone;
@end
而使用mutablecopy必须遵守NSMutableCopying协议规定,该类也是只有一个方法:
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(NSZone *)zone;
@end
对于初学者而言,很难理解zone的含义。在很早的时候,开发系统程序时,会把内存分成很多不同的“区域”(zone),而对象则放在其中。当然现在苹果为了给开发者提供更快速,更方便的环境,将每个程序都只用了一个区域——“默认区域”(default zone)。所以开发者不用再纠结于zone这个参数了。
两者都可以生成并持有对象的副本,区别在于copy生成的副本不可变,而mutablecopy生成的副本是可变的。调用二者都会让对象的引用计数递增,于使用retain方法不同的是,retain是持有一个新的对象,而copy/mutablecopy则是持有对象本身的副本。那么使用copy以及mutablecopy对程序编写有什么好处呢?
(NSString mutablecopy) ===> NSMutableString
(NSMutableString copy) ===> NSString
如上面为代码所示,使用copy与mutablecopy可以使对象在可变与不可变之间互相切换。另外使用copy/mutablecopy可以将一些对象从“栈内存”(stack)复制到“堆内存”(heap),比如“闭包”(block),当然block这个对象还有很多使用规则,在本节不做过多介绍,后面将会对此进行详细讲解。
1.2.2 在开发项目中的循环引用问题
通过上面的介绍,对于objective-c的内存管理多少有一些认识,那接下来结合实际对内存的使用规则进行更深入的了解。
在使用引用计数机制时,最为需要注意的问题就是“循环引用”(retain cycle),有人也会称之为“内存保留环”,也就是两个或者多个对象间互相引用,导致内存泄露,如图1.3。
图1.3 循环引用实例图
从图上可以看出来对象之间互相持有,这就造成至少有一个对象不能正常释放,在这个循环里,所有对象的保留计数都是1。这种情况,常见于类与类之间的联系,如“代理”(delegate)或是“闭包”(block)。下面通过不同代码,对代理与闭包分别展示一下。
BasicManage.h文件
#import <Foundation/Foundation.h>
@class BasicManage;
@protocol BasicManageDelegate <NSObject>
/**
* 设置网络请求完成后回调
*
* @param manage 网络请求管理器
* @param data 返回值
*/
-(void)netWorkManage:(BasicManage *)manage
didReceiveData:(NSData *)data;
/**
* 设置网络请求异常回调
*
* @param manage 网络管理器
* @param error 错误信息
*/
-(void)netWorkManage:(BasicManage *)manage
didFailWithError:(NSError *)error;
@end
@interface BasicManage : NSObject
@property(assign,nonatomic)id<BasicManageDelegate>delegate;
@end
BasicManage.m文件
#import "BasicManage.h"
@implementation BasicManage
@end
BasicModel.h文件
#import "BasicManage.h"
@interface BasicModel : NSObject<BasicManageDelegate>
/**
* 归档网络请求数据
*
* @param data 网络请求数据
*/
-(void)saveArchiveNetWorkData:(NSData *)data;
@end
BasicModel.m文件
#import "BasicModel.h"
@implementation BasicModel
-(instancetype)init
{
self = [super init];
if (self)
{
BasicManage *manage = [BasicManage new];
manage.delegate = self;
}
return self;
}
/**
* 返回成功方法
*
* @param manage 网络请求管理器
* @param data 请求返回值
*/
-(void)netWorkManage:(BasicManage *)manage didReceiveData:(NSData *)data
{
[self saveArchiveNetWorkData:data];
}
-(void)netWorkManage:(BasicManage *)manage didFailWithError:(NSError *)error
{
}
-(void)saveArchiveNetWorkData:(NSData *)data
{
}
@end
此例中delegate属性用assign修饰,原因在于如果使用retain修饰的话,在设置代理方时manage对象被model对象持有,形成循环引用,导致最后dealloc时manage对象不能正常销毁,而使用assign只是简单赋值,并不持有,所以不会造成循环引用的问题。同样的情况也发生在block中,如下代码。
BasicManage *manage = [BasicManage new];
manage.delegate = self;
[manage receiveDataWithError:^(NSError *error) {
self.title = [NSString stringWithFormat:@"%@",error];
}];
由于在block中使用对象,当block从栈存储区域复制到堆时,对象同时被block强引用持有,此时self已经被block持有,很多初学者,对于block的内存管理不够了解,常常会造成这样的内存问题,导致内存暴增,如何解决这样问题?如下代码。
BasicModel __weak *obj = self;
[manage receiveDataWithError:^(NSError *error) {
obj.title = [NSString stringWithFormat:@"%@",error];
}];
这里先不对上述代码做过多解释,后面将通过对block一步步了解之后,解决这些问题。
通过上述代理对于循环引用的处理方法可以知道,采用简单赋值并不持有的方法,可以有效避免循环引用问题,或者从外界命令循环中的某个对象不在保留另一个对象,这两种方法都可以有效的解决循环引用的问题,从而避免内存泄漏。