ARC
自动引用计数(ARC)是苹果在 LLVM 编译器中引入的一项重要特性,它在保留 Objective-C 手动内存管理模型灵活性的基础上,自动插入 retain / release 等引用计数相关操作,从而极大减轻了开发者的内存管理负担。
但 ARC 并不是魔法。理解它的底层机制,可以帮助我们写出更高性能、更安全的 Objective-C 代码。本文将围绕编译器视角,深入剖析 ARC 的原理与实战技巧。
ARC 是什么?
ARC 是在 编译阶段 由 Clang 插入引用计数函数调用的系统。也就是说,ARC 并不是运行时的 GC(垃圾回收器),而是静态地将引用计数操作编译进目标代码中。
id obj = [[NSObject alloc] init]; // ARC 编译器自动变成:
id tmp = objc_retain([[NSObject alloc] init]);
objc_storeStrong(&obj, tmp);
这些 objc_ 开头的函数由 runtime 提供,行为和手动调用 retain / release 一样
示例代码片段
void foo (void) { NSObject *obj = [NSObject new]; NSObject *objOther = obj;}
下面验证是编译阶段插入的代码
LLVM IR
clang -fobjc-arc -S -emit-llvm main.m -o main.ll
; Function Attrs: noinline optnone ssp uwtable
define void @foo() #1 {
%1 = alloca ptr, align 8
%2 = alloca ptr, align 8
%3 = load ptr, ptr @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%4 = load ptr, ptr @OBJC_SELECTOR_REFERENCES_.38, align 8, !invariant.load !11
%5 = call ptr @objc_msgSend(ptr noundef %3, ptr noundef %4)
store ptr %5, ptr %1, align 8
%6 = load ptr, ptr %1, align 8
%7 = call ptr @llvm.objc.retain(ptr %6) #3
store ptr %7, ptr %2, align 8
call void @llvm.objc.storeStrong(ptr %2, ptr null) #3
call void @llvm.objc.storeStrong(ptr %1, ptr null) #3
ret void
}
可以看出Clang生成的IR代码中含有对应的ARC代码
@llvm.objc.retain ==> objc_retain
@llvm.objc.storeStrong ==> objc_storeStrong
voidobjc_storeStrong(id *location, id obj){ id prev = *location; if (obj == prev) { return; } objc_retain(obj); *location = obj; objc_release(prev);}
汇编
clang -fobjc-arc -S main.m -o main.s
_foo: ## @foo .cfi_startproc## %bb.0: pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp subq $16, %rsp movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi movq _OBJC_SELECTOR_REFERENCES_.38(%rip), %rsi callq *_objc_msgSend@GOTPCREL(%rip) movq %rax, -8(%rbp) movq -8(%rbp), %rdi callq *_objc_retain@GOTPCREL(%rip) movq %rax, -16(%rbp) leaq -16(%rbp), %rdi xorl %eax, %eax movl %eax, %esi callq _objc_storeStrong leaq -8(%rbp), %rdi xorl %eax, %eax movl %eax, %esi callq _objc_storeStrong addq $16, %rsp popq %rbp retq .cfi_endproc
汇编中同样也可以看出对应的ARC代码
内存管理模型基于对象所有权。任何对象都可以具有一个或多个所有者。只要对象至少有一个所有者,它就会继续存在。如果对象没有所有者,则运行时系统会自动销毁该对象
在ARC中持有对象的指针类型有三种
1. block指针
2. Objective-C 对象指针
3. 标有 __attribute__((NSObject))
举例: 使用 __attribute__((NSObject)) 修饰 CFTypeRef
typedef CFStringRef MyCFString __attribute__((NSObject));
@interface MyClass : NSObject
@property (nonatomic, strong) MyCFString cfString; // ARC 会自动管理内存
其他类型的指针不在ARC管辖范围,如int*,CFStringRef.
初识对象的创建
int main(int argc, char * argv[]) { [CalleeObject new];
return 0;
}
思考仅仅调用对象的构建方法,创建后的CalleeObject对象实例会在什么时机销毁?
编译器识别到没有对象指针持有,会自动调用release 进而销毁对象,也就是对象创建之后立即被销毁
参数中的ARC
@implementation CallerObject
- (void) foo: (id) x { NSLog(@"Callee internal method content:%@",x);}
@end
int main (int argc, const char * argv[]) {
CallerObject *caller = [CallerObject new];
[caller foo:[CalleeObject new]];
return 0;
}
思考一个问题,caller在调用foo:的过程中[CalleeObject new] 并没有指针对象持有
[caller foo:[CalleeObject new]];
如果按照ARC的规则在[CalleeObject new]执行之后,这个没有被持有的对象会自动销毁才对,如何传递到foo:方法内部的
Consumed parameters
__attribute((ns_consumed))
这个标记可以使用函数、方法和对象指针,表示被调用方预期获得的对象引用计数+1
- (void) foo: (id) __attribute((ns_consumed)) x;
这个属性标记是函数或方法类型的一部分,而不是参数类型的一部分。它仅控制参数的传递和接收方式。
传递此类参数时,ARC 会在进行调用之前保留该参数。
当收到这样的参数时,ARC 会在函数的末尾释放该参数
验证
下面通过汇编调试来观察这个过程
方法设置断点
(lldb) breakpoint set -n foo:Breakpoint 15: where = MemoryManagement`-[CallerObject foo:] + 56 at main.m:22:1, address = 0x0000000100ed4038
断点越过了前面的15行代码,这个与预期不一致。而且此时观察x的引用技术已经为2了。
重新执行,此次我们把断点打在调用处。
在调用之前 [CalleeObject new]的初始化过程
(lldb) register read sp
sp = 0x000000016fdc3420
(lldb) x 0x000000016fdc3420
0x16fdc3420: 00 03 00 00 00 00 00 00 b0 80 c4 81 02 00 00 00 ................
0x16fdc3430: 00 c0 c4 81 02 00 00 00 00 01 00 00 00 00 00 00 ................
(lldb) x 0x0281c4c000
0x281c4c000: 89 d6 04 00 01 00 00 01 00 00 00 00 00 00 00 00 ................
0x281c4c010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(lldb) register x2
在lldb中通过运行时获取方法的地址, 设置地址断点
(lldb) po class_getInstanceMethod(CallerObject.class , @selector(foo:))
0x000000010004c448
(lldb) x 0x000000010004c448
0x10004c448: 21 a3 04 00 01 00 00 00 6c a4 04 00 01 00 00 00 !.......l.......
0x10004c458: 60 40 04 00 01 00 00 00 80 00 00 00 08 00 00 00 `@..............
(lldb) breakpoint set -address 0x0100044060
Breakpoint 24: where = MemoryManagement`-[CallerObject foo:] at main.m:28, address = 0x0000000100044060
查看调用参数,获取到入参x的地址
(lldb) register read
General Purpose Registers:
x0 = 0x0000000281c480b0
x1 = 0x000000010004a321 "foo:"
x2 = 0x0000000281c4c000
(lldb) x 0x0000000281c4c000
0x281c4c000: 89 d6 04 00 01 00 00 01 00 00 00 00 00 00 00 00
0x281c4c010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
此时x的引用计算还是1.没问题继续分析
观察第6、10、14行
伪代码
CalleeObject *tmp = nil; tmp = x; // objc_storeStrong(tmp, x)
这里执行对x retain,对nil release,oc中对ni 执行方法是安全的,c方法里面也做了防护
__attribute__((always_inline))static void _objc_release(id _Nullable obj) { if (_objc_isTaggedPointerOrNil(obj)) return; return obj->release();}
此时x的引用计算为2 与首次符号断点的结果一致。至此观察到了ARC在方法头插入一段调整引用逻辑的代码。
有增加就会有减少来维持平衡。继续分析,方法尾部
方法的尾部又对x减持,CalleeObject *tmp = nil;
跳出方法,再看调用方的后续
又对作为参数的x做了一次release操作,引用计数归零,被析构,到此完成了参数生命周期的完整分析。
一般arc中的对象指针作为参数时默认方法或函数被标记__attribute((ns_consumed))