最近在做皮肤适配时遇到了很多的坑,进而产生了很多的疑问。下面是想到了一些问题和最终得到的验证,内功太菜。之前一直在怀疑是他们都太飘了还是我拿不动刀了。现在看看内功太菜,确实是我打不动刀了。。。
之前看 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
对于以new
、copy
、mutableCopy
和alloc
以及 以这四个单词开头的所有函数,默认认为函数返回值直接持有对象。这是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 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]; }
首先当被调用函数是以new
,copy
,mutableCopy
和alloc
开头的特殊函数时,函数返回的的对象持有引用计数,所以我们设置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 对象。
参考: