最近在做皮肤适配时遇到了很多的坑,进而产生了很多的疑问。下面是想到了一些问题和最终得到的验证,内功太菜。之前一直在怀疑是他们都太飘了还是我拿不动刀了。现在看看内功太菜,确实是我打不动刀了。。。

之前看 Autorelease 时知道了底层是由 AutoreleasePoolPage 实现的,并且是由 RunLoop 来驱动的。那么的疑问就是我们在子线程中添加 Autorelease 时,这时没有 RunLoop,那么 autorelease 是怎样工作的呢?

最近遇到了一个疑问,之前看 Autorelease 时知道了底层是由 AutoreleasePoolPage 实现的,并且是由 RunLoop 来驱动的。那么的疑问就是我们在子线程中添加 Autorelease 时,这时没有 RunLoop,那么 autorelease 是怎样工作的呢?

在整理上面的疑问时还遇到了几个别的内存管理的问题:

  • ARC 到底是怎样管理内存的,平时只知道在合适的位置帮我们release,那么这个时机是在哪里?
  • weak、strong、autoreleasing 在 ARC 下的具体实现是怎样的?

ARC 内存管理原理

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

自动 release

来一段测试代码:

1
2
3
4
5
@implementation Person
- (void)test {
id a;
}
@end

使用 clang -S -fobjc-arc -emit-llvm Person.m -o person_arc.ll 命令来查看下生成的中间代码:

1
2
3
4
5
6
7
8
9
10
define internal void @"\01-[Person test]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8
%4 = alloca i8*, align 8
%5 = alloca i8*, align 8
store %0* %0, %0** %3, align 8
store i8* %1, i8** %4, align 8
store i8* null, i8** %5, align 8
call void @llvm.objc.storeStrong(i8** %5, i8* null) #2
ret void
}

alloca 是在当前执行的函数堆栈帧中分配内存。store 则表示将值存到指定地址。

关于 llvm 语法问题可以查看官方文档:https://llvm.org/docs/LangRef.html

这里有一个很重要的函数:objc.storeStrong(i8* %5, i8 null),来看下 objc 的源码:

1
2
3
4
5
6
7
8
9
10
11
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}

可以看到其操作就是将 obj 做一次 retain 操作,然后再将 location 指向 obj,最后将 location 做一次 release。

OK,objc.storeStrong(i8* %5, i8 null) 函数其实就是将 location 置空,并且是在函数作用域结束时做的。

自动 retain

再来加点测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)test {
id a;
__strong id b = a;
}

define internal void @"\01-[Person test]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8
%4 = alloca i8*, align 8
%5 = alloca i8*, align 8
%6 = alloca i8*, align 8
store %0* %0, %0** %3, align 8
store i8* %1, i8** %4, align 8
store i8* null, i8** %5, align 8
%7 = load i8*, i8** %5, align 8
%8 = call i8* @llvm.objc.retain(i8* %7) #1
store i8* %8, i8** %6, align 8
call void @llvm.objc.storeStrong(i8** %6, i8* null) #1
call void @llvm.objc.storeStrong(i8** %5, i8* null) #1
ret void
}

在给 b 指针赋值时调用了一次 retain。并在函数最后面调用了两次 objc.storeStrong。这里可以看到使用强指针会自动插入 retain 操作,而在作用域结束时会插入 release 操作。

再来试下其他修饰符:

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
44
45
46
/// __autoreleasing
define internal void @"\01-[Person test]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8
%4 = alloca i8*, align 8
%5 = alloca i8*, align 8
%6 = alloca i8*, align 8
store %0* %0, %0** %3, align 8
store i8* %1, i8** %4, align 8
store i8* null, i8** %5, align 8
%7 = load i8*, i8** %5, align 8
%8 = call i8* @llvm.objc.retainAutorelease(i8* %7) #1 // 注意这里
store i8* %8, i8** %6, align 8
call void @llvm.objc.storeStrong(i8** %5, i8* null) #1
ret void
}

