preface

As an iOS developer, one of the most common questions in the interview process is: When will an AutoRelease object be released in the ARC environment? If you don’t know how to answer, or you have only a vague idea, then you should definitely not miss this article.

This article will examine the underlying principles of @AutoReleasepool through Runtime OBJC4-756.2 source code, macOS and iOS project examples. Some questions about autorelease and @Autoreleasepool will be answered at the end.

1. A quick chat about ARC and MRC

In iOS 5, Apple has introduced ARC (Automatic Reference Counting) memory management, using the LLVM compiler and Runtime collaboration to automatically manage memory. The LLVM compiler inserts retain, release, and AutoRelease code for OC objects where appropriate at compile time, eliminating the need to manually insert this code under MRC (Manual Reference Counting), thus reducing the workload for developers.

Under MRC, when we don’t need an object, we call the release or autoRelease method torelease it. Calling Release immediately subsets the object’s reference count by one, and if the object’s reference count is zero, the object is destroyed. Calling autoRelease will add the object to the automatic release pool, which will automatically call release on the object at the appropriate moment, so autoRelease is equivalent to delaying the release of the object.

Under ARC, the autorelease method is disabled and we can use the __autoreleasing modifier to register objects into the auto-release pool. For more details, see iOS – Memory Management Cliche iii: The ARC Launch — The Ownership Modifier.

2. Automatically release the pool

The official documentation

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, You typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, It may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.

The AppKit and UIKit frameworks create an autorelease pool on the main thread at the start of each RunLoop and destroy it at the end of each loop. When the RunLoop is destroyed, all autoRelease objects in the autorelease pool are released. Normally we don’t need to manually create an autorelease pool, but if we create a lot of temporary AutoRelease objects in the loop, manually creating an autorelease pool to manage those objects can greatly reduce the memory spike.

Create an automatic release pool

  • inMRCBelow, can useNSAutoreleasePoolor@autoreleasepool. It is recommended to use@autoreleasepool“, Apple said it was better thanNSAutoreleasePoolAbout six times faster.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
Copy the code

NSAutoreleasePool = pool release = pool drain = NSAutoreleasePool = pool release Objective-c itself does support GC, but it is limited to MacOS development. IOS development uses RC. [Pool release] has the same effect as [Pool drain] in iOS RC environment, but in GC environment drain triggers GC and release does nothing. It is better to use [pool drain] because it is more compatible with the system and because it is different from the release of normal objects. (Note: When Apple introduced ARC, it said that GC was deprecated in OS X Mountain Lion V10.8 and ARC was used instead)

  • And in theARCIt has been bannedNSAutoreleasePoolClass created to automatically release pools that can only be used@autoreleasepool.
@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}
Copy the code

3. Principle analysis

Let’s take a look at the underlying principles of @AutoReleasepool using the macOS project. The Main () function in the macOS project does nothing but put @autoreleasepool.

int main(int argc, const char * argv[]) {
    @autoreleasepool {}
    return 0;
}
Copy the code

__AtAutoreleasePool

The above code is converted to C++ code by Clang clang-rewrite-objc main.m.

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

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ 
    { __AtAutoreleasePool __autoreleasepool;  }
    return 0;
}
Copy the code

You can see:

  • @autoreleasepoolThe bottom layer is to create a__AtAutoreleasePoolStructure object;
  • When creating a__AtAutoreleasePoolStructure is called in the constructorobjc_autoreleasePoolPush()Function and returns oneatautoreleasepoolobj(POOL_BOUNDARYThe location of the memory to be stored, described below);
  • The release of__AtAutoreleasePoolStructure is called in the destructorobjc_autoreleasePoolPop()Function, and willatautoreleasepoolobjThe incoming.

AutoreleasePoolPage

Let’s go to the Runtime ObjC4 source code and see the implementation of the two functions mentioned above.

// NSObject.mm
void * objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

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

Objc_autoreleasePoolPush () and objc_autoreleasePoolPop() call the AutoreleasePoolPage class methods push() and pop(). So the underlying @Autoreleasepool is implemented using the AutoreleasePoolPage class.

Let’s look at the definition of the AutoreleasePoolPage class:

