iOS - the underlying principle of Autoreleasepool

What is an autorelease pool

As the name implies, autoreleasepool is also called autorelease pool, which is similar to automatic variables in C language. We can add objects to autoreleasepool, and call release to release the object when it exceeds its scope.
When exploring the ARC implementation before, we can find that ARC adds objects that are not held by itself to the autoreleasepool.
insert image description here
Let's take a deeper look at the structure of autoreleasepool

The structure of the autorelease pool

Looking at the source code in objc, you can find that you will go to the end:
insert image description here
the above method seems to be the encapsulation of the corresponding static methods push and pop of AutoreleasePoolPage. Let's take a look at this AutoreleasePoolPage.

AutoreleasePoolPage

Open its source code in objc:

// MARK: - AutoReleasepool class
class AutoreleasePoolPage 
{
    
    
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    // 校验完整性
    magic_t const magic;
    
    // next指针指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
    id *next;
    
    // 指向当前线程
    pthread_t const thread;
    
    // 父节点
    AutoreleasePoolPage * const parent;
    
    // 子节点
    AutoreleasePoolPage *child;
    
    // 节点深度
    uint32_t const depth;
    uint32_t hiwat;

    // SIZE-sizeof(*this) bytes of contents follow

    static void * operator new(size_t size) {
    
    
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    static void operator delete(void * p) {
    
    
        return free(p);
    }

This data structure is very familiar. It turns out that the bottom layer of AutoreleasePool is composed of a series of AutoreleasePoolPages
(and the size of each AutoreleasePoolPage is 4096 bytes (hexadecimal 0x1000))

#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES

The structure is as follows:
insert image description here
it is a doubly linked list, and parent and child are pointers used to construct the doubly linked list.

In the official document's explanation of AutoreleasePool, we can see the following concepts, which can be used in the source code below.


Autorelease pool implementation
 
- A thread's autorelease pool is a stack of pointers. 
线程的自动释放池是指针的堆栈
 
- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
每个指针都是要释放的对象,或者是POOL_BOUNDARY,它是自动释放池的边界。
 
- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
池令牌是指向该池的POOL_BOUNDARY的指针。弹出池后,将释放比哨点更热的每个对象。
 
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. 
堆栈分为两个双向链接的页面列表。根据需要添加和删除页面。
 
- Thread-local storage points to the hot page, where newly autoreleased objects are stored. 
线程本地存储指向热页面,该页面存储新自动释放的对象。

1. The automatic release pool is a stack structure about pointers

2. The pointer refers to the object to be released or the pool_boundary sentinel (now often referred to as the boundary)
pool_boundary : POOL_BOUNDARY: nil macro definition, replacing the previous sentinel object POOL_SENTINEL, when the autorelease pool is created, set it in objc_autoreleasePoolPush Pushed into the autorelease pool. When objc_autoreleasePoolPop is called, the objects in the pool will be released sequentially until the last POOL_BOUNDARY is encountered.

3. The automatic release pool is a page structure (mentioned in virtual memory), and this page is a doubly linked list (indicating that there are parent nodes and child nodes, mentioned in the class, that is, the inheritance chain of the class)

4. Some nouns

  • hotPage: It is currently in use page, and the operations are all hotPagecompleted on it, usually at the end of the linked list or the penultimate position. Stored TLS in , it can be understood as an autorelease pool linked list shared by each thread.

  • coldPage: At the head of the linked list page, it may be at the same time hotPage.

  • EMPTY_POOL_PLACEHOLDER: When no object has been pushed into the autorelease pool, if one is pushed at this time POOL_BOUNDARY, it will be EMPTY_POOL_PLACEHOLDERstored in TLS as an identifier first, and it will not be pushed this time POOL_BOUNDARY. When an object is pushed into the automatic release pool again, check to take out the identifier in TLS, and push it in again at this time POOL_BOUNDARY.

  • next: Points to AutoreleasePoolPagethe pointer to the empty space at the top of the stack, and moves up every time a new element is added.

Stack in autoreleasePool

If one of our AutoreleasePoolPages is initialized at 0x100816000 ~ 0x100817000 in memory, its structure in memory is as follows:
insert image description here
56 bits are used to store member variables of AutoreleasePoolPage, and the remaining 0x100816038 ~ 0x100817000 are used to store and add to autorelease Objects in the pool.
The instance methods of the two classes begin() and end() help us quickly obtain the boundary address of the range 0x100816038 ~ 0x100817000.
next points to the next empty memory address. If an object is added to the address pointed to by next, it will move to the next empty memory address as shown in the figure below: when releasing the added element in the stack, it will be based
insert image description here
on The sentinel is pool_boundary as the boundary Pop.
When the method objc_autoreleasePoolPop is called, a release message is sent to the objects in the autorelease pool until the first POOL_SENTINEL:
insert image description here

Implementation source code of autoreleasePool

objc_autoreleasePoolPush

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

It calls the class method push of AutoreleasePoolPage, which is also very simple:

// push 返回的是next指针指向的obj(是一个released obj)
    static inline void *push() 
    {
    
    
        id *dest;
        if (DebugPoolAllocation) {
    
    
            
            // 每一个autoreleasepool对象开始于一个新的autoreleasepoolpage
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
    
    
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

objc_autoreleasePoolPushThe method is actually to push one into the autorelease pool POOL_BOUNDARYas autoreleasepoolthe starting point for the release. The specific logic of the autoreleaseFast method will autoreleasebe analyzed later when analyzing the method.

autorelease

Let's look at the code implementation of adding the autorelease pool method autorelease:

static inline id autorelease(id obj) {
    
    
        id *dest __unused = autoreleaseFast(obj);
        return obj;
}

You can see that the object is called as a parameter here autoreleaseFast.

 // 创建一个page逻辑
    static inline id *autoreleaseFast(id obj)
    {
    
    
        // hotPage可以理解为正在使用的AutoreleasePoolPage
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
    
    
            // 有 hotPage 且不满 直接加入栈中
            return page->add(obj);
        } else if (page) {
    
    
            // hotPage 已满 先创建一个 Page 后,加入新 Page 中
            return autoreleaseFullPage(obj, page);
        } else {
    
    
            // 没有 hotPage 直接新建一个 Page,并加入 Page 中
            return autoreleaseNoPage(obj);
        }
    }

The logic of this code:

  • If the HotPage exists and is not full, it is directly pushed into the hotPage.
  • If the HotPage exists and is full, call autoreleaseFullPage to initialize a new page and push it
  • If there is no HotPage, call autoreleaseNoPage, create a hotPage, and push the hotPage.

autoreleaseFullPage (current hotPage is full)

 // MARK: - 创建一个new page
    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);
        // 找到一个未满的 page , 未找到则新建一个 page ,设置成 hotPage
        do {
    
    
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

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

The logic of this method is as follows:

  • Check whether the hotPage has a successor node (page), and if so, use the successor node directly.
  • If there is no successor node, create a new AutoreleasePoolPage.
  • Adding the object to the obtained page and setting it as a hotPage is actually storing it in TLS for sharing.

autoreleaseNoPage (no hotPage)

// autoreleasepool中没有page
    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
        // 执行 No page 表示目前还没有释放池,或者有一个空占位符池,但是还没有加入对象
        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) {
    
    //如果没有创建一个autoreleasepool,则直接抛异常
            // 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", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
    
     // 如果传入 POOL_BOUNDARY 则设置空池占位符
            // 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();
        }

        // We are pushing an object or a non-placeholder'd pool.
        // 初始化一个 page 并设置 hotPage
        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // 插入释放池边界
        if (pushExtraBoundary) {
    
    
            page->add(POOL_BOUNDARY);
        }
        
        //  obj添加到page中
        return page->add(obj);
    }

autoreleaseNoPage is only called when there is no page in the autorelease pool, the main logic:

