ARC原理探究

探究ARC的原理,搞清楚开发中遇到的几个问题,1.performSelector的内存泄露,2.对象的指针的定义,3. getReturnValue的返回值声明。

2017-05-06 | 阅读

探究ARC

ARC即OC的自动引用计数技术,通过在编译阶段自动添加引用计数,达到自动管理引用计数的目的。使用ARC可以做到接近垃圾回收的代码编写体验,同时拥有引用计数的性能与效率。

参考资料:OBJECTIVE-C AUTOMATIC REFERENCE COUNTING (ARC)

分析方式,通过clang将代码编译,分析llvm的中间语言,通过以下命令将代码编译成中间语言:

clang -S -fobjc-arc -emit-llvm main.m -o main.ll

自动添加release

对于一段代码中有声明强引用的对象,如 :

main(){
	id a;
}

我们对这段代码进行编译,结果为:

; Function Attrs: nounwind ssp uwtable
define i32 @main(i32, i8**) #3 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca i8*, align 8
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  store i8* null, i8** %6, align 8
  store i32 0, i32* %3, align 4
  call void @objc_storeStrong(i8** %6, i8* null) #4
  %7 = load i32, i32* %3, align 4
  ret i32 %7
}

alloca函数申请内存地址,而store表示将值存到指定地址。 函数的最后调用了函数objc_storeStrong,我们查阅clang文档得知这个函数的实现如下:

void objc_storeStrong(id *object, id value) {
  id oldValue = *object;
  value = [value retain];
  *object = value;
  [oldValue release];
}

分析代码,这里传入的object&a,而valuenull,所以这个函数实际操作为:对null进行了retain,而对a进行了release。即释放了a对象。

这里我们可以总结,在__strong类型的变量的作用域结束时,自动添加release函数进行释放。

自动添加retain

然后研究赋值语句的实现:

id a;
__strong id b = a;

这里编译的结果为 :

define i32 @main(i32, i8**) #3 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca i8*, align 8
  %7 = alloca i8*, align 8
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  store i8* null, i8** %6, align 8
  %8 = load i8*, i8** %6, align 8
  %9 = call i8* @objc_retain(i8* %8) #4
  store i8* %9, i8** %7, align 8
  store i32 0, i32* %3, align 4
  call void @objc_storeStrong(i8** %7, i8* null) #4
  call void @objc_storeStrong(i8** %6, i8* null) #4
  %10 = load i32, i32* %3, align 4
  ret i32 %10
}

最后调用了两个objc_storeStrong进行release操作。而其中赋值操作之前被arc添加了函数objc_retain,这个函数如其名称所示,实现就是进行retain

如果我们将b变量的声明改为__autoreleasing,我们会发现编译结果中与上述不同的地方如下:

%9 = call i8* @objc_retainAutorelease(i8* %8) #4

在赋值之前,调用了函数objc_retainAutorelease,这个函数的实现为:

id objc_retainAutorelease(id value) {
  return objc_autorelease(objc_retain(value));
}

即对一个变量先进行一次retain,再添进行autorelease

如果我们将变量b的声明改为__weak,我们会发现编译结果中与上述不同的地方如下:

%9 = call i8* @objc_initWeak(i8** %7, i8* %8) #4
store i32 0, i32* %3, align 4
call void @objc_destroyWeak(i8** %7) #4

在为weak对象赋值时,调用objc_initWeak函数,而在weak对象超过作用域时,使用objc_destroyWeak进行释放。

最后,我们将变量b的声明改为__unsafe_unretained,会发现编译结果中,只有store对指针进行赋值,并没有其他相关函数的添加,所以unsafe_unretained只是单纯的保存指针,不考虑引用计数相关的内存管理问题。

ARC会自动的在赋值语句之前执行一些 引用计数相关的函数,这也就是ARC实现的主要原理。

retain和release的优化

ARC对于以new,copy,mutableCopyalloc以及 以这四个单词开头的所有函数,默认认为函数返回值直接持有对象。这是ARC中必须要遵守的命名规则。即对于函数 :

+ (instancetype)creatO {
	id a = [[self alloc] init];
	return a;
}
+ (instancetype)newO {
	id a = [[self alloc] init];
	return a;
}