class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  // EMPTY_POOL_PLACEHOLDER: placeholder for empty automatic pool release
#   define POOL_BOUNDARY nil                // POOL_BOUNDARY: indicates the sentinel object
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;   // To mark the object that has been released
    static size_t const SIZE =              // Each Page object occupies 4096 bytes of memory
#if PROTECT_AUTORELEASEPOOL                 // PAGE_MAX_SIZE = 4096
        PAGE_MAX_SIZE;  // must be muliple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);  // The number of pages

    magic_t const magic;                // To verify that the structure of the Page is complete
    id *next;                           // Point to the next location where the address of the AutoRelease object can be stored, initialized to begin()
    pthread_t const thread;             // Point to the current thread
    AutoreleasePoolPage * const parent; // Point to the parent. Parent for the header is nil
    AutoreleasePoolPage *child;         // Point to the child, and the tail child is nil
    uint32_t const depth;               // The Page depth increases from 0uint32_t hiwat; . }Copy the code

Note: This article uses objC4-756.2 source code for analysis. In objC4-779.1, AutoreleasePoolPage inherits from AutoreleasePoolPageData as follows. But the principle doesn’t change. It doesn’t affect the analysis.

/ / objc4-779.1
// NSObject.mm
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
	friend struct thread_data_t;

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

    // 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

    // SIZE-sizeof(*this) bytes of contents follow. }// NSObject-internal.h
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)
	{
	}
};
Copy the code

There may be multiple AutoreleasePoolPage objects throughout the course of the program. From its definition we can know:

  • Automatically release the pool (that is, allAutoreleasePoolPageObject) byThe stackFor node passTwo-way linked listThe form of;
  • Automatic release of pools corresponding to threads;
  • eachAutoreleasePoolPageObject takes up4096Bytes of memory, where56The remaining space (4040Bytes) for storageautoreleaseObject address.

The memory distribution is as follows:

Let’s analyze the implementation of push(), pop(), and autoRelease methods through the source code.

POOL_BOUNDARY

Before we look at these methods, let’s introduce POOL_BOUNDARY.

  • POOL_BOUNDARYThe previous life is calledPOOL_SENTINEL, called sentinel object or boundary object;
  • POOL_BOUNDARYTo distinguish different automatic release pools, to solve the problem of automatic release pools nesting;
  • Is called whenever an automatic release pool is createdpush()Method will place aPOOL_BOUNDARYPush and return the memory address where it is stored;
  • When added to the automatic release poolautoreleaseObject, willautoreleaseObjects with at least one memory address in front of themPOOL_BOUNDARY;
  • Is called when an automatic release pool is destroyedpop()Method and pass aPOOL_BOUNDARYIs sent to each object in turn, starting with the last object in the automatic release poolreleaseMessage until you come across thisPOOL_BOUNDARY.

push

    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) { // Enter the debug state when an error occurs
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);  // Pass the POOL_BOUNDARY sentinel object
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
Copy the code

The push() method is called when an automatic release pool is created. The autoreleaseFast() method is called in the push() method and the POOL_BOUNDARY sentinel object is passed in.

Let’s look at the implementation of the autoreleaseFast() method:

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();     // The last Page in the two-way list
        if(page && ! page->full()) {// If the current Page exists and is not full
            return page->add(obj);                 // Add the autoRelease object to the current Page;
        } else if (page) {                  // If the current Page exists but is full
            return autoreleaseFullPage(obj, page); // Create a new Page and add the AutoRelease object to it
        } else {                            // If the current Page does not exist, the Page has not been created
            return autoreleaseNoPage(obj);         // Create the first Page and add the AutoRelease object to it}}Copy the code

AutoreleaseFast () first calls the hotPage() method to get the Page that is not full. The AutoreleasePoolPage class defines the memory size of each Page as 4096 bytes. When the Page is full, a new Page is created. The hotPage() method is used to get this newly created Page that is not fully filled. AutoreleaseFast () can be executed in three ways:

  • (1) the currentPageIf it exists and is not full, passpage->add(obj)willautoreleaseObject pushes, that is, adds to the currentPage;
  • (2) the currentPageExists but is full when passedautoreleaseFullPage(obj, page)Create a new onePageAnd willautoreleaseObject;
  • (3) the currentPageIt doesn’t exist, it hasn’t been created yetPageThrough theautoreleaseNoPage(obj)Create the first onePageAnd willautoreleaseObject.

Let’s look at the implementation of the three methods mentioned above:

    id *add(idobj) { assert(! full()); unprotect();id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }
Copy the code

Page ->add(obj) adds an AutoRelease object to the page where the next pointer points to, and returns the object’s position.

    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);
    }
Copy the code

The autoreleaseFullPage() method finds the last Page through the Page’s child pointer through the while loop.

  • If the last onePageFull, passpage->add(obj)willautoreleaseObject to the last onePage;
  • If the last onePageIf it’s full, create a new onePageAnd will thePageSet tohotPageThrough thepage->add(obj)willautoreleaseObject.
    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 yetassert(! 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", 
                         pthread_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();
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
    }
Copy the code

The autoreleaseNoPage() method creates the first Page. This method determines whether an empty autofree pool exists, and if not, generates a placeholder for an empty autofree pool via setEmptyPoolPlaceholder(). Next, create the first Page and set it to hotPage. Finally a POOL_BOUNDARY is added to the Page and the next position of POOL_BOUNDARY is returned.

Summary: This is the implementation of the push operation, adding a POOL_BOUNDARY to the automatic release pool and returning the memory address where it was stored. Then each time an object calls the autoRelease method, its memory address is added to the auto-release pool.

autorelease

The autoRelease method function call stack is as follows, please refer to iOS – Cliche memory Management (4) : Memory management method source code Analysis.

// NSObject.mm1) objc_autorelease// objc-object.h (2) objc_object: : autorelease// NSObject.mm(3) autorelease (4) _objc_rootAutorelease// objc-object.h(5) objc_object: : rootAutorelease// NSObject.mm6 objc_object: : rootAutorelease2 7 AutoreleasePoolPage: : autoreleaseCopy the code

The autoRelease method of the AutoreleasePoolPage class is implemented as follows:

static inline id autorelease(id obj) { assert(obj); assert(! obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); assert(! dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); return obj; }Copy the code

As you can see, the object that calls the autoRelease method is also added to the Page by the autoreleaseFast() method resolved above.

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.
                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 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.
                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();
            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);
        } 
        else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if(page->child->child) { page->child->child->kill(); }}}Copy the code

The token passed to the pop() method is the address POOL_BOUNDARY in the Page. When the autorelease pool is destroyed, the pop() method is called torelease all autoRelease objects in the autorelease pool (actually starting with the last autoRelease object pushed in the autorelease pool, sending a release message to each of them in turn, Until this POOL_BOUNDARY is encountered. The pop() method executes as follows:

  • (1) to determinetokenIsn’t itEMPTY_POOL_PLACEHOLDERIf yes, empty the automatic release pool.
  • ② If not, passpageForPointer(token)gettokenWhere thePageThe first to automatically release the poolPage);
  • (3) bypage->releaseUntil(stop)Is automatically released from the poolautoreleaseAll objects are released, and the parameters are passedstopIs thePOOL_BOUNDARYThe address;
  • ④ Judge the current situationPageIf there is a childPageIf any, destroy them.

The release of the autoRelease object in pop() is in the releaseUntil() method. Here’s a look at the implementation:

    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
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;  // The next pointer points to the position after the last object, so we need to subtract 1 first
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            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
    }
Copy the code

The releaseUntil() method simply sends a release message from the last autoRelease object pushed through a while loop until the POOL_BOUNDARY is encountered.

AutoreleasePoolPage()

Let’s look at the process of creating a Page. The parameter of the AutoreleasePoolPage() method is parentPage, the depth of the newly created Page is increased by one, the initial position of the next pointer is pointed to begin, and the parent pointer of the newly created Page is pointed to parentPage. The parentPage child pointer is pointed to itself, which forms a two-way linked list structure.

    AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
        : magic(), next(begin()), thread(pthread_self()),
          parent(newParent), child(nil), 
          depth(parent ? 1+parent->depth : 0), 
          hiwat(parent ? parent->hiwat : 0)
    { 
        if(parent) { parent->check(); assert(! parent->child); parent->unprotect(); parent->child =this;
            parent->protect();
        }
        protect();
    }
Copy the code

Begin, end, empty, and full

Let’s look at the implementation of the begin, end, empty, and full methods.

  • beginThe address of:PageMy own address +PageSize of object56Bytes;
  • endThe address of:PageMy own address +4096Bytes;
  • emptyjudgePageThe condition for whether it is null isnextIs the address equal tobegin;
  • fulljudgePageThe condition of whether it is full isnextIs the address equal toend(top of the stack).
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

    bool empty() {
        return next == begin();
    }

    bool full() { 
        return next == end();
    }
Copy the code

Summary:

  • pushThe action is to add one to the automatic release poolPOOL_BOUNDARY, and return the memory location where it was stored;
  • And then every time there’s an object callautoreleaseMethod to add its memory address to the automatic release pool.
  • popThe operation is passing in aPOOL_BOUNDARYThe memory address from the last pushautoreleaseObject is automatically released from the poolautoreleaseObjects are all released (actually sending one to themreleaseMessage) until it came across thisPOOL_BOUNDARY .

4. Check whether the pool is automatically released

The following private functions can be used to see how the pool is automatically freed:

extern void _objc_autoreleasePoolPrint(void);
Copy the code

5. Use the macOS project example

Due to the iOS project, the system registered some objects in the automatic release pool. To help you understand @autoreleasepool, let’s look at some macOS engineering code examples, the memory distribution of AutoreleasePoolPage, and the _objc_autoreleasePoolPrint() private function to help you understand @autoreleasepool.

Note:

  • Due to theARCEnvironment cannot be calledautoreleaseSo you need to switch the project toMRCThe environment.
  • If you are usingARC, can be used__autoreleasingOwnership modifier insteadautoreleaseMethods.

A single @ autoreleasepool

int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();     // print1
    @autoreleasepool {
        _objc_autoreleasePoolPrint(); // print2
        HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
        HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
        _objc_autoreleasePoolPrint(); // print3
    }
    _objc_autoreleasePoolPrint();     // print4
    return 0;
}
Copy the code

// Automatically release the pool
objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122] :0 releases pending. // There are no objects in the automatic release pool
objc[68122] : [0x102802000]... PAGE (hot) (cold) objc[68122] : # # # # # # # # # # # # ## objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122] :1 releases pending. // There is one object in the pool. This object is POOL_BOUNDARY
objc[68122] : [0x102802000]... PAGE (hot) (cold) objc[68122] : [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68122] : # # # # # # # # # # # # ## objc[68122]: ############## (print3)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122] :3 releases pending. // There are currently 3 objects in the automatic release pool
objc[68122] : [0x102802000]... PAGE (hot) (cold) objc[68122] : [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68122] : [0x102802040]       0x100704a10  HTPerson          //p1
objc[68122] : [0x102802048]       0x10075cc30  HTPerson          //p2
objc[68122] : # # # # # # # # # # # # ## objc[68156]: ############## (print4)
objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68156] :0 releases pending. // There are no objects in the autoreleasepool pool because the @autoreleasepool scope ended and the pop method was called torelease the objects
objc[68156] : [0x100810000]... PAGE (hot) (cold) objc[68156] : # # # # # # # # # # # # # #Copy the code

Nested @ autoreleasepool

int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();             // print1
    @autoreleasepool { //r1 = push()
        _objc_autoreleasePoolPrint();         // print2
        HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
        HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
        _objc_autoreleasePoolPrint();         // print3
        @autoreleasepool { //r2 = push()
            HTPerson *p3 = [[[HTPerson alloc] init] autorelease];
            _objc_autoreleasePoolPrint();     // print4
            @autoreleasepool { //r3 = push()
                HTPerson *p4 = [[[HTPerson alloc] init] autorelease];
                _objc_autoreleasePoolPrint(); // print5
            } //pop(r3)
            _objc_autoreleasePoolPrint();     // print6
        } //pop(r2)
        _objc_autoreleasePoolPrint();         // print7
    } //pop(r1)
    _objc_autoreleasePoolPrint();             // print8
    return 0;
}
Copy the code

// Automatically release the pool
objc[68285]: ############## (print1)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :0 releases pending. // There are no objects in the automatic release pool
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : # # # # # # # # # # # # ## objc[68285]: ############## (print2)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :1 releases pending. // There is one object in the automatic release pool
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68285] : # # # # # # # # # # # # ## objc[68285]: ############## (print3)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :3 releases pending. // There are currently 3 objects in the autoreleasepool (1 @autoreleasepool)
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68285] : [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285] : [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285] : # # # # # # # # # # # # ## objc[68285]: ############## (print4)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :5 releases pending. // There are currently 5 objects in the autoreleasepool (2 @autoreleasepool)
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68285] : [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285] : [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285] : [0x102802050]  ################  POOL 0x102802050  //POOL_BOUNDARY
objc[68285] : [0x102802058]       0x1005065b0  HTPerson          //p3
objc[68285] : # # # # # # # # # # # # ## objc[68285]: ############## (print5)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :7 releases pending. // There are currently 7 objects in autoreleasepool (3 @autoreleasepool)
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68285] : [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285] : [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285] : [0x102802050]  ################  POOL 0x102802050  //POOL_BOUNDARY
objc[68285] : [0x102802058]       0x1005065b0  HTPerson          //p3
objc[68285] : [0x102802060]  ################  POOL 0x102802060  //POOL_BOUNDARY
objc[68285] : [0x102802068]       0x100551880  HTPerson          //p4
objc[68285] : # # # # # # # # # # # # ## objc[68285]: ############## (print6)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :5 releases pending. // There are currently 5 objects in the autoreleasepool (the third @autoreleasepool is released)
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : [0x102802038]  ################  POOL 0x102802038
objc[68285] : [0x102802040]       0x100707d80  HTPerson
objc[68285] : [0x102802048]       0x100707de0  HTPerson
objc[68285] : [0x102802050]  ################  POOL 0x102802050
objc[68285] : [0x102802058]       0x1005065b0  HTPerson
objc[68285] : # # # # # # # # # # # # ## objc[68285]: ############## (print7)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :3 releases pending. // There are currently three objects in the autoreleasepool (the second and third @autoreleasepool are released)
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : [0x102802038]  ################  POOL 0x102802038
objc[68285] : [0x102802040]       0x100707d80  HTPerson
objc[68285] : [0x102802048]       0x100707de0  HTPerson
objc[68285] : # # # # # # # # # # # # ## objc[68285]: ############## (print8)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285] :0 releases pending. // There are no objects in the autoreleasepool currently (all 3 @autoreleasepool have been released)
objc[68285] : [0x102802000]... PAGE (hot) (cold) objc[68285] : # # # # # # # # # # # # # #Copy the code

Complex situation @Autoreleasepool

The AutoreleasePoolPage class defines that the AutoreleasePoolPage pool (that is, all AutoreleasePoolPage objects) is composed of stack nodes in the form of a two-way list. Whenever the Page is full, a new Page is created and set to hotPage, and the first Page to coldPage. Let’s look at multiple pages and multiple @AutoReleasepool nesting.

int main(int argc, const char * argv[]) {
    @autoreleasepool { //r1 = push()
        for (int i = 0; i < 600; i++) {
            HTPerson *p = [[[HTPerson alloc] init] autorelease];
        }
        @autoreleasepool { //r2 = push()
            for (int i = 0; i < 500; i++) {
                HTPerson *p = [[[HTPerson alloc] init] autorelease];
            }
            @autoreleasepool { //r3 = push()
                for (int i = 0; i < 200; i++) {
                    HTPerson *p = [[[HTPerson alloc] init] autorelease];
                }
                _objc_autoreleasePoolPrint();
            } //pop(r3)
        } //pop(r2)
    } //pop(r1)
    return 0;
}
Copy the code

An AutoreleasePoolPage object has a memory size of 4096 bytes, and its own member variables occupy 56 bytes of memory, so the remaining 4040 bytes are used to store the memory address of the AutoRelease object. And because the 64-bit next OC object pointer memory is 8 bytes, so a Page can store 505 object addresses. POOL_BOUNDARY is also an object because it has a value of nil. So the automatic release pool memory distribution of the above code is shown below.