  • If the current autorelease pool push is a sentinel POOL_BOUNDARY, it will EmptyPoolPlaceholderbe stored in TLS.
  • If TLS is stored EmptyPoolPlaceholder, after the page is created, one will be pushed first POOL_BOUNDARY, and then the object added to the autorelease pool will be pushed.
    The push calls the add function:
// 向page中加入对象
    id *add(id obj)
    {
    
    
        assert(!full());
        unprotect();
        
        // 将next节点赋值给ret
        id *ret = next;  // faster than `return next-1` because of aliasing
        
        // 将obj赋值给next
        *next++ = obj;
        protect();
        return ret;
    }

This is easy to understand, it is an operation of pushing onto the stack.

objc_autoreleasePoolPop

When the scope of the autorelease pool ends, objc_autoreleasePoolPop is called to release the objects in the autorelease pool.

// pop
    static inline void pop(void *token) 
    {
    
    
        AutoreleasePoolPage *page;
        id *stop;

        // 如果是空池占位符,要清空整个自动释放池
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
    
    
            // Popping the top-level placeholder pool.
            if (hotPage()) {
    
    
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                // 如果存在 hotPage ,则找到 coldPage 的起点 重新 pop
                pop(coldPage()->begin());
            } else {
    
    
                // 未使用过的释放池,置空 TLS 中存放的 hotPage.
                setHotPage(nil);
            }
            return;
        }

        // 根据token获取page
        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
    
    
            // 在 stop 不为 POOL_BOUNDARY 的情况下 只可能是 coldPage()->begin()
            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 {
    
    
                // 如果不是 POOL_BOUNDARY 也不是 coldPage()->begin() 则报错
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        /// 释放 stop 后面的所有对象
        page->releaseUntil(stop);

        // 清除空节点
        if (DebugPoolAllocation  &&  page->empty()) {
    
    
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
    
    
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        }
        // 清除后续节点 page
        else if (page->child) {
    
    
            // 如果当前 page 没有达到半满,则干掉所有后续 page
            if (page->lessThanHalfFull()) {
    
    
                page->child->kill();
            }
            // 如果当前 page 达到半满以上,则保留下一页
            else if (page->child->child) {
    
    
                page->child->child->kill();
            }
        }
    }

This code logic:

  • Check whether the input parameter is an empty pool placeholder EMPTY_POOL_PLACEHOLDER, if it is, continue to judge whether hotPageit exists, if hotPageit exists, change the end point of release to coldPage()->begin(), if hotPageit does not exist, empty the TLS storage hotPage.
  • Checking that stop is neither POOL_BOUNDARYnor coldPage()->begin()will report an error.
  • Empty all objects in the autorelease pool after stop.
  • It is judged that if the current page is not half full, all subsequent pages will be killed, and if it is more than half full, only the next page will be kept.

Several functions are used here, the first is to get the pageForPointer of the page through the token:

 static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
    {
    
    
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;
// 将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:
        assert(offset >= sizeof(AutoreleasePoolPage));

        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }

Take the pointer and the size of the page, which is 4096, to get the offset of the current pointer, because all AutoreleasePoolPages are aligned in memory:

p = 0x100816048
p % SIZE = 0x48
result = 0x100816000
and the last called method fastCheck() is used to check whether the current result is an AutoreleasePoolPage.

By checking whether a member in the magic_t structure is 0xA1A1A1A1.

releaseUntil release object

The implementation is as follows:

 // MARK: - 释放obj
    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();

            // fixme I think this `while` can be `if`, but I can't prove it
            
            // 获取page的父指针
            while (page->empty()) {
    
    
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            
            // 根据next指针缩小一个位置获取obj
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
    
    
                
                // 发送release 消息
                objc_release(obj);
            }
        }

        setHotPage(this);

Here, through a while loop, it is judged that if the current page is cleared, continue to clean up the previous page in the linked list, and release the objects stored in the page.

kill() method

 void kill() 
    {
    
    
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
    
    
            deathptr = page;
            page = page->parent;
            if (page) {
    
    
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;
        } while (deathptr != this);
    }

Probably the logic is to find the last page first, and then delete until the current page is deleted.

The call stack of autorelease

insert image description here
The first is that we create autoreleasePool, which starts from autoreleasePoolPush and ends with Pop.

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

In autoreleasethe call stack of the method, autoreleaseFastthe method to add the current object AutoreleasePoolPageto .

RunLoop和autoreleasePool

iOS registers two Observers in the RunLoop of the main thread:

  • The first Observer listens to the kCFRunLoopEntry event and calls objc_autoreleasePoolPush();
  • The second Observer:
    ① After listening to the kCFRunLoopBeforeWaiting event, it will call objc_autoreleasePoolPop(), objc_autoreleasePoolPush();
    ② After listening to the kCFRunLoopBeforeExit event, it will call objc_autoreleasePoolPop().

Guess you like

Origin blog.csdn.net/chabuduoxs/article/details/126326184