在函数creatO中,函数的返回的对象最后一步会自动添加上autorelease。而在函数newO中,返回的结果就是不带有autorelease,是直接持有的对象。

同样,在指针赋值操作上,两者也不同:

int main(){
	id a = [Hello newO];
	id b = [Hello creatO];
}

这里,赋值前不会对[Hello newO]进行操作,因为外面是一个strong的指针,而返回的对象已经持有引用计数的。

而对[Hello creatO]的返回值需要retain,因为函数返回的对象进行了一次retainautorelease后,引用计数为0,所以需要进行持有操作。

而ARC对于id a = [Hello creatO];这个赋值操作进行了优化,对函数返回处的autorelease和赋值处的retain进行了优化,对于返回值,使用objc_autoreleaseReturnValue函数,对于赋值时使用objc_retainAutoreleasedReturnValue函数。

objc_autoreleaseReturnValue处理函数返回值,如果在此次调用堆栈后面对这个函数操作的对象执行了objc_retainAutoreleasedReturnValue函数,则这里会跳过autorelease操作,否则执行autorelease操作。

objc_retainAutoreleasedReturnValue在赋值处持有返回对象,如果调用堆栈前面有进行objc_autoreleaseReturnValue的标记,则跳过retain操作,否则执行retain操作。

ARC还对内存调用函数进行了优化,即ARC相关的函数不通过Objective-C的消息派发机制,而是直接调用底层的C函数。而且ARC是在编译器自动添加引用计数函数调用,而不是运行时判断。综上所示,因为这些原因,所以ARC性能要优于手动引用计数。

weak实现

weak指针的实现借助Objective-C的运行时特性,runtime通过 objc_storeWeak, objc_destroyWeakobjc_moveWeak等方法,直接修改__weak对象,来实现弱引用。

objc_storeWeak函数,将附有__weak标识符的变量的地址注册到weak表中,weak表是一份与引用计数表相似的散列表。

而该变量会在释放的过程中清理weak表中的引用,变量释放调用以下函数:

  1. dealloc
  2. _objec_rootDealloc
  3. object_dispose
  4. objc_destructInstance
  5. objc_clear_deallocating

在最后的objc_clear_deallocating函数中,从weak表中找到弱引用指针的地址,然后置为nil,并从weak表删除记录。

performSelector内存泄露

当我们直接使用performSelector:执行一个传入的SEL时,编译器会抛出异常

performSelector may cause a leak because its selector is unknown 

现在我们了解ARC的原理后,就可以解释原因了。

对于函数performSelector:,其返回值是id,对于以下函数:

Hello *b;
b = [a performSelector:sel];

我们知道b会对performSelector:返回的结果调用retain操作,在b对象离开作用域时进行一次release操作。

而如果selector是以 new,copy,mutableCopyalloc开头的,则返回的对象是带有一个引用计数的,则在调用函数处进行了一次retainrelease后,该对象还是拥有一个引用计数,在ARC下就发生了内存泄露。

注意NSInvocation的返回值

使用NSInvocation时,我们通过- (void)getReturnValue:(void *)retLoc;获取返回值。但是观察函数声明和函数描述,苹果说,这个函数由于不知道返回值的类型,所以只进行指针赋值,不进行对象的内存管理操作。所以结合ARC时我们就要考虑如何避免内存问题。

Hello *a = [[Hello alloc] init];
SEL sel = @selector(newO);
NSMethodSignature *signature = [Hello methodSignatureForSelector:sel];
NSInvocation *invoc = [NSInvocation invocationWithMethodSignature:signature];
__strong id returnValue;
invoc.selector = sel;
invoc.target = a;
[invoc invoke];
[invoc getReturnValue:&returnValue];

首先当被调用函数是以newcopy,mutableCopyalloc开头的特殊函数时,函数返回的的对象持有引用计数,所以我们设置returnValue的类型是__strong,这样在这个returnValue的作用域结束时,会进行release,内存处理正常。

当被调用函数是普通函数时,函数内部最后执行了autorelease导致引用计数为0时。所以我们一定要设置returnValue的类型为

__autoreleasing id returnValue;

因为如果设置为__strong,则会在returnValue的作用域结束时,对这个引用计数为0的对象再进行一次release,导致内存问题。