/// __weak
define internal void @"\01-[Person test]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8
%4 = alloca i8*, align 8
%5 = alloca i8*, align 8
%6 = alloca i8*, align 8
store %0* %0, %0** %3, align 8
store i8* %1, i8** %4, align 8
store i8* null, i8** %5, align 8
%7 = load i8*, i8** %5, align 8
%8 = call i8* @llvm.objc.initWeak(i8** %6, i8* %7) #1
call void @llvm.objc.destroyWeak(i8** %6) #1
call void @llvm.objc.storeStrong(i8** %5, i8* null) #1
ret void
}

/// __unsafe_unretained
define internal void @"\01-[Person test]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8
%4 = alloca i8*, align 8
%5 = alloca i8*, align 8
%6 = alloca i8*, align 8
store %0* %0, %0** %3, align 8
store i8* %1, i8** %4, align 8
store i8* null, i8** %5, align 8
%7 = load i8*, i8** %5, align 8 // 注意这里是直接赋值
store i8* %7, i8** %6, align 8
call void @llvm.objc.storeStrong(i8** %5, i8* null) #1
ret void
}

__autoreleasing 其实其实就是调用 objc_retainAutorelease 方法:

1
2
3
4
5
/// 对 obj 做一次 retain 操作,然后加入自动释放池
id objc_retainAutorelease(id obj)
{
return objc_autorelease(objc_retain(obj));
}

__weak 是调用 objc_initWeak 对 weak 对象赋值,在作用域结束时调用 objc_destryWeak 进行释放。

__unsafe_unretained 则只是进行指针的赋值,并不考虑引用计数相关的问题。

综上我们可以看到,ARC 会自动的在赋值语句之前插入一些引用计数相关的函数,这就是 ARC 实现的主要原理。

对 retain、release 的一些优化

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

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
44
45
46
47
48
49
50
51
52
+ (instancetype)newPerson {
Person * p = [Person new];
return p;
}

+ (instancetype)createPerson {
Person * p = [Person new];
return p;
}

define internal i8* @"\01+[Person newPerson]"(i8*, i8*) #0 {
%3 = alloca i8*, align 8
%4 = alloca i8*, align 8
%5 = alloca %0*, align 8
store i8* %0, i8** %3, align 8
store i8* %1, i8** %4, align 8
%6 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%7 = bitcast %struct._class_t* %6 to i8*
%8 = call i8* @objc_opt_new(i8* %7)
%9 = bitcast i8* %8 to %0*
store %0* %9, %0** %5, align 8
%10 = load %0*, %0** %5, align 8
%11 = bitcast %0* %10 to i8*
%12 = call i8* @llvm.objc.retain(i8* %11) #1
%13 = bitcast i8* %12 to %0*
%14 = bitcast %0* %13 to i8*
%15 = bitcast %0** %5 to i8**
call void @llvm.objc.storeStrong(i8** %15, i8* null) #1
ret i8* %14
}

define internal i8* @"\01+[Person createPerson]"(i8*, i8*) #0 {
%3 = alloca i8*, align 8
%4 = alloca i8*, align 8
%5 = alloca %0*, align 8
store i8* %0, i8** %3, align 8
store i8* %1, i8** %4, align 8
%6 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%7 = bitcast %struct._class_t* %6 to i8*
%8 = call i8* @objc_opt_new(i8* %7)
%9 = bitcast i8* %8 to %0*
store %0* %9, %0** %5, align 8
%10 = load %0*, %0** %5, align 8
%11 = bitcast %0* %10 to i8*
%12 = call i8* @llvm.objc.retain(i8* %11) #1
%13 = bitcast i8* %12 to %0*
%14 = bitcast %0* %13 to i8*
%15 = bitcast %0** %5 to i8**
call void @llvm.objc.storeStrong(i8** %15, i8* null) #1
%16 = tail call i8* @llvm.objc.autoreleaseReturnValue(i8* %14) #1
ret i8* %16
}

