This article will continue where we left off in our last article, which looked at the memory layout, memory management scheme, reference counting, and more for IOS memory management.

  • IOS Memory Management 1: Tagged Pointer& Reference count
  • IOS Memory Management 2: Automatic releasepool Autoreleasepool

1. Preliminary study of Autoreleasepool

If you are familiar with OC development, you will know that there is such a thing as @AutoReleasepool in the main function, which is the automatic releasepool. What does the underlying implementation of @Autoreleasepool look like? Using clang-rewrite-objc main.m -o main. CPP on the command line, we tell the compiler to rewrite the file to get a main. CPP file.

int main(int argc, const char *argv[])
{
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 0;
}
Copy the code

We can see that @ Autoreleasepool is converted to an __AtAutoreleasePool structure, which means that @ Autoreleasepool is essentially an __AtAutoreleasePool structure.

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush(); } ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj); } void * atautoreleasepoolobj; };Copy the code

This structure calls the objc_autoreleasePoolPush() method at initialization and the objc_autoreleasePoolPop method at destructor.

This shows that the main function actually works like this:

int main(int argc, const char *argv[])
{
    void * atautoreleasepoolobj = objc_autoreleasePoolPush();

    // do whatever you want

    objc_autoreleasePoolPop(atautoreleasepoolobj);
    return 0;
}
Copy the code

Everything seems to revolve around objc_autoreleasePoolPush() and objc_autoreleasePoolPop. So let’s look at the source implementation of these two methods:

Void * objc_autoreleasePoolPush(void) {// The AutoreleasePoolPage push method is invokedreturnAutoreleasePoolPage::push(); } void objc_autoreleasePoolPop(void * CTXT) {AutoreleasePoolPage::pop(CTXT);} void objc_autoreleasepoolPage ::pop(CTXT); }Copy the code

The above two methods appear to encapsulate the static methods push and POP corresponding to AutoreleasePoolPage.

2, AutoreleasePoolPage

There is a comment found in the runtime source code (objC4-756.2) that will help us understand the underlying structure of AutoreleasePoolPage.

  • 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.
  • 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.
  • 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.

The translation is as follows:

  • A thread’s auto-release pool is a stack structure of Pointers.
  • Each pointer represents an object that needs to be released or POOL_BOUNDARY
  • A pool token is the memory address of the POOL_BOUNDARY corresponding to this pool. When the pool is popped, all objects with memory addresses after the pool token are released.
  • The stack is divided into a bidirectional linked list of pages. Pages are dynamically added and removed as necessary.
  • Thread-local storage points to the Hot Page where the newly added Autoreleased object resides.

The autorelease pool is a stack structure that follows the principle of “first in, last out”. Each autorelease pool consists of a series of Autoreleasepoolpages that are connected in a bidirectional linked list.

2.1. AutoreleasePoolPage structure

Take a look at the code definition for AutoreleasePoolPage (only the key code is listed; some code is omitted).

class AutoreleasePoolPage 
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nilstatic pthread_key_t const key = AUTORELEASE_POOL_KEY; static uint8_t const SCRIBBLE = 0xA3; // AutoreleasePoolPage SIZE = 4096 bytes 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
 #endifstatic size_t const COUNT = SIZE / sizeof(id); magic_t const magic; //16 bytes id *next; //8 bytes pthread_t const thread; //8 bytes AutoreleasePoolPage * const parent; //8 bytes AutoreleasePoolPage *child; //8 bytes uint32_t const depth; //4 bytes uint32_t hiwat; / /} 4 bytesCopy the code
  • magic: used for verificationAutoreleasePoolPageIs the structure complete?
  • *next: Next refers to the next oneAutoreleasePoolPageThe next memory address to which the new object will be stored is emptynextIs pointed to when initializedbegin().
  • thread: saves when the previous page is in the thread (oneAutoreleasePoolPageBelongs to a thread, and there can be more than one threadAutoreleasePoolPage).
  • *parent: points to the parent node, the first oneparentNodes fornil.
  • *child: points to the child node, the last onechildNodes fornil.
  • Depth: indicates the depth. The value starts from 0 and increases by +1.
  • hiwatRepresentative:high water MarkMaximum number of stacks.
  • SIZE:AutoreleasePoolPageThe value isPAGE_MAX_SIZE,4096 bytes.
  • POOL_BOUNDARY: justnilThe alias. Initialize the call in each auto release poolobjc_autoreleasePoolPushWhen, will put aPOOL_SENTINELPush to the top of the auto-release pool stack and return thisPOOL_SENTINELAutomatically release pool boundaries. And when the methodobjc_autoreleasePoolPopWhen called, it is sent to objects in the auto-release poolreleaseMessage, until the first onePOOL_SENTINEL.

The first object in AutoreleasePoolPage is stored after Next, resulting in a structure like the one shown below.

The 56 bytes store the member variables of AutoreleasePoolPage, and the other areas store objects loaded into the auto-release pool. When next==begin(), AutoreleasePoolPage is empty. When next==end(), AutoreleasePoolPage is full.

2.2 AutoreleasePoolPage capacity

Each AutoreleasePoolPage is 4096 bytes, 56 bytes of which are used to store member variables, and the remaining area is used to store objects loaded into the auto-release pool. An AutoreleasePoolPage can store (4096-56)/8=505 objects. One caveat, however, is that the first page should hold 504 objects that need to be freed, because 1POOL_SENTINEL will be inserted at the next location when the page is created.

2.3. Push method

From the analysis in the previous section, we know that the essence of objc_autoreleasePoolPush is to call the push method. Let’s take a look at the source of the push method.

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);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}
Copy the code

The autoreleaseFast method is actually called in the push method, and a POOL_BOUNDARY object is first inserted at the top of the stack. Slowpath indicates a low probability of occurrence.

2.3.1 autoreleaseFast method

Here is the source code for the autoreleaseFast method

Static Inline ID *autoreleaseFast(id obj) {// hotPage is the current AutoreleasePoolPage AutoreleasePoolPage *page = hotPage();if(page && ! Page ->full()) {// There is a hotPage and hotPage is not satisfied, add the object to the hotPagereturn page->add(obj);
    } else if(page) {// hotPage exists but hotPage is full // Initialize a new page with autoreleaseFullPage and add the object to the new AutoreleasePoolPagereturn autoreleaseFullPage(obj, page);
    } else{// None hotPage // Create a hotPage with autoreleaseNoPage and add the object to the newly created pagereturnautoreleaseNoPage(obj); }}Copy the code

The code for the autoreleaseFast method is simple, just three judgment branches.

  1. If you havehotPageAnd is not fulladdThe hotPage method adds an object to the hotPage. Otherwise, go to Step 2.
  2. If you havehotPageBut it is already fullautoreleaseFullPageMethod initializes a new page and adds objects to the new pageAutoreleasePoolPageIn the. Otherwise, go to Step 3.
  3. If there is nohotPageThe callautoreleaseNoPageMethod to create ahotPageAnd add the object to the newly createdpageIn the

HotPage is the AutoreleasePoolPage that is currently in use.

2.3.2 add Add objects

The add method adds an object to AutoreleasePoolPage.

id *add(id obj) { ASSERT(! full()); unprotect(); id *ret = next; // faster than `returnnext-1` because of aliasing *next++ = obj; // store obj at next and point next to protect();return ret;
}
Copy the code

This method is simply a pushdown operation, adding an object to AutoreleasePoolPage and moving the top pointer.

2.3.3, autoreleaseFullPage

The autoreleaseFullPage method reopens a new AutoreleasePoolPage page and adds objects to it.

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 is not empty, use page->childif(page->child) page = page->child; // Otherwise, initialize a new AutoreleasePoolPageelse page = new AutoreleasePoolPage(page);
    } while(page->full()); // Set the appropriate page found to hotPagesetHotPage(page); // Add the object to the hotPagereturn page->add(obj);
}
Copy the code

If not found, initialize a new page, set page to hotPage, and add objects to the page.

2.3.4, autoreleaseNoPage

If hotPage does not currently exist in memory, the autoreleaseNoPage method is called to initialize an AutoreleasePoolPage.

id *autoreleaseNoPage(id obj)
{
    // Install the first page.
    // Initialize an AutoreleasePoolPage
    // If there is no AutoreleasePoolPage in memory, build AutoreleasePoolPage from scratch, i.e. its parent is nil
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    // Initialize AutoreleasePoolPage to hotPage
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    // Add a boundary object (nil)
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    // Add the object to AutoreleasePoolPage
    return page->add(obj);
}
Copy the code

If there is no AutoreleasePoolPage in memory, build AutoreleasePoolPage from scratch, that is, its parent is nil. After initialization, the current page is marked as hotPage, and a POOL_SENTINEL object is first added to the page to ensure that no exceptions are raised when pop is called. Finally, add the object to the automatic release pool.

2.4 pop method

In the previous section we explored objc_autoreleasePoolPush. Now let’s look at objc_autoreleasePoolPop. The essence of objc_autoreleasePoolPop is to call the POP method.

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.
            pop(coldPage()->begin());
        } else {
            // Pool was never used. Clear the placeholder.
            setHotPage(nil);
        }
        return;
    }

    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 pagein place
            // 2. an object is autoreleased with no pool
        } else {
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            returnbadPop(token); }}if (PrintPoolHiwat) printHiwat();

    page->releaseUntil(stop);

    // memory: delete empty children
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill(a);setHotPage(parent);
    } else if(DebugMissingPools && page->empty() && ! page->parent) { // specialcase: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill(a);setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill(a); }else if (page->child->child) {
            page->child->child->kill(a); }}}Copy the code

The above method does several things:

  1. callpageForPointerGets the page where the current token is located.
  2. callreleaseUntilMethod releases objects on the stack untilstop.
  3. callchildthekillMethods.

2.4.1 pageForPointer Find page

The pageForPointer method obtains the first address of the page where the token is located through the operation of the memory address.

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    ASSERT(offset >= sizeof(AutoreleasePoolPage));

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

    return result;
}
Copy the code

Modulating the pointer to the size of the page (4096) yields the offset of the current pointer. Then subtract the offset from the address of the pointer to get the first address.

2.4.2 releaseUntil Releases objects

void releaseUntil(id *stop) 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    
    // Release objects in AutoreleasePoolPage until next points to stop
    while (this->next ! = stop) {// Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        // hotPage can be interpreted as the current page in use
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        // If the page is empty, point to the previous page
        // I guess I can use if here, and I feel I can use if too
        // Because of the structure of AutoreleasePoolPage, it is theoretically impossible to have two consecutive empty pages
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        // obj = page->next; page->next--;
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        // POOL_BOUNDARY is nil and is the sentinel object
        if(obj ! = POOL_BOUNDARY) {// Release the obj objectobjc_release(obj); }}// Reset hotPage
    setHotPage(this);

#if DEBUG
    // we expect any children to be completely empty
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}
Copy the code

AutoreleasePool is actually a bidirectional list of AutoreleasePoolPage, so *stop may not be in the latest AutoreleasePoolPage (hotPage). All AutoreleasePoolPage objects are released until stop. IOS memory Management 1: Tagged Pointer& Reference counting

2.4.3 Kill method

The kill method deletes every page in the bidirectional list

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

    AutoreleasePoolPage *deathptr;
    // Delete each page in a loop
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while(deathptr ! =this);
}
Copy the code

Automatically release pools and threads

The official Using Autorelease Pool Blocks documentation describes the relationship between auto-release pools and threads.

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.

If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

Translated into Chinese:

Each thread in the application maintains its own auto-release pool block stack. If you are writing a foundation-only program, or if you are separating a thread, you need to create your own auto-release pool block. If your application or thread is long-lived and likely to generate a large number of auto-release objects, you should use auto-release pool blocks (such as AppKit and UIKit on the main thread); Otherwise, automatically freed objects will accumulate and the memory footprint will increase. If separate threads do not make Cocoa calls, there is no need to use automatic release pool blocks.

The auto-release pool is closely related to threads, with only one thread per auto-release pool.

4. AutoreleasePool and RunLoop

Auto-release pools are rarely associated with runloops, but if you print [NSRunLoop currentRunLoop] you will find the auto-release pool-related callbacks.

<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
Copy the code

The App starts, apple will give RunLoop registered a number of observers, two of them are associated with automatic release pool, the callback is _wrapRunLoopWithAutoreleasePoolHandler (). \

  • The first oneobserverListening isActivities = 0 x1 (kCFRunLoopEntry), that is, about to enterloop, its callback is called_objc_autoreleasePoolPush()Create an automatic release pool;
  • The secondobserverListening isActivities = 0 xa0 (kCFRunLoopBeforeWaiting | kCFRunLoopExit)That is, listening is ready to go to sleep and about to exitloopTwo events. Call before you are ready to go to sleep, because sleep can be long, so as not to consume resources_objc_autoreleasePoolPop()Release the old release pool and call_objc_autoreleasePoolPush()Create a new one to load event objects to be processed after being woken up. It’s about to quit at the endloopWhen will be_objc_autoreleasePoolPop()Release pool.

5, summary

  1. Automatic release pool is made byAutoreleasePoolPageIt is implemented in a two-way linked list.
  2. When the object callsautoreleaseMethod, the object is addedAutoreleasePoolPageThe stack.
  3. callAutoreleasePoolPage::popMethods are sent to objects on the stackreleaseThe message.
  4. Auto-release pools and threads are closely related, with only one thread per auto-release pool

The resources

  • IOS Memory Management 1: Tagged Pointer& Reference count
  • Using Autorelease Pool Blocks
  • NSAutoreleasePool