objc[69731] : # # # # # # # # # # # # ##
objc[69731]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[69731] :1303 releases pending. // There are currently 1303 objects in the automatic release pool (3 POOL_BOUNDARY and 1300 HTPerson instances)
objc[69731] : [0x100806000]... PAGE (full) (cold)/* First PAGE, full = full, cold = coldPage */
objc[69731] : [0x100806038]  ################  POOL 0x100806038    //POOL_BOUNDARY
objc[69731] : [0x100806040]       0x10182a040  HTPerson            //p1
objc[69731] : [0x100806048].../ /...
objc[69731] : [0x100806ff8]       0x101824e40  HTPerson            //p504
objc[69731] : [0x102806000]... PAGE (full)/* The second PAGE */
objc[69731] : [0x102806038]       0x101824e50  HTPerson            //p505
objc[69731] : [0x102806040].../ /...
objc[69731] : [0x102806330]       0x101825440  HTPerson            //p600
objc[69731] : [0x102806338]  ################  POOL 0x102806338    //POOL_BOUNDARY
objc[69731] : [0x102806340]       0x101825450  HTPerson            //p601
objc[69731] : [0x102806348].../ /...
objc[69731] : [0x1028067e0]       0x101825d90  HTPerson            //p1008
objc[69731] : [0x102804000]... PAGE (hot)/* The third PAGE, hot = hotPage */
objc[69731] : [0x102804038]       0x101826dd0  HTPerson            //p1009
objc[69731] : [0x102804040].../ /...
objc[69731] : [0x102804310]       0x101827380  HTPerson            //p1100
objc[69731] : [0x102804318]  ################  POOL 0x102804318    //POOL_BOUNDARY
objc[69731] : [0x102804320]       0x101827390  HTPerson            //p1101
objc[69731] : [0x102804328].../ /...
objc[69731] : [0x102804958]       0x10182b160  HTPerson            //p1300
objc[69731] : # # # # # # # # # # # # # #Copy the code

6. Use an iOS project example

As you can see from the macOS project example above, at the end of the @autoreleasepool brace, the pop() method on Page is called to send a release message to the autoRelease object in @Autoreleasepool.

So in an iOS project, when is an Autorelease object in a method released? There are two cases of systematic intervention release and manual intervention release.

  • System intervention release is not specified@autoreleasepoolAll,autoreleaseObjects are owned by the main threadRunLoopTo create the@autoreleasepoolTo manage.
  • Manual intervention release willautoreleaseObject that we created manually@autoreleasepoolIn the.

Again, the analysis is performed in the MRC environment.

System intervention release

Let’s first look at the differences between the main() function in Xcode 11 iOS applications and the older version.

// Xcode 11
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Copy the code
// Old version of Xcode
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil.NSStringFromClass([AppDelegate class])); }}Copy the code

Note: There is an explanation online for @autoreleasepool in the main() function of the iOS project: The @Autoreleasepool in the main() function of the iOS project is responsible for the release of all the autorelease objects in the application. In fact, this explanation is wrong. If your program uses the AppKit or UIKit framework, the RunLoop on the main thread will create and process @Autoreleasepool on each iteration of the event loop. That is, all autorelease objects in an application are managed by @Autoreleasepool created by RunLoop. @AutoReleasepool in main() is just responsible for managing autoRelease objects in its scope. The nesting of @Autoreleasepool is mentioned in the MacOS Project Example Analysis section above. In older versions of Xcode, the main function had the entire application run (UIApplicationMain) in @autoReleasepool, and the RunLoop for the main thread was created in UIApplicationMain. So RunLoop creates @Autoreleasepool nested inside the @Autoreleasepool of the main function. RunLoop will pop and push the autoreleasepool in each event loop (more on this below), but its pop will only release the object after its POOL_BOUNDARY. It does not affect @autoreleasepool in the outer main function. What are the changes to the main function in the new version of Xcode 11? In previous versions, @Autoreleasepool was used to run the entire application, and because of the RunLoop, @Autoreleasepool scope would not end until the application ended. This means that the AutoRelease object in @AutoReleasepool in the main function is not released until the program ends. In Xcode 11, the UIApplicationMain function that triggers the RunLoop main thread is placed outside @Autoreleasepool. This ensures that the AutoRelease object in @Autoreleasepool is released immediately after the program starts. As the comment in the new version of @AutoReleasepool says “Setup code that might create autoreleased objects goes here.” (above), you can place autorelease objects here.

Let’s look at an example of a “system intervention release” situation:

