先提几个问题,然后带着问题一起来学习autorelease的原理
- 加入autorelease pool的对象释放时机
- autorelease pool跟runloop之间的关系
- @autoreleasepool {}做了什么
1.释放时机
1.1 @autoreleasepool {}都做了什么
在手动管理内存的时候,我们可以创建NSAutoreleasePool来处理自动释放
可以看到NSAutoreleasePool的定义,我们可以手动调用drain来释放
NS_AUTOMATED_REFCOUNT_UNAVAILABLE
@interface NSAutoreleasePool : NSObject {
@private
void *_token;
void *_reserved3;
void *_reserved2;
void *_reserved;
}
+ (void)addObject:(id)anObject;
- (void)addObject:(id)anObject;
- (void)drain;
@end
在自动管理内存的情况下,我们不能直接通过上面这种方式来管理,系统提供了
@autoreleasepool {}
来管理
要了解它干了什么,我们可以通过汇编代码来查看,查看汇编代码可以直接使用Xcode IDE也可以用clang去rewrite成汇编代码
- Xcode的方式:
Product
-Perform Action
-Assemble main.m
- Clang的方式:
xcrun -sdk iphonesimulator clang -S -g -fobjc-arc main.m -o main.s
原始代码
int main(int argc, char * argv[]) {
@autoreleasepool { // line 13
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
} // line 15
}
我截取了main函数的汇编代码,可以看到有个_objc_autoreleasePoolPush
和_objc_autoreleasePoolPop
的。这个就是分别对应着pool的压栈、出栈;也印证了上面说的出了作用域就释放了。你也可以对照汇编的中的行号信息和代码可以发现_objc_autoreleasePoolPop
的调用就是在@autoreleasepool出了作用域那一行。
_main: ## @main
Lfunc_begin0:
.file 1 "/Users/hechao/Documents/Demos/LearningTest/LearningTest" "main.m"
.loc 1 12 0 ## main.m:12:0
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $48, %rsp
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
Ltmp0:
.loc 1 13 22 prologue_end ## main.m:13:22
callq _objc_autoreleasePoolPush
Ltmp1:
.loc 1 14 34 ## main.m:14:34
movl -8(%rbp), %edi
.loc 1 14 40 is_stmt 0 ## main.m:14:40
movq -16(%rbp), %rsi
.loc 1 14 69 ## main.m:14:69
movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx
movl %edi, -20(%rbp) ## 4-byte Spill
movq %rcx, %rdi
movq %rax, -32(%rbp) ## 8-byte Spill
movq %rsi, -40(%rbp) ## 8-byte Spill
callq _objc_opt_class
.loc 1 14 51 ## main.m:14:51
movq %rax, %rdi
callq _NSStringFromClass
movq %rax, %rdi
callq _objc_retainAutoreleasedReturnValue
xorl %edx, %edx
## kill: def $rdx killed $edx
movl -20(%rbp), %edi ## 4-byte Reload
movq -40(%rbp), %rsi ## 8-byte Reload
.loc 1 14 16 ## main.m:14:16
movq %rax, %rcx
movq %rax, -48(%rbp) ## 8-byte Spill
callq _UIApplicationMain
.loc 1 14 9 ## main.m:14:9
movl %eax, -4(%rbp)
movq -48(%rbp), %rcx ## 8-byte Reload
movq %rcx, %rdi
callq *_objc_release@GOTPCREL(%rip)
movq -32(%rbp), %rdi ## 8-byte Reload
.loc 1 15 5 is_stmt 1 ## main.m:15:5
callq _objc_autoreleasePoolPop
Ltmp2:
.loc 1 16 1 ## main.m:16:1
movl -4(%rbp), %eax
addq $48, %rsp
popq %rbp
retq
Ltmp3:
Lfunc_end0:
这里可以看到在出了@autoreleasepool{}的作用域的时候就会去释放加入到pool中的对象
1.2 主线程的autoreleasepool了?
看了上面的解释,你可能有疑问,不是说主线程的释放在runloop BeforeWaiting和Exit的时候会释放吗
这个跟上面说的出了作用域就释放是不冲突的,由于系统在主线程的runloop中添加了observer回调来处理pool的释放与创建;怎么验证一下了?
很简单,我们在主线程下个断点,然后po一下[NSRunloop currentRunLoop]
来看看详细的信息
(lldb) po [NSRunLoop currentRunLoop]
<CFRunLoop 0x600003584000 [0x7fff80617cb0]>{wakeup port = 0x2407, stopped = false, ignoreWakeUps = false,
current mode = kCFRunLoopDefaultMode,
common modes = <CFBasicHash 0x6000007bcde0 [0x7fff80617cb0]>{type = mutable set, count = 2,
// ...省略一大堆
observers = (
"<CFRunLoopObserver 0x600003884460 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000007c2970 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f94d7803048>\n)}}",
"<CFRunLoopObserver 0x6000038881e0 [0x7fff80617cb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff47c2f06a), context = <CFRunLoopObserver context 0x600002280930>}",
"<CFRunLoopObserver 0x600003884320 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff480bc2eb), context = <CFRunLoopObserver context 0x7f94d67006b0>}",
"<CFRunLoopObserver 0x60000388c640 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x7fff2b0c046e), context = <CFRunLoopObserver context 0x0>}",
"<CFRunLoopObserver 0x6000038843c0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff480bc354), context = <CFRunLoopObserver context 0x7f94d67006b0>}",
"<CFRunLoopObserver 0x600003884500 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000007c2970 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f94d7803048>\n)}}"
),
// ...省略一大堆
}
}
东西一大堆,我们关注的就是callout,发现有一个_wrapRunLoopWithAutoreleasePoolHandler
这个似乎跟autoreleasepool有关系的,可以看到它回调的Activity有2个0x1``0xa0
Activity的定义如下:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 0x1
kCFRunLoopBeforeTimers = (1UL << 1), // 0x2
kCFRunLoopBeforeSources = (1UL << 2), // 0x4
kCFRunLoopBeforeWaiting = (1UL << 5), // 0x20
kCFRunLoopAfterWaiting = (1UL << 6), // 0x40
kCFRunLoopExit = (1UL << 7), // 0x80
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
对应的16进制的值也都备注了,同时kCFRunLoopBeforeWaiting | kCFRunLoopExit = 0xa0``kCFRunLoopEntry = 0x1
;
可以看到在Runloop的Activity为Entry、BeforeWaitting、Exit的时候都会触发注册的那个回调_wrapRunLoopWithAutoreleasePoolHandler
,这个函数里面就会根据Activity来做pop push的操作,具体就不讲解了,下次学习runloop的时候再细究,打了个符号断点_wrapRunLoopWithAutoreleasePoolHandler
,简单的看下流程
_wrapRunLoopWithAutoreleasePoolHandler
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
* frame #0: 0x00007fff4808bf55 UIKitCore`_wrapRunLoopWithAutoreleasePoolHandler + 1
frame #1: 0x00007fff23bd3867 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
frame #2: 0x00007fff23bce2fe CoreFoundation`__CFRunLoopDoObservers + 430
frame #3: 0x00007fff23bce04d CoreFoundation`CFRunLoopRunSpecific + 413
frame #4: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
frame #5: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
frame #6: 0x000000010a5d5da0 LearningTest`main(argc=1, argv=0x00007ffee5632d20) at main.m:14:16
frame #7: 0x00007fff5227ec25 libdyld.dylib`start + 1
(lldb)
至此,autoreleasepool的释放时机也就基本梳理清楚了:
- 出了作用域会释放
- runloop的BeforeWaitting、Exit触发观察回调处理释放
2.源码解读
接下来学习下_objc_autoreleasePoolPush
和_objc_autoreleasePoolPop
都做了什么
先看下AutoreleasePool的数据结构,数据结构我一窍不通,知道是个双向链表就好了。
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next; // 对象指针
pthread_t const thread;
AutoreleasePoolPage * const parent; // 前一页
AutoreleasePoolPage *child; // 后一页
uint32_t const depth; // 链表长度
uint32_t hiwat;
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
2.1 _objc_autoreleasePoolPush
void *
_objc_autoreleasePoolPush(void)
{
return objc_autoreleasePoolPush();
}
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY); // autoreleaseFast(nil)
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
2.1.1 autoreleaseFast
关键的代码在autoreleaseFast(POOL_BOUNDARY)``POOL_BOUNDARY
就是一个nil
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage(); // 可以理解为上次添加obj的page
if (page && !page->full()) { // page有并且没有满
return page->add(obj); // 直接加到该page中
} else if (page) { // page满了
return autoreleaseFullPage(obj, page);
} else { // 没有hotPage
return autoreleaseNoPage(obj);
}
}
它的整体流程如下,代码中我也加了注释:
- 先获取hotPage--可以理解为上次添加obj的page
- 没有hotPage,则表示pool还没创建,调用
autoreleaseNoPage
去创建一个page并压栈 - 有hotPage且该page还没有满,还可以添加对象进去,则直接加到该page中
page->add(obj)
- 有hotPage但是该page满了,则通过
autoreleaseFullPage(obj, page)
去遍历该page的子节点是否可以放,不行就新建一个page,并把对象加进去 - 对象添加到某个page之后,会将该page设置为hotPage
- 在新建一个page的时候都会add一个
POOL_BOUNDARY
这个来标记page的头,同时也用它的地址来表示page的token,pop的时候可以根据token来pop到指定的page
代码也挺多,具体代码就不贴了;可以参照上面的流程去阅读源码去体会
2.1.2 对象是如何加到pool的page中的
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
可以看到将page数据结构的next指针指向着添加到page中的对象
2.2 _objc_autoreleasePoolPop
void
_objc_autoreleasePoolPop(void *ctxt)
{
objc_autoreleasePoolPop(ctxt);
}
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
page = coldPage();
token = page->begin();
} else {
page = pageForPointer(token);
}
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
return popPage<false>(token, page, stop);
}
这里会根据传的token来确定pop到哪个page
- token为
EMPTY_POOL_PLACEHOLDER
表示是pop到最顶层,全部释放掉;如果有 - token为其他值,则会根据
pageForPointer(token)
来确定pop到哪个page - 确定pop到的page之后,则调用
popPage<false>(token, page, stop)
来释放对象和清除page信息
2.2.1 pageForPointer
根据token的值来计算得到page
static AutoreleasePoolPage *pageForPointer(const void *p)
{
return pageForPointer((uintptr_t)p);
}
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE; // SIZE就是一个page的大小;
ASSERT(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
2.2.2 释放page-popPage
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (allowDebug && DebugPoolAllocation && page->empty()) { // page为空
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent); // 将hotPage设置为该page的parent
} else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) { // page为空并且是根节点
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
} else if (page->child) { // page有子节点
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) { // page内容还未满一半,则将全部子节点干掉
page->child->kill(); // 从child节点开始遍历删除子节点
}
else if (page->child->child) { // page内容超过一半了,则保留一个空的子节点
page->child->child->kill();
}
}
}
整体逻辑:
- 调用
page->releaseUntil(stop);
来释放page中的obj - 释放完了之后,会根据当前page的存储的内容是否超过了容量的一半,来决定是否需要保留一个空的子节点;这里可以这么理解,当前page快满了,保留一个空节点备用,没有超过容量的一半那就还能用一会就不保留了。
releaseUntil
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
while (this->next != stop) { // 判断是否释放到了结束的对象
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage(); // hotPage一般是链表的tail
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) { // page空了,就取父节点
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next; // 通过位置偏移从动态数组中取出obj
memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 将这一块内存设置为SCRIBBLE
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj); // 释放对象
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}
- releaseUntil从hotPage(一般是链表的tail)开始遍历对象进行release操作,直到释放到传进来的stop对象
- 由于对象压入page是从低到高的,释放的时候就通过page的next动态数组来
--
操作拿到对象,从高到低来释放 - 当某个page被释放完了,就找到它的parent来继续遍历释放,并且将hotPage设置为当前遍历的page
2.3 autorelease
这里的autorelease是pool的内部的实现,跟NSObject的autorelease有一些差异
NSObject中的实现
- (id)autorelease {
return _objc_rootAutorelease(self);
}
调用到rootAutorelease
NEVER_INLINE id
_objc_rootAutorelease(id obj)
{
ASSERT(obj);
return obj->rootAutorelease();
}
// Base autorelease implementation, ignoring overrides.
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this; // tagged pointer直接返回不走这一套流程
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
最终调用到AutoreleasePoolPage的autorelease方法
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
最终调用了AutoreleasePoolPage的autorelease方法
static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj); // 将对象加入到pool的page中
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj; // 返回原对象
}
- autorelease内部调用跟push是一个函数
autoreleaseFast
区别在于新建一个page,加进去一个nil-标记位,这里是将需要释放的对象加进去。 - NSObject中autorelease的实现有个优化
prepareOptimizedReturn
返回true就直接返回对象了不加到pool中去;
这个优化暂时还未弄明白,后续再学习分析,这里有sunnyxx大神的分析
3.总结
- 本文梳理了autorelease的大概流程,一些细节我也还没吃透,后续在继续深入学习吧
- 对于autorelease的释放时机,一是出了@autorelease {}作用域,二是结合runloop来看,由于注册了回调在runloop BeforeWaittting、Exit、Entry的时候会触发,来处理pool的创建和释放的逻辑,BeforeWaittting、Exit的时候也会释放
讲解的不对或者不清晰的地方,也欢迎大家指导相互学习。
网友评论