ARC原理

ARC 在编译期插入生命周期的代码,内存管理的方式和MRC一样。
编译器会在合适的地方插入 retain release autorelease delloc 代码来管理对象的生命周期

使用ARC必须遵守的规则:

  1. 不能使用 retain/release/retainCount/autorelease
  2. 不能使用 NSAllocateObject/NSDeallocateObject
  3. 必须遵守内存管理的方法命名规则
  4. 不要显示调用 dealloc
  5. 使用@autorelease块代替NSAutoreleasePool
  6. 不能使用区域(NSZone)
  7. 对象型变量不能作为C语言结构体的成员
  8. 显式转换id和void*

Apple文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- You cannot explicitly invoke dealloc, or implement or invoke retain, release, retainCount, or autorelease.
The prohibition extends to using @selector(retain), @selector(release), and so on.
You may implement a dealloc method if you need to manage resources other than releasing instance variables. You do not have to (indeed you cannot) release instance variables, but you may need to invoke [systemClassInstance setDelegate:nil] on system classes and other code that isn’t compiled using ARC.
Custom dealloc methods in ARC do not require a call to [super dealloc] (it actually results in a compiler error). The chaining to super is automated and enforced by the compiler.
You can still use CFRetain, CFRelease, and other related functions with Core Foundation-style objects (see Managing Toll-Free Bridging).
- You cannot use NSAllocateObject or NSDeallocateObject.
You create objects using alloc; the runtime takes care of deallocating objects.
- You cannot use object pointers in C structures.
Rather than using a struct, you can create an Objective-C class to manage the data instead.
- There is no casual casting between id and void *.
You must use special casts that tell the compiler about object lifetime. You need to do this to cast between Objective-C objects and Core Foundation types that you pass as function arguments. For more details, see Managing Toll-Free Bridging.
- You cannot use NSAutoreleasePool objects.
ARC provides @autoreleasepool blocks instead. These have an advantage of being more efficient than NSAutoreleasePool.
- You cannot use memory zones.
There is no need to use NSZone any more—they are ignored by the modern Objective-C runtime anyway.
To allow interoperation with manual retain-release code, ARC imposes a constraint on method naming:
- You cannot give an accessor a name that begins with new. This in turn means that you can’t, for example, declare a property whose name begins with new unless you specify a different getter:

1
2
3
4
5
// Won't work:
@property NSString *newTitle;
// Works:
@property (getter=theNewTitle) NSString *newTitle;

属性关键字与所有权修饰符

ARC 引入了几种生命周期修饰符和 weak 引用,unsafe_unretained strong autoreleasing weak
生命周期修饰符是ARC 引入的新特性,代码中属性 局部变量 全局变量 参数否会用到,属性关键字最终也是映射到这几种所有权修饰符上

属性关键字 所有权修饰符
assign __unsafe_unretained
copy __strong
retain __strong
strong __strong
__unsafe_unretained __unsafe_unretained
weak __weak

关于他们的官方介绍:

1
2
3
4
__strong is the default. An object remains “alive” as long as there is a strong pointer to it.
__weak specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.
__unsafe_unretained specifies a reference that does not keep the referenced object alive and is not set to nil when there are no strong references to the object. If the object it references is deallocated, the pointer is left dangling.
__autoreleasing is used to denote arguments that are passed by reference (id *) and are autoreleased on return.

所有权修饰符的规范写法:ClassName * qualifier variableName;

outlets 应该使用 weak,官方解释:

1
2
3
outlets should be weak, except for those from File’s Owner to top-level objects in a nib file (or a storyboard scene) which should be strong.
Full details are given in Nib Files in Resource Programming Guide.

ARC 下使用 strong/weak/autoreleasing 修饰变量,变量会隐式初始化为 nil

不支持weak弱引用的类:NSATSTypesetter, NSColorSpace, NSFont, NSMenuView, NSParagraphStyle, NSSimpleHorizontalTypesetter, and NSTextView.
在ARC下 不能使用weak变量指向 NSHashTable, NSMapTable, or NSPointerArray

内存管理思想:
自己生成的对象自己持有 alloc new copy mutableCopy
非自己生成的对象,自己也能持有
不再需要自己持有的对象时释放对象
非自己持有的对象无法释放

实现原理

alloc

1
2
3
4
+alloc
+allocWithZone:
class_createInstance//生成实例
calloc//分配内存块

retainCount

1
2
__CFdoExternRefOperation
CFBasicHashGetCountOfKey

retain

1
2
__CFdoExternRefOperation
CFBasicHashAddValue

release

1
2
__CFdoExternRefOperation
CFBasicHashRemoveValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
猜想实现
- (NSUInteger)retainCount
{
return (NSUInteger)____CFDoExternRefOperation(OPERATION_retainCount,self);
}
- (id)retain
{
return (id)____CFDoExternRefOperation(OPERATION_retain,self);
}
//这里返回值应该是id,原书这里应该是错了
- (id)release
{
return (id)____CFDoExternRefOperation(OPERATION_release,self);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对象的散列表(obj);
int count;
switch (op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
break;
case OPERATION_retain:
count = CFBasicHashAddValue(table, obj);
return obj;
case OPERATION_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}

autorelease 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
objc4/NSObject.mm AutoreleasePoolPage
class AutoreleasePoolPage
{
static inline void *push()
{
//生成或者持有 NSAutoreleasePool 类对象
}
static inline void pop(void *token)
{
//废弃 NSAutoreleasePool 类对象
releaseAll();
}
static inline id autorelease(id obj)
{
//相当于 NSAutoreleasePool 类的 addObject 类方法
AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例;
autoreleaesPoolPage->add(obj)
}
id *add(id obj)
{
//将对象追加到内部数组中
}
void releaseAll()
{
//调用内部数组中对象的 release 方法
}
};
//压栈
void *objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}
//出栈
void objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
AutoreleasePoolPage::pop(ctxt);
}

###苹果是如何实现autoreleasepool的?
autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.
• objc_autoreleasepoolPush(压入)
• objc_autoreleasepoolPop(弹出)
• objc_autorelease(释放内部)

__strong内部实现:

1
2
3
{
id __strong obj = [NSObject alloc] init];//obj持有对象
}

等同于:

1
2
id __strong obj = [NSObject alloc] init];//obj持有对象
[obj release];

使用命名规则以外的构造方法

1
2
3
{
id __strong array = [NSMutableArray array];
}

等同于:

1
2
3
4
5
6
7
8
9
10
id obj = [NSMutableArray array];
//obj 对 autoreleasePool 内的 array 对象引用加一
[obj release];
+ (id)array
{
return [[[NSMutableArray alloc] init] autorelease];
}

ARC下,runtime有一套对autorelease返回值的优化策略。
=>

1
2
3
4
5
6
7
8
9
10
{
id tmp = objc_retainAutoreleasedReturnValue([NSArray array]); // 代替我们调用retain
id obj = tmp;
objc_storeStrong(&obj,nil); // 相当于代替我们调用了release
}
+ (id)array
{
id tmp = [NSArray array];
return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}

实际代码中,苹果会进行优化

objc_retainAutoreleasedReturnValue的作用:持有对象,将对象注册到autoreleasepool并返回
objc_autoreleaseReturnValue:返回注册到autoreleasepool的对象

__weak 实现

weak 不影响引用计数,可以打破循环引用
特点:
● 若使用__weak修饰符的变量引用对象被废弃时,则将nil赋值给该变量

● 使用附有__weak修饰符的变量,就是使用注册到autoreleasepool的对象。

weak 变量加入 autoreleasePool 的原因:__weak修饰符支持有对象的弱引用,在访问引用对象的过程中,该对象可能被释放。而如果将该对象加入到autoreleasepool中,在pool被释放之前,tmp对该对象的引用都是有效的。

1
2
3
4
id obj = [Obj new];
{
id __weak weakObj = obj;
}

编译器模拟代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id obj = [Obj new];
{
id weakObject;
//初始化 weak 指针
objc_initWeak(&weakObject,obj);
/* objc_initWeak 方法做了什么
{
weakObject = 0;
objc_storeWeak(&weakObj,obj);
}
*/
//取出附有__weak修饰符变量所引用的对象并 retain
id temp = objc_loadWeakRetained(&weakObject);
//将对象注册到autorelease中
object_autorelease(temp);
objc_destroyWeak(&weakObject);
/* objc_destroyWeak 方法做了什么
{
objc_storeWeak(&obj,0);
}
*/
}

实际上,objc_storeWeak函数会把第二个参数的对象的地址作为key,并将第一个参数(weak关键字修饰的指针的地址)作为值,注册到weak表中。如果第二个参数为0(说明weak 变量超出了作用域),直接让 weakObject 变量置为 nil,weak关键字的核心思想!如果 obj release 释放 则通过 dealloc 调用的 objc_clear_deallocating 函数。

释放对象时,废弃对象的同时,程序的动作是怎样的呢?对象通过objc_release释放。

  1. objc_release

  2. 因为引用计数为0所以执行dealloc

  3. _objc_rootDealloc

  4. object_dispose

  5. objc_destructInstance

  6. objc_clear_deallocating

而,调用objc_clear_deallocating的动作如下:

  1. 从weak表中获取废弃对象的地址为键值的记录。

  2. 将包含在记录中的所有附有__weak修饰符变量的地址,赋值为nil

  3. 从weak表中删除记录

  4. 从引用计数表中删除废弃对象的地址作为键值的记录

autorelease 的实现

ARC 下 @autoreleasepool{} 的实现

1
2
3
4
5
id pool = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(pool);
// pool 指当前 autoreleasepool 哨兵对象的地址

objc_autoreleasePoolPush 与 objc_autoreleasePoolPop 都是对 AutoreleasePoolPage 的封装。

AutoreleasepoolPage c++ 类

1
2
3
4
5
6
7
magic_t const magic;
id * next; //新对象将被加入的地址
pthread_t const thread; //当前线程
AutoreleasePoolPage * const parent; //指向上一个 page
AutoreleasePoolPage *child; //指向下一个 page
uint32_t const depth;
uint32_t hiwat;
  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

当前page页加入的autorelease对象后满了(也就是next指针指向了end),这时就要执行建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

释放

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil)
objc_autoreleasePoolPush 的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

根据传入的哨兵对象地址找到哨兵对象所处的page
在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

嵌套

知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

##参考
官方资料
其他参考资料:iOS 内存管理高级编程
Objective-C 高级编程
可能是史上最全面的内存管理文章
可能碰到的iOS笔试面试题(6)–内存管理