在函数 newPerson 中,函数的返回值不带 autorelease,是直接持有对象。而函数 createPerson 中返回对象的最后一步会调用 autoreleaseReturnValue

再来看下使用赋值的时候:

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
- (void)test {
Person * a = [Person createPerson];
Person * b = [Person newPerson];
}

define internal void @"\01-[Person test]"(%0*, i8*) #1 {
%3 = alloca %0*, align 8
%4 = alloca i8*, align 8
%5 = alloca %0*, align 8
%6 = alloca %0*, align 8
store %0* %0, %0** %3, align 8
store i8* %1, i8** %4, align 8
%7 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%8 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
%9 = bitcast %struct._class_t* %7 to i8*
%10 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %9, i8* %8)
%11 = notail call i8* @llvm.objc.retainAutoreleasedReturnValue(i8* %10) #2
%12 = bitcast i8* %11 to %0*
store %0* %12, %0** %5, align 8
%13 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%14 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !9
%15 = bitcast %struct._class_t* %13 to i8*
%16 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %15, i8* %14)
%17 = bitcast i8* %16 to %0*
store %0* %17, %0** %6, align 8
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
%18 = bitcast %0** %6 to i8**
call void @llvm.objc.storeStrong(i8** %18, i8* null) #2
%19 = bitcast %0** %5 to i8**
call void @llvm.objc.storeStrong(i8** %19, i8* null) #2
ret void
}

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

而对 [Person createPerson] 的返回值需要 retain,因为函数对返回的对象进行了一次 autoreleaseReturnValue 操作,和前面的 retain 操作对应,正好达到引用计数器的加减平衡,所以外面的 strong 指针需要对返回值进行持有。

这里还有一个ARC 的优化,如果返回值使用了 objc_autoreleaseReturnValue函数,则在赋值时对应使用 objc_retainAutoreleasedReturnValue 函数。

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
44
45
// Prepare a value at +1 for return through a +0 autoreleasing convention.
id
objc_autoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

return objc_autorelease(obj);
}

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

return objc_retain(obj);
}

static ALWAYS_INLINE ReturnDisposition
acceptOptimizedReturn()
{
ReturnDisposition disposition = getReturnDisposition();
setReturnDisposition(ReturnAtPlus0); // reset to the unoptimized state
return disposition;
}

static ALWAYS_INLINE ReturnDisposition
getReturnDisposition()
{
return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);
}

static inline void *tls_get_direct(tls_key_t k)
{
ASSERT(is_valid_direct_key(k));

if (_pthread_has_direct_tsd()) {
return _pthread_getspecific_direct(k);
} else {
return pthread_getspecific(k);
}
}

extern void *pthread_getspecific(unsigned long);
extern int pthread_setspecific(unsigned long, const void *);

既然编译器已经知道了这么多,那么干嘛还要用 autorelease 这个开销不小的机制呢?从源码中也可以看到答案。这里苹果使用了一个黑魔法:Tread Local Storage (TLS) 线程局部存储,目的很简单,将一块内存作为某个线程的专有存储,以 key-value 的形式进行读写。

在返回值身上调用 autoreleaseReturnValue 方法时,runtime 将这个返回值对象存储在 TLS 中,然后直接返回这个值(不调用 autorelease);同时,外部接收这个值时调用 retainAutoreleasedReturnValue,如果发现 TLS 中有这个对象,则直接返回这个对象(不会 retain),可以看到调用方和被调用方很默契的利用 TLS 做中转,免去了对返回值的内存管理。

ARC 对内存调用函数进行了优化,即 ARC 相关的函数不通过 OC 的消息发送机制,而是直接调用底层的 C 函数,而且 ARC 是在编译阶段有编译器自动添加引用计数函数调用,而不是运行时判断。综上,ARC 性能要优于 MRC。

