内存管理

98 阅读7分钟

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 一样

../Art/ARC_Illustration.jpg

示例代码片段

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代码

内存管理模型基于对象所有权。任何对象都可以具有一个或多个所有者。只要对象至少有一个所有者,它就会继续存在。如果对象没有所有者,则运行时系统会自动销毁该对象

../Art/memory_management_2x.png

在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))

<未完待续。。。>