- (void)viewDidLoad {
    [super viewDidLoad];    
    HTPerson *person = [[[HTPerson alloc] init] autorelease];    
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];    
    NSLog(@"%s", __func__);
}

// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[HTPerson dealloc]
// -[ViewController viewDidAppear:]
Copy the code

You can see that the Person object that called the autoRelease method is not freed after the viewDidLoad method ends, it’s freed after the viewWillAppear method ends, which means that when the viewWillAppear method ends, The pop() method is called to release the Person object. This is actually controlled by RunLoop. Here’s how RunLoop relates to @Autoreleasepool.

RunLoop with @ autoreleasepool

To learn this, you need to understand RunLoop’s event loop mechanism and its six active states. Check out my article:

RunLoop (3) : Event Loop Mechanism

IOS registers two observers in the RunLoop of the main thread.

  • 1.ObserverListen to thekCFRunLoopEntryEvent, will be calledobjc_autoreleasePoolPush();
  • The secondObserver

(1) Listen for kCFRunLoopBeforeWaiting event, call objc_autoreleasePoolPop(), objc_autoreleasePoolPush(); (2) If the kCFRunLoopBeforeExit event is monitored, objc_autoreleasePoolPop() is called.

Therefore, the timing of the autoRelease object that the system intervenes torelease in the iOS project is controlled by the RunLoop and will be released at the end of each current RunLoop loop. The above person object is released after the viewWillAppear method, indicating that viewDidLoad and viewWillAppear are in the same loop.

  • kCFRunLoopEntry: About to enterRunLoopIs automatically created__AtAutoreleasePoolStructure object, and callobjc_autoreleasePoolPush()Function.
  • kCFRunLoopBeforeWaitingIn:RunLoopWhen it is about to sleep, it will automatically destroy one__AtAutoreleasePoolObject, callobjc_autoreleasePoolPop(). And I’m going to create a new one__AtAutoreleasePoolObject and callobjc_autoreleasePoolPush().
  • kCFRunLoopBeforeExit, is about to exitRunLoopIs automatically destroyed when the last one is created__AtAutoreleasePoolObject and callobjc_autoreleasePoolPop().

Manual intervention release

Let’s look again at manual intervention release.

- (void)viewDidLoad {
    [super viewDidLoad];    
    @autoreleasepool {
        HTPerson *person = [[[HTPerson alloc] init] autorelease];  
    }  
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];    
    NSLog(@"%s", __func__);
}

// -[HTPerson dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]
Copy the code

As you can see, the AutoRelease object added to the manually specified @AutoReleasepool is released at the end of the @Autoreleasepool curly bracket, and is not controlled by RunLoop.

Issues related to

Q: When is an AutoRelease object released in an ARC environment?

Going back to our original interview question, when is an Autorelease object released in an ARC environment? We respond to the two situations of subsystem intervention release and manual intervention release.

Q: Do I need to manually add @autoreleasepool in ARC?

The AppKit and UIKit frameworks create and process @Autoreleasepool during each iteration of the RunLoop event loop, so you usually don’t have to create @Autoreleasepool yourself or even know how to write the code to create @Autoreleasepool. However, there are situations where you need to create @Autoreleasepool yourself.

For example, if we need to create a lot of temporary AutoRelease objects in the loop, manually adding @autoreleasepool to manage these objects can greatly reduce memory spikes. In memory intensive scenarios such as alloc image data in a for loop, @Autoreleasepool needs to be added manually.

Apple gives three situations where you need to manually add @Autoreleasepool:

  • (1) If you write a program that is not based on a UI framework, such as a command line tool;
  • If you write a loop that creates a large number of temporary objects;

    You can use it in a loop@autoreleasepoolThese objects are processed before the next iteration. Used in a loop@autoreleasepoolHelps reduce the maximum memory footprint of an application.
  • ③ If you create a helper thread.

    Once the thread starts executing, it must create its own@autoreleasepool; Otherwise, your application will have a memory leak.

Q: What happens if the autoRelease method is called on the NSAutoreleasePool object?

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [pool autorelease];
Copy the code

A: throw an exception NSInvalidArgumentException and led to a program Crash, abnormal reason: unable to call autorelease NSAutoreleasePool object.

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '*** -[NSAutoreleasePool autorelease]: Cannot autorelease an autorelease pool' 
Copy the code