自动引用计数(ARC)是指内存管理中对引用采取自动计数的技术。
使用ARC,就无需再次键入retain或者release代码,这降低了程序崩溃,内存泄漏等风险的同时,很大程度上减少了开发程序的工作量。ARC技术使得编译器清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。
人工引用计数(Manual Reference Counting)
对象操作 | oc方法 |
---|---|
生成并持有对象 | alloc/new/copy/mutableCopy 等方法 |
持有对象 | retain 方法 |
释放对象 | release 方法 |
废弃对象 | dealloc 方法 |
id obj = [[NSObject alloc] init];
id obj = [NSObject new];
// 两者完全一致,生成并持有对象
// NSCopying和NSMutableCopying
id obj = [NSMutableArray array];
// 取得的对象存在,但自己不持有
[obj retain];
// 自己持有对象
自己持有的对象,一旦不再需要,持有者有义务释放该对象。
id obj = [NSMutableArray array];
// 取得的对象存在,但自己不持有
[obj retain];
// 自己持有对象
[obj release];
// 释放对象
// 对象不可再被访问
array方法的实现
- (id) object
{
id obj = [[NSObject alloc] init];
// 自己持有对象
[obj autorelease];
// 释放后取得的对象存在,但自己不持有该对象
return obj;
}
release方法时调用后立即释放,而autorelease方法则是不立即释放而是注册到autoreleasepool中,在超出指定的生存范围时能够自动并正确释放(调用release)。
id obj1 = [obj0 object];
// 取得对象存在但不持有
[obj1 retain];
// 自己持有对象
id obj = [NSObject alloc] init];
[obj release];
[obj release];
id obj1 = [obj0 object];
[obj1 release];
id obj = [NSObject alloc];
+(id)alloc
{
return [self allocWithZone: NSDefaultMallocZone()];
}
+(id)allocWithZone:(NSZone*)z
{
return NSAllocateObject(self, 0, z);
}
//
struct obj_layout
{
NSUInteger retained;
};
inline id NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
int size = 计算容纳对象所需内存大小;
id new = NSZoneMalloc(zone, self);
memset(new, o, size);
new = (id)&((struct obj_layput *)new)[1];
}
NSDefaultMallocZone和NSZoneMalloc中包含的NSZone时防止内存碎片化而引入的结构,对内存分配的区域本身进行多重化管理,根据使用对象的目的,对象的大小分配内存,从而提高了内存管理的效率。
去掉NSZone后代码简化版
struct obj_layout
{
NSUInteger retained;
};
+(id) alloc
{
int size = sizeof(struct obj_layout) + 对象大小;
struct obj_layout *p = (struct obj_layout *)calloc(1, size);
return (id)(p+1);
}
方法中的retain整数用来保持引用计数并将其写入对象内存头部。
对象的引用计数通过retainCount来实现
id obj = [[NSObject alloc] init];
[obj retainCount];
- (NSUInteger)retainCount
{
return NSExtraRefCount(self) + 1;
}
inline NSUInteger NSExtraRefCount(id anObject)
{
return ((struct obj_layout *)anObject)[-1].retained;
}
通过retain方法可使retained变量加1,通过release方法可使retained变量减1
retain方法的实现:
-(id)retain
{
NSIncrementExtraRefCount(self);
return self;
}
inline void NSIncrementExtraRefCount(id anObject)
{
if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1)
[NSException raise: NSInternalInconsistencyException format: @"....."];
((struct obj_layout *) anObject)[-1].retained++;
}
release方法的实现
- (void)release
{
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL NSDecrementExtraRefCountWasZero (id anObject)
{
if ((struct obj_layout *) anObject)[-1].retained == 0) {
return YES;
}
else
{
((struct obj_layout *)anObject)[-1].retained--;
return NO;
}
}
dealloc方法的实现
-(void)dealloc
{
NSDeallocateObject(self);
}
inline void NSDeallocateObject (id anObject)
{
struct obj_layout *o = &((struct obj_layout *) anObject)[-1];
free(o);
}
// 废弃alloc分配的内存块
实际上苹果是通过散列表的形式实现引用计数的,散列表的键值为内存块地址的散列值。
散列表存储信息为引用计数以及内存块地址。
autorelease会类似c语言中的自动变量来对待对象实例,超出作用域时则将对象释放。
{
int a;
}
autorelease就是类似 { }
的作用,使用方法如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; //等同于[obj release]
在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool 对象进行生成、持有和废弃处理。因此开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。
但是在大量产生autorelease对象时,只有不废弃NSAutoreleasePool对象,那么生成的对象就不能被释放,因此可能出现内存不足的情况。例如:
for (int i = 0; i < 图像数; ++i)
{
/*
* 读入图像
* 大量产生autorelease对象
* 没有废弃NSAutoreleasePool 对象
* 导致最终内存不足
*/
[pool drain];
/*
* 通过drain方法autorelease对象被release
*/
}
Cocoa框架中也有很多类方法返回autorelease对象,比如NSMutableArray类的arrayWithCapacity类方法。
id array = [NSMutableArray arrayWithCapacity:1];
等同于
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
[obj autorelease]
-(id)autorelease
{
[NSAutoreleasePool addObject:self];
}
+(void)addObject:(id)anObj
{
NSAutorelease *pool = 取得正在使用的NSAutorelease对象;
if (pool != nil)
{
[pool addObject:anObj];
}
else
{
NSLog(@"AutoreleasePool 对象非存在状态下调用");
}
}
如果嵌套生成或持有多个NSAutoreleasePool对象,则会使用最内侧的对象。
[pool drain]
- (void)drain
{
[self dealloc];
}
- (void) dealloc
{
[self emptyPool];
[array release];
}
- (void) emtpyPool
{
for (id obj in array)
{
[obj release];
}
}
与MRC的引用计数式内存管理在本质上没有太大变化,只是自动帮我们处理引用计数的部分。
通过__strong修饰符可以不必再次键入retain或者release,自动地实现了上述的四种内存管理思考方式。
但引用计数式内存管理会发生"循环引用"的问题。
{
id test0 = [[Test alloc] init]; /* 对象A */
/*
* test0 持有Test对象A的强引用
*/
id test1 = [[Test alloc] init]; /* 对象B */
/*
* test1 持有Test对象B的强引用
*/
[test0 setObject:test1];
/*
* 此时持有Test对象B的强引用变量为test1和对象A的obj_成员变量
*/
[test1 setObject:test0];
/*
* 此时持有Test对象A的强引用变量为test0和对象B的_obj变量
*/
}
/*
* 因为test0变量超出其作用域,强引用失效,所以自动释放对象A
*
* 因为test1变量超出其作用域,强引用失效,所以自动释放对象B
*
* 此时持有对象A的强引用变量为对象B的_obj,持有对象B的强引用变量为对象A的_obj
*
* 发生内存泄漏!(应当废弃的对象在超出其生命周期后依然存在)
*/
同时像下面的情况,虽然只有一个对象,但对象持有其自身时也会发生循环引用。
id test = [[Test alloc] init];
[test setObject:test];
使用弱引用__weak可以实现,弱引用不持有对象,只是持有该对象的强引用,如果该对象的所有强持有者都释放,则对象废弃,即使此时仍有弱持有者。
_unsafe_unretained 和weak的不同之处在于前者在赋值给带有strong修饰符的变量必须确保被赋值对象确实存在,不然其不会像weak一样被设置为nil而是造成悬浮指针。(存在是历史遗留问题,有了weak后基本不用它了)
strong修饰符类似于c++中的std::shared_ptr指针,而weak修饰符类似于c++中的std::weak_ptr,在c++中没有strong,weak强烈推荐使用这两个指针。
说了这么多总结一下ARC,MRC的区别吧。MRC类似于c++中的普通指针,程序猿要手动的进行生成持有释放废弃操作,但是可以将其注册到autoreleasepool中(类似c++的作用域),在作用域消失时也就是autoreleasepool对象释放时会释放所有注册在它里面的对象。而ARC则是类似于c++的智能指针,不需要显示的对对象实例进行释放,出现了strong,weak等修饰符,采用自动引用计数的方法,使得当一个对象实例在强引用计数为0时,则废弃这个对象实例,释放其所占的内存块。
属性声明 | 所有权修饰符 |
---|---|
assign | _unsafe__unretained修饰符 |
copy | __strong修饰符 |
retain | __strong修饰符 |
strong | __strong修饰符 |
unsafe_unretained | _unsafe__unretained修饰符 |
weak | __weak修饰符 |
**assign:**对应到__unsafe_unretained, 表明setter仅做赋值,不增加对象的引用计数,用于基本数据类型
__weak修饰符赋值后不会注册到autoreleasepool中,只会在使用时注册,而且每次使用都会注册一次,在autoreleasepool结束后会全部释放。
**copy:**对应到__strong,但是赋值操作比较特殊:赋值时进行copy而非retain操作,原来的值可变则深拷贝,不可变则浅拷贝。
先看如下一段代码:
{
// ARC中默认会在对象前添加一个修饰符__strong
id obj = [[NSObject alloc] init];
//<==>等价于
id __strong obj = [[NSObject alloc] init];
}
根据runtime特性,它的实际调用如下:
{
// 消息转发
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 编译器在obj作用域结束时自动插入release
objc_release(obj);
}
当然这里是以alloc/new/copy/mutableCopy
生成的对象,这种对象会被当前的变量所持有,引用计数会加1.那如果不是用被持有的方式生成对象呢?
看下面这段代码:
{
id obj = [NSMutableArray array];
}
这种方式生成的对象不会被obj持有,通常情况下会被注册到autoreleasepool
中.但也有特殊情况,上面的代码可以转换成如下代码:
{
// 消息转发
id obj = objc_msgSend(NSMutableArray,@selector(array));
// 调用objc_retainAutoreleasedReturnValue函数
objc_retainAutoreleasedReturnValue(obj);
// 编译器在obj作用域结束时自动插入release
objc_release(obj);
}
这里介绍两个相关函数:
autoreleasepool
当中的对象.objc_retainAutoreleasedReturnValue()
成对出现的.目的是注册对象到autoreleasepool
中.但不仅限于此
.不仅限于此
呢?原因在于,objc_retainAutoreleaseReturnValue()
函数在发现对象调用了方法或者函数之后又调用了objc_retainAutoreleasedReturnValue()
,那么就不会再把返回的对象注册到autoreleasepool
中了,而是直接把对象传递过去.autoreleasepool
中取出对象,传递出去,而是越过autoreleasepool
直接传递,提升了性能.weak
修饰符想必大家都非常熟悉,它有一个众所周知的特性:用weak修饰的对象在销毁后会被自动置为nil
.另外还补充一点:凡是用weak修饰过的对象,必定是注册到autoreleasepool中的对象
.
看下面的代码:
{
// obj默认有__strong修饰
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}
实际过程如下:
{
// 省略obj的实现
id obj1;
// 通过objc_initWeak初始化变量
objc_initWeak(&obj1,obj);
// 通过objc_destroyWeak释放变量
objc_destroyWeak(&obj1);
}
objc_initWeak()
函数的作用是将obj1初始化为0,然后将obj作为参数传递到这个函数中objc_storeWeak(&obj1,obj)
objc_destroyWeak()
函数则将0作为参数来调用:objc_storeWeak(&obj1,0)
objc_storeWeak()
函数的作用是以第二个参数(obj || 0)
作为key,第一个参数(&obj1)
作为value,将第一个参数的地址注册到weak表中.当key为0,即从weak表中删除变量地址.那么weak表中的对象是如何被释放的呢?
这就是__weak修饰的变量会在释放后自动置为nil的原因.同时,因为weak修饰之后涉及到注册到weak表等相关操作,如果大量使用weak可能会造成不必要的CPU资源浪费,所以书里指出尽量在循环引用中使用weak
.
这里不得不提到另外一个和__weak相近的属性:__unsafe_unretained
,它与weak的区别在于,释放对象后不会对其置为nil,在某些特定的场合下,需要延迟释放的时候,可以考虑用这个属性修饰.
好了,下一个问题,看如下代码:
{
id __weak obj1 = obj;
// 这里使用了obj1这个用weak修饰的变量
NSLog(@"%@",obj1);
}
在weak变量被使用的情况下,实际过程如下:
{
id obj1;
objc_initWeak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);
}
从这段实现代码中我们可以看出如下几点:
tmp
对象,因为objc_loadWeakRetained()
函数会从weak表中取出weak修饰的对象,所以tmp会对这个取出的对象进行一次强引用.objc_autorelease()
会将tmp对象也注册到autoreleasepool
中.所以当大量使用weak对象的时候,注册到autoreleasepool
的对象会大量增加.解决方案就是用一个__strong修饰的临时变量来使用.{
id __weak obj1 = obj;
id tmp = obj1;
// 后面使用tmp即可
}
延伸一下:为什么有循环引用block内用weakObject的时候最好能在block内套一层strongObject?
!!! 为什么访问weak修饰的对象就会访问注册到自动释放池的对象呢?
因为weak不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在autoreleasePool销毁时释放对象占用的内存。
它的主要作用就是将对象注册到autoreleasepool
中.没啥好说的.
最后补充几种在ARC环境下获取引用计数的方法,但并不一定准确:ARC的一些引用计数优化,以及多线程的中的竞态条件问题,有兴趣的可以自己去了解一下
.
(1) 使用_objc_rootRetainCount()私有函数
OBJC_EXTERN int _objc_rootRetainCount(id);
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",_objc_rootRetainCount(obj));
}
@end
(2) 使用KVC
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",[[obj valueForKey:@"retainCount"] integerValue]);
}
@end
(3) 使用CFGetRetainCount()
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",CFGetRetainCount((__bridge CFTypeRef)(obj)));
}
@end