PerformSelector 问题

当我们调用 performSelector 时来看一个比较经典的警告:

PerformSelector may cause a leak because its selector is unknown

当我们了解完 ARC 的原理后,这个就不难解释了,对于 performSelector 返回值是 id,对于以下调用:

1
2
3
4
5
6
7
8
- (instancetype)newP {    
return [Person new];
}

- (void)test {
SEL sel = NSSelectorFromString(@"newP");
Person * person = [self performSelector:sel];
}

我们知道 person 为强指针,会对 performSelector 的返回值进行一次 retain 操作,然后在 person 离开作用域时进行一次 release 操作。

而如果 sel 是以 new、copy、mutableCopy、alloc 开头的,则返回的对象时带有一个引用计数的,所以 person 只进行了一次 retain 和一次 release,此时引用计数还是为 1,这就会发生内存泄漏问题。

NSInvocation 返回值问题

当我们使用 NSInvocation 的 getReturnValue获取返回值时,看苹果的声明,这个函数由于不知道返回值类型,只进行指针赋值不进行对象的内存管理操作,所以结合上面讲到的 ARC 内存管理问题我们就要考虑如何避免内存问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (instancetype)newP {
return [Person new];
}

- (instancetype)createPerson {
return [Person new];
}

- (void)test {
Person * targetPerson = [Person new];
SEL sel = @selector(newP);
NSMethodSignature * signature = [self methodSignatureForSelector:sel];
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = sel;
[invocation invokeWithTarget:targetPerson];

__strong Person * returnValue;
[invocation getReturnValue:&returnValue];
}

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

当被调用的函数是 createPerson 时,由于函数内部最后执行了 autorelease,如果此时我们再使用 strong 指针的话,就会导致内存泄漏问题。所以这里我们要使用 autoreleasing 来修饰 returnValue。

子线程的 Autorelease 是怎样维护的

前面一章中我们知道了主线程的 autorelease 对象是由 AutoreleasePoolPage 对象管理的,并且 AutoreleasePoolPage 的push 和 pop 操作是由主线程中在 RunLoop 中注册的两个 observer 维护的。那么在子线程中 autorelease 对象是如何维护的呢?因为我们知道子线程一般是没有 RunLoop的,那么在子线程中是如何维护的呢?来源码中找下答案:

子线程 AutoreleasePoolPage 创建

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
// 加入 autorealease 对象到 page 的入口函数
return AutoreleasePoolPage::autorelease((id)this);
}

static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
// 调用 autoreleaseFast
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}

static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
// 如果当前 page 为空
return autoreleaseNoPage(obj);
}
}

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
ASSERT(!hotPage());

bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
return setEmptyPoolPlaceholder();
}
// Install the first page.
// 关键来了:如果 page 为空则创建 page 对象
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}

// Push the requested object or pool.
return page->add(obj);
}

子线程 AutoreleasePoolPage 管理对象的释放,在线程退出时会调用 pthread_exit方法,最终会来到 tls_dealloc 函数。由于 objc 的源码不能调试到 pthread_exit 方法,所以这里我们只能看关于 AutoreleasePoolPage 的相关源码,当 if (!page->empty()) 满足时执行:objc_autoreleasePoolPop(page->begin()); 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void tls_dealloc(void *p) 
{
if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
return;
}

// reinstate TLS value while we work
setHotPage((AutoreleasePoolPage *)p);

if (AutoreleasePoolPage *page = coldPage()) {
if (!page->empty()) objc_autoreleasePoolPop(page->begin()); // pop all of the pools
if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
// pop() killed the pages already
} else {
page->kill(); // free all of the pages
}
}
// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}

综上,子线程的 autorelease 对象也是由 AutoreleasePoolPage 来管理的,再加入page时如果 page 为空则新建一个。

在线程退出时则会调用 tls_dealloc 方法,然后进行 pop 操作 来释放所有相关的 autorelease 对象。

参考: