What is AutoreleasePool

AutoreleasePool is a memory management mechanism used in iOS development. Objects called autoRelease are placed in the cache pool to be released later. When the cache pool needs to be cleared, A release message is sent to these Autoreleased objects.

Create a new Xcode project and change it to MRC:

MRC
retain/release/autorelease

int main(int argc, const char * argv[]) {
    NSLog(@"-A-");
    Coder *coder = [[Coder alloc] init];
    [coder release];
    NSLog(@"-B-");
    return 0;
}

// log
-A-
Coder dealloc
-B-
Copy the code

You create the coder object with alloc, let its reference count increase, and then call the Release method to complete the release. If you use autorelease, you need to use the automatic cache pool, which looks like this:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"-A-");
        Coder *coder = [[[Coder alloc] init] autorelease];
        NSLog(@"-B-");
    }
    NSLog(@"-C-");
    return 0;
}

// log
-A-
-B-
Coder dealloc
-C-
Copy the code

The coder object here is released automatically when it is out of scope of the automatic cache pool.

Not all cases are automatically released out of scope, more on that later.

What did @autoreleasepool do

Convert main.m to C++ code using the xcrun-sdk iphoneos clang-arch arm64-rewrite-objc main.m command.

You’ll see that @Autoreleasepool is converted to a member variable:

__AtAutoreleasePool __autoreleasepool; 
Copy the code

An implementation of the __AtAutoreleasePool structure:

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

__AtAutoreleasePool() is the constructor, called when the structure is created, and ~__AtAutoreleasePool() is the destructor, called when the structure is destroyed.

int main(int argc, const char * argv[]) {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // @autoreleasepool code inside the parentheses
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    return 0;
}
Copy the code

This explains why not all cases are automatically released out of the @Autoreleasepool scope, as this is just a syntactic sugar that essentially calls the Push&PoP method above.

AutoreleasePoolPage

Runtime source address, objC4-723 used here

Check the source code for Push&Pop above:

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

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

This calls the AutoreleasePoolPage class’s Push&Pop function, which I’ll stop here. Let’s look at how AutoreleasePoolPage is structured, with only member variables:

class AutoreleasePoolPage 
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    // ...
}
Copy the code

The next pointer here points to the next location of the newly added AutoRelease object. This is not easy to understand, so I’ll just say AutoreleasePoolPage first.

The autorelease pool is actually a wrapped C++ class, AutoreleasePoolPage, in the form of a bidirectional linked list. Each AutoreleasePoolPage object allocates 4096 bytes of memory (the size of a page of virtual memory), except for the space occupied by instance variables above, and the remaining space is used to stack autoRelease objects. When the AutoreleasePoolPage space is used up, an AutoreleasePoolPage object is created as a linked list and the address of the AutoRelease object is stored in it. As shown in the figure:

Source code analysis

Back to the original C++ code:

void *atautoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(atautoreleasepoolobj);
Copy the code

When we call Push, we get a return value, which is passed in as an argument to Pop. Here’s how it works. Push function Push function Push function Push

/ / simplified
static inline void *push(a) 
{
    id *dest;
    dest = autoreleaseFast(POOL_BOUNDARY);
    return dest;
}
Copy the code

There is a POOL_BOUNDARY that deserves our attention, but if we look at its definition we will see that it is actually a nil equivalent macro definition:

#   define POOL_BOUNDARY nil
Copy the code

That is, POOL_BOUNDARY is just a sentinel value. Enter autoreleaseFast (…). Function:

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if(page && ! page->full()) {/ / 1.
        return page->add(obj);
    } else if (page) {
        / / 2.
        return autoreleaseFullPage(obj, page);
    } else {
        / / 3.
        returnautoreleaseNoPage(obj); }}Copy the code

HotPage refers to the currently used AutoreleasePoolPage node, and coldPage refers to the full list node.

AutoreleasePoolPage = AutoreleasePoolPage = AutoreleasePoolPage

  • 1. The currentpageIf it exists and is not full, the object is added directly to the currentpageIn the.
  • 2. The currentpageIf it exists and is full, create a new onepageAnd add the object to the newly createdpage, and then link the two list nodes.
  • 3. The currentpageIf it does not exist, create the firstpageAnd add the object to the newly createdpageIn the.

Push
POOL_BOUNDARY
Pop
page
Pop

@autoreleasepool {
    @autoreleasepool{}}Copy the code

It is also important to note that this is implemented using a double-linked list. New pages are created only after the current page space is used up. There is not an AutoreleasePoolPage object for each @Autoreleasepool.

Next look at the source of Pop:

/ / simplified
static inline void pop(void *token) 
{   
    AutoreleasePoolPage *page;
    id *stop;
    page = pageForPointer(token);
    stop = (id *)token;
    // 1. POOL_BOUNDARY releases the 'autoreleased' object based on the token
    page->releaseUntil(stop);

// hysteresis: keep one empty child if page is more than half full
    // 2. After releasing the 'Autoreleased' object, destroy the extra pages.
    if (page->lessThanHalfFull()) {
        page->child->kill();
    }
    else if(page->child->child) { page->child->child->kill(); }}Copy the code

Nothing to say here, come to releaseUntil(…) Internal:

/ / simplified
void releaseUntil(id *stop) 
{
    / / 1.
    while (this->next ! = stop) { AutoreleasePoolPage *page = hotPage();/ / 2.
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }
        / / 3.
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }
    / / 4.
    setHotPage(this);
}
Copy the code
  • 1. The external loop iterates one by oneautoreleasedObject until traversed tostopthisPOOL_BOUNDARY
  • 2. If the currenthatPageThere is noPOOL_BOUNDARYThat will behatPageSet to the parent node.
  • 3. To the currentautoreleasedObject to sendreleaseThe message.
  • 4. Configure the configuration againhatPage.

Let’s look at the implementation of autoRelease, which goes directly to the page autoRelease:

/ / simplified
static inline id autorelease(id obj)
{
    id *dest __unused = autoreleaseFast(obj);
    return obj;
}
Copy the code

The same function called in the push operation above, autoreleaseFast, is nothing to say.

Here’s how automatic cache pooling works at the source level.

AutoreleasePool and runloop

For runloop knowledge, see my previous article iOS runloop

The App starts, apple registered in the main thread RunLoop two Observer, the callback is _wrapRunLoopWithAutoreleasePoolHandler, used to deal with automatic buffer pool.

Print the main thread runloop for confirmation.

print(RunLoop.main)
Copy the code

Observer
activities = 0x1
activities = 0xa0
runloop
runloop

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),              / / 1
    kCFRunLoopBeforeTimers = (1UL << 1),       / / 2
    kCFRunLoopBeforeSources = (1UL << 2),      / / 4
    kCFRunLoopBeforeWaiting = (1UL << 5),      / / 32
    kCFRunLoopAfterWaiting = (1UL << 6),       / / 64
    kCFRunLoopExit = (1UL << 7),               / / 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Copy the code

0x1 (equal to 1) corresponds to kCFRunLoopEntry, which creates an automatic release pool by calling _objc_autoreleasePoolPush() in its callback when the first Observer is about to enter the Loop. Its order is -2147483647, the highest priority, ensuring that the creation of the cache pool occurs before all other callbacks.

0xa0 (hexadecimal equals 160 equals 32+128) corresponds to kCFRunLoopBeforeWaiting&kCFRunLoopExit, the second Observer monitors two events: Call _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() torelease the old pool and create a new one when you are ready to go to sleep. Call _objc_autoreleasePoolPop() torelease the automatic release pool when exiting the Loop. The order of this Observer is 2147483647, the lowest priority, ensuring that its release of the cache pool occurs after all other callbacks.

So for our application, autoreleased objects are more likely to be released while Runloop is asleep.

reference

AutoreleasePool also has some practical tips, such as solving memory problems with autoreleased objects in loops, and so on.

Autorelease behind the black screen

Objective-c Autorelease Pool