美文网首页
OC-内存管理autorelease原理

OC-内存管理autorelease原理

作者: jayhe | 来源:发表于2020-09-25 19:37 被阅读0次

先提几个问题,然后带着问题一起来学习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大神的分析

补充:编译器对autorelease的优化

3.总结

  • 本文梳理了autorelease的大概流程,一些细节我也还没吃透,后续在继续深入学习吧
  • 对于autorelease的释放时机,一是出了@autorelease {}作用域,二是结合runloop来看,由于注册了回调在runloop BeforeWaittting、Exit、Entry的时候会触发,来处理pool的创建和释放的逻辑,BeforeWaittting、Exit的时候也会释放

讲解的不对或者不清晰的地方,也欢迎大家指导相互学习。

相关文章

网友评论

      本文标题:OC-内存管理autorelease原理

      本文链接:https://www.haomeiwen.com/subject/bazfuktx.html