Memory autorelease pool in iOS

autorelease pool

The main thread of the iOS application creates an autorelease pool at the beginning of each runloop, and releases the autorelease pool at the end of the runloop. If the application creates a large number of temporary objects within a runloop, the autorelease pool can reduce memory spikes.

for (int i = 0; i < 1000000; i++) {
    
    

    NSString *string = [NSString stringWithFormat:@"hello world!"];
}

If you execute the above code, you can see an obvious short-term memory increase using XCode.

for (int i = 0; i < 1000000; i++) {
    
    
    @autoreleasepool {
    
    
        NSString *string = [NSString stringWithFormat:@"hello world!"];
    }
}    

After the automatic release pool is added, the memory usage of the program does not increase significantly. At this time, the automatic release pool releases the temporarily created variable memory.

Autorelease pool push and pop timing

The push/pop of autoreleasePool is related to runloop.

When the runloop enters the kCFRunLoopEntry state, call the objc_autoreleasePoolPush method

When runloop enters kCFRunLoopBeforeWaiting state, call objc_autoreleasePoolPop method and objc_autoreleasePoolPush method

When the runloop enters the kCFRunLoopBeforeExit state, it calls the objc_autoreleasePoolPop method.

Autorelease pool data structures

struct AutoreleasePoolPageData
{
    
    
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    struct AutoreleasePoolEntry {
    
    
        uintptr_t ptr: 48;
        uintptr_t count: 16;

        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
    static_assert((AutoreleasePoolEntry){
    
     .ptr = OBJC_VM_MAX_ADDRESS }.ptr == OBJC_VM_MAX_ADDRESS, "OBJC_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif

	magic_t const magic;   //校验page是否完整
	__unsafe_unretained id *next;   //指向下一个存放数据的地址
	objc_thread_t const thread;     //当前线程
	AutoreleasePoolPage * const parent;  //父结点
	AutoreleasePoolPage *child;       //子结点
	uint32_t const depth;   //page深度
	uint32_t hiwat;   //最大入栈数量

	AutoreleasePoolPageData(__unsafe_unretained id* _next, objc_thread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
		: magic(), next(_next), thread(_thread),
		  parent(_parent), child(nil),
		  depth(_depth), hiwat(_hiwat)
	{
    
    
	}
};

AutoreleasePoolPageData is the base class of AutoreleasePoolPage .

The automatic release pool is a data structure of a doubly linked list. The parent pointer points to the parent node, and the child points to the child node. The data size of each page is 4096 bytes, and each page is a node of the doubly linked list. next represents the position where data can be inserted on the current page.

Implementation principle of automatic release pool

push process

void *
objc_autoreleasePoolPush(void)
{
    
    
    return AutoreleasePoolPage::push();
}

This method is called internally by Objective-C when the code adds an object to the autorelease pool.

 static inline void *push() 
    {
    
    
        ReturnAutoreleaseInfo info = getReturnAutoreleaseInfo();
        moveTLSAutoreleaseToPool(info);

        id *dest;
        if (slowpath(DebugPoolAllocation)) {
    
    
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
    
    
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == (id *)EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

This method mainly calls the autoreleaseFast method.

 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 {
    
    
            return autoreleaseNoPage(obj);
        }
    }

The autoreleaseFast method is used to obtain the current hotPage. If the hotPage is not full, insert data directly. If the hotPage is full, reallocate a page space and insert it. If the page is not obtained, it means that the current autorelease pool has not been initialized, and the autorelease needs to be initialized. pool.

id *add(id obj)
    {
    
    
        ASSERT(!full());
        unprotect();
        id *ret;

        ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;

     done:
        protect();
        return ret;
    }

After the add method is simplified, you can see that the add method actually puts obj into the position of the next pointer, and puts next++. The return value of the function is the storage address of obj.

static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
    
    
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        ASSERT(page == hotPage());
        ASSERT(page->full()  ||  DebugPoolAllocation);

        do {
    
    
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

The autoreleaseFullPage function is to find a page that is not full, call the add function to insert the obj object

static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
    
    
        // "No page" could mean no pool has been pushed
        // or an empty placeholder pool has been pushed and has no contents yet
        ASSERT(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
    
    
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
    
    
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         objc_thread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
    
    
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }

        

        // 初始化首页
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // 增加boundry标识
        if (pushExtraBoundary) {
    
    
            page->add(POOL_BOUNDARY);
        }
        
        // 添加obj
        return page->add(obj);
    }

If there is no autorelease pool page currently, you need to initialize AutoreleasePoolPage, and then call the add method to add obj. The role of setHotPage here is to identify the current page.

pop process

void objc_autoreleasePoolPop(void *ctxt)
{
    
    
    AutoreleasePoolPage::pop(ctxt);
}

The autorelease pool will call this method when it pops.

    static inline void
    pop(void *token)
    {
    
    
        // We may have an object in the ReturnAutorelease TLS when the pool is
        // otherwise empty. Release that first before checking for an empty pool
        // so we don't return prematurely. Loop in case the release placed a new
        // object in the TLS.
        while (releaseReturnAutoreleaseTLS())
            ;

        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
    
    
            // 获取hotPage
            page = hotPage();
            if (!page) {
    
    
                // 自动释放池没有使用过,清空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 {
    
    
                
                return badPop(token);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
    
    
            return popPageDebug(token, page, stop);
        }
        //清理自动释放池
        return popPage<false>(token, page, stop);
    }

When popping, it will judge whether it is marked as EMPTY_POOL_PLACEHOLDER, POOL_BOUNDARY, etc. This function calls the popPage function at the end.

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()) {
    
    
            //debug使用
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
    
    
            // special case: delete everything for pop(top)
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } else if (page->child) {
    
    
            // 如果页面实际占用空间小于一半,将child结点销毁
            if (page->lessThanHalfFull()) {
    
    
                page->child->kill();
            }
            如果页面实际占用空间大于一半,将child结点保留
            else if (page->child->child) {
    
    
                page->child->child->kill();
            }
        }
    }
    void releaseUntil(id *stop) 
    {
    
    
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage

        do {
    
    
            while (this->next != stop) {
    
    
             
                AutoreleasePoolPage *page = hotPage();

                while (page->empty()) {
    
    
                    page = page->parent;
                    setHotPage(page);
                }

                page->unprotect();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

                // create an obj with the zeroed out top byte and release that
                id obj = (id)entry->ptr;
                int count = (int)entry->count;  // grab these before memset
#else
                id obj = *--page->next;
#endif
                memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
                page->protect();

                if (obj != POOL_BOUNDARY) {
    
    
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                    // release count+1 times since it is count of the additional
                    // autoreleases beyond the first one
                    for (int i = 0; i < count + 1; i++) {
    
    
                        objc_release(obj);
                    }
#else
                    objc_release(obj);
#endif
                }
            }

            // Stale return autorelease info is conceptually autoreleased. If
            // there is any, release the object in the info. If stale info is
            // present, we have to loop in case it autoreleased more objects
            // when it was released.
        } while (releaseReturnAutoreleaseTLS());

        setHotPage(this);

#if DEBUG
        // 检查子结点是否清空
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
    
    
            ASSERT(page->empty());
        }
#endif
    }

The releaseUntil function will clear an autorelease pool, and the objc_release function is actually called when the object is released.

Guess you like

Origin blog.csdn.net/u011608357/article/details/128439012