[iOS]关于ARC实现的一些总结

最近在重温《Objective-C 高级编程》这本书,深深为这本薄书里蕴含的"惊人"能量所倾倒.本篇文章将总结一下ARC实现的一些细节.

ARC的概念

ARC翻译过来就是自动引用计数,相信大家都知道.苹果官方文档中说明:ARC是由由编译器进行内存管理的;本书指出了一个观点:实际上只有编译器是无法完全胜任的,在此基础上还需要Objective-C运行时库的协助.ARC的核心就是在对象需要释放的地方自动插入release.
说起ARC,就逃不过讨论几个修饰符:__strong,__weak,__autoreleasing和引用计数,下面一一来进行细节说明.

__strong

先看如下一段代码:

{
// 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);
}

这里介绍两个相关函数:

  • objc_retainAutoreleasedReturnValue():这个函数的作用是返回注册在autoreleasepool当中的对象.
  • objc_retainAutoreleaseReturnValue():这个函数一般是和objc_retainAutoreleasedReturnValue()成对出现的.目的是注册对象到autoreleasepool中.但不仅限于此.
    为何说不仅限于此呢?原因在于,objc_retainAutoreleaseReturnValue()函数在发现对象调用了方法或者函数之后又调用了objc_retainAutoreleasedReturnValue(),那么就不会再把返回的对象注册到autoreleasepool中了,而是直接把对象传递过去.
    这样的好处显而易见:不用再去autoreleasepool中取出对象,传递出去,而是越过autoreleasepool直接传递,提升了性能.

__weak

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表中获取废弃对象的键值记录.
  • 将记录中所有包含__weak的变量地址,赋值为nil.
  • 从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);
}

从这段实现代码中我们可以看出如下几点:

  • 当我们使用weak修饰的对象时,实际过程中产生了一个tmp对象,因为objc_loadWeakRetained()函数会从weak表中取出weak修饰的对象,所以tmp会对这个取出的对象进行一次强引用.
  • 因为上述原因,weak修饰的对象在当前变量作用域结束前都可以放心使用.
  • objc_autorelease()会将tmp对象也注册到autoreleasepool中.所以当大量使用weak对象的时候,注册到autoreleasepool的对象会大量增加.解决方案就是用一个__strong修饰的临时变量来使用.
{
id __weak obj1 = obj;
id tmp = obj1;
// 后面使用tmp即可
}

延伸一下:为什么有循环引用block内用weakObject的时候最好能在block内套一层strongObject?

  • 在异步线程中weakObject可能会被销毁,所以需要套一层strong.
  • 如果内部有耗时的循环语句,频繁使用weakObject也会增加内存损耗.

__autoreleasing

它的主要作用就是将对象注册到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

你可能感兴趣的:([iOS]关于ARC实现的一些总结)