To prepare

  • Objc4-818.2 – the source code
  • NSAutoreleasePool is an official Apple document
  • Apple’s official documentation for Run Loops
  • Apple official documentation Using Autorelease Pool Blocks

1. Introduction to Autorelease

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, releasing all autoRelease objects in the pool at the end of each RunLoop. Normally we do not need to manually create an autorelease pool, but if we create a lot of temporary Autorelease objects in a loop, manually creating an autorelease pool to manage these objects can greatly reduce memory spikes.

Analysis of the principle of automatic release pool

2.1 @ autoreleasepool {}

Create a MAC project with the following code:

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

Run clang-rewrite-objc main.m -o main. CPP to get main. CPP:

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { 
        __AtAutoreleasePool __autoreleasepool; 
    }
    return 0;
}
Copy the code
  • The code is converted toC++After the source code,@autoreleasePoolBlock is__AtAutoreleasePoolStruct creation, called when it is createdThe constructorIs called when the scope endsThe destructor.

Look again at the __AtAutoreleasePool structure:

struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush(); } ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj); } void * atautoreleasepoolobj; };Copy the code
  • The constructor is calledobjc_autoreleasePoolPushFunction and returns a boundary objectatautoreleasepoolobj.
  • The destructor is calledobjc_autoreleasePoolPopFunction and pass in the boundary objectatautoreleasepoolobj.

So, we can simplify the main function as follows:

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

2.2 objc_autoreleasePoolPush与objc_autoreleasePoolPop

Objc_autoreleasePoolPush and objc_autoreleasePoolPop

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

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

2.3 Structure analysis of AutoreleasePoolPage

AutoreleasePoolPage is a C++ class that inherits from AutoreleasePoolPageData.

class AutoreleasePoolPage : private AutoreleasePoolPageData { public: static size_t const SIZE = #if PROTECT_AUTORELEASEPOOL PAGE_MAX_SIZE; // must be multiple of vm page size #else PAGE_MIN_SIZE; // 4096 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); static size_t const MAX_FAULTS = 2; # define EMPTY_POOL_PLACEHOLDER ((id*)1) # define pool_placeholder nil } struct AutoreleasePoolPageData { magic_t const magic; __unsafe_unretained id *next; // Repeat AutoreleasePoolPage. // Points to the next location of the newly added autoreleased object, initialized to begin() 8-byte pthread_t const thread; AutoreleasePoolPage * const parent; Parent = nil 8 bytes AutoreleasePoolPage *child; Uint32_t const depth; // uint32_t hiwat; // The AutoreleasePoolPageData constructor (__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
  • Automatic release pool is made up of severalAutoreleasePoolPageFormed a bidirectional linked list structure,AutoreleasePoolPagehaveparentandchildPointers to the previous one and the next onepage.
  • The current onepageThe space is taken up (eachAutorelePoolPageIs 4096 bytes), a new one will be createdAutorelePoolPageObject is attached to the linked list, and later Autorelease objects are added to the new onepageIn the.
  • In addition, whennext== begin()Said,AutoreleasePoolPageIs empty; whennext == end()Said,AutoreleasePoolPageIs full.

Why multiple pages?

  1. If all the pages are on the same page, the operation is very complicated, and one object has to wait for the others to operate.
  2. Full pages do not operate, only the operation is not full of that, the efficiency is higher.
  3. It doesn’t have to be a contiguous piece of memory.

2.4 AutoreleasePoolPage Space size

In the AutoreleasePoolPage structure above, we can see that the size of the structure is PAGE_MIN_SIZE:

#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)
Copy the code
  • 1 << 12Is equal to the4096, that is,4K.

Verify the following:

  • In the example505Object, added504Then he created another onehot pageTo illustrate apageYou can add at most504An object.
  • 504 * 8 + 56(Member variable size) +8(boundary object) =4096It is the same size as above.

2.5 Sentinel object/boundary object (POOL_BOUNDARY)

In the AutoreleasePoolPage structure above, we see that there is a boundary object (sentinel object) :POOL_BOUNDARY

  • Boundary objects are justnil“, and its function is actually to play the role of an identifier.
  • Whenever you callobjc_autoreleasePoolPushMethod, will bePOOL_BOUNDARYIn the currentpageAnd returns the boundary object.
  • In the callobjc_autoreleasePoolPopMethod, the boundary object is passed in as a parameter, which is then sent to the objects in the release pool by the automatic release poolreleaseMessage until the first boundary object is found.

validation

Apple provides a debug function: _objc_autoreleasePoolPrint, which can be used to print the creation information of the automatic release pool. See the following test code:

extern void _objc_autoreleasePoolPrint(void); Extern = extern; // Extern = extern; // extern = extern; const char * argv[]) { @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; _objc_autoreleasePoolPrint(); } return 0; }Copy the code

View the print result:

Objc :<NSObject: 0x10127eA90 > ObjC [10265]: ############## objc[10265]: AUTORELEASE POOLS for thread 0x1000ebe00 objc[10265]: 2 releases pending. objc[10265]: [0x10200b000] ................ PAGE (hot) (cold) objc[10265]: [0x10200b038] ################ POOL 0x10200b038 objc[10265]: [0x10200b040] 0x10127ea90 NSObject objc[10265]: ##############Copy the code
  • You can see that the auto-release pool added objects are printed out, wherex10127ea90 NSObjectWe created itobjcObject, one morePOOL 0x10200b038The address is the sentinel object.

2.6 objc_autoreleasePoolPush

After the previous analysis, objc_autoreleasePoolPush ultimately calls the AutoreleasePoolPage push method, which is implemented as follows:

static inline void *push() { return autoreleaseFast(POOL_BOUNDARY); } static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && ! page->full()) { return page->add(obj); } else if (page) {return autoreleaseFullPage(obj, page); } else {return autoreleaseNoPage(obj); }} id *add(id obj) {id *ret; ret = next; // faster than `return next-1` because of aliasing *next++ = obj; } static __attribute__((noinline)) id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { do { if (page->child) page = page->child; Else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); // set to hotPage return page->add(obj); } static __attribute__((noinline)) id *autoreleaseNoPage(id obj) {// Create a page by constructor AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); // Set hotPage to if (pushExtraBoundary) {page->add(POOL_BOUNDARY); Return page->add(obj); }Copy the code

AutoreleaseFast is called in the push function. There are three cases:

  • The currentpageExists and not satisfied, callpage->add(obj)Method to add an object topageIn the stack, i.enextPoint to.
  • The currentpageExists but full, callautoreleaseFullPageInitialize a new onepage, the callpage->add(obj)Method to add an object topageThe stack.
  • The currentpageDoes not exist when calledautoreleaseNoPageTo create ahotPage, the boundary object (POOL_BOUNDARY) added to thepageStack, and then callpage->add(obj)Method to add an object topageThe stack.

Look again at the AutoreleasePoolPage constructor:

AutoreleasePoolPage(AutoreleasePoolPage *newParent) : AutoreleasePoolPageData(begin(), objc_thread_self(), // current thread newParent, newParent? 1+newParent->depth: 0, // +1 newParent? newParent->hiwat : 0) { if (objc::PageCountWarning ! = -1) { checkTooMuchAutorelease(); } if (parent) { parent->check(); ASSERT(! parent->child); parent->unprotect(); parent->child = this; Parent ->protect(); } protect(); // Add return (id *) ((uint8_t *)this+sizeof(*this)); }Copy the code

2.7 objc_autoreleasePoolPop

Objc_autoreleasePoolPop finally calls the pop method of AutoreleasePoolPage and passes in the POOL_BOUNDARY. This method is implemented as follows:

Static inline void pop(void *token) // POOL_BOUNDARY address {AutoreleasePoolPage *page; id *stop; page = pageForPointer(token); // use POOL_BOUNDARY to find page stop = (id *)token; return popPage<false>(token, page, stop); } template<bool allowDebug> static void popPage(void *token, AutoreleasePoolPage *page, id *stop) { page->releaseUntil(stop); // Send a release message to an object on the stack until the first sentry object is encountered: Delete empty children if (allowDebug && DebugPoolAllocation && Page ->empty()) {AutoreleasePoolPage *parent =  page->parent; // Get parent page->kill(); // Kill setHotPage(parent); Else if (allowDebug && DebugMissingPools && page->empty() &&! page->parent) { page->kill(); setHotPage(nil); } else if (page->child) { if (page->lessThanHalfFull()) { page->child->kill(); } else if (page->child->child) { page->child->child->kill(); } } } void releaseUntil(id *stop) { while (this->next ! = stop) { AutoreleasePoolPage *page = hotPage(); while (page->empty()) { page = page->parent; setHotPage(page); } id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); if (obj ! = POOL_BOUNDARY) { for (int i = 0; i < count + 1; i++) { objc_release(obj); } } } setHotPage(this); } void kill() { 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); }Copy the code

In the above code, first according to the incoming boundary object address to find the boundary object in the page, and then select the latest object in the current page to clean up until the location of the boundary; The cleanup is done by sending a release message to these objects, reducing their reference count by one;

In addition, emptying page objects follows a few rules:

  1. If the currentpageIf less than half of the objects are stored inpageDelete all;
  2. If the currentpageIf you store more than half of it (meaning it’s about to be full), keep onepageTo save creating a newpageThe overhead of;

2.8 Automatic release pool can be nested validation

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[[NSObject alloc] init] autorelease];
        NSLog(@"objc:%@",objc);

        @autoreleasepool {
            NSObject *objc2 = [[[NSObject alloc] init] autorelease];
            NSLog(@"objc2:%@",objc2);

            _objc_autoreleasePoolPrint();
        }
    }
    return 0;
}
Copy the code
The 2021-09-20 21:54:23. 661658 + 0800 KCObjcBuild [16800-319949] objc: < NSObject: 0x101A58AB0 > 2021-09-20 21:54:22.663090 +0800 KCObjcBuild[16800:319949] OBJC :<NSObject 0x1006282B0 > ObjC [16800]: ############## objc[16800]: AUTORELEASE POOLS for thread 0x1000ebe00 objc[16800]: 4 releases pending. objc[16800]: [0x102016000] ................ PAGE (hot) (cold) objc[16800]: [0x102016038] ################ POOL 0x102016038 objc[16800]: [0x102016040] 0x101a58ab0 NSObject objc[16800]: [0x102016048] ################ POOL 0x102016048 objc[16800]: [0x102016050] 0x1006282b0 NSObject objc[16800]: ##############Copy the code
  • As you can see from the print, the auto release pool is added in sequence0x102016038Boundary object ->objcObject – >0x102016048Boundary object ->objc2Object.

When will the autoRelease object be released?

Autorelease objects can be released either by system intervention or by manual intervention.

  • System intervention release is not specified@autoreleasepoolAll,autoreleaseObjects are threaded by the main threadRunLoopTo create the@autoreleasepoolTo manage.
  • Manual intervention release will beautoreleaseObject added to the one we created manually@autoreleasepoolIn the.

3.1 System intervention Release

Let’s create an iOS project with the following code and see the results:

@implementation SSLPerson - (void)dealloc { NSLog(@"%s", __func__); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; SSLPerson *person = [[[SSLPerson 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__); } @end  -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[SSLPerson dealloc] -[ViewController viewDidAppear:]Copy the code
  • As you can see, it’s calledautoreleasemethodspersonThe object is not thereviewDidLoadThe method is released after it finishes, but instead inviewWillAppearMethod is released after the end of the statement inviewWillAppearWhen the method ends, it is calledpop()Method releasepersonObject. This is actually the result ofRunLoopControlled. Let’s talk about itRunLoopand@autoreleasepoolThe relationship between.

3.2 with @ autoreleasepool RunLoop

The auto release operation in RunLoop can be represented as follows:

  • kCFRunLoopEntry: About to enterRunLoopIs automatically created__AtAutoreleasePoolStructure object and callobjc_autoreleasePoolPush()Function.
  • kCFRunLoopBeforeWaitingIn:RunLoopIt automatically destroys one when it is about to sleep__AtAutoreleasePoolObject, callobjc_autoreleasePoolPop(). Then create a new one__AtAutoreleasePoolObject and callobjc_autoreleasePoolPush().
  • kCFRunLoopBeforeExit, is about to quitRunLoopWill automatically destroy the last one created__AtAutoreleasePoolObject and callobjc_autoreleasePoolPop().

So, in iOS engineering, the timing of the release of an autoRelease object is controlled by the RunLoop, and it is released at the end of each loop of the current RunLoop. The above person object is freed after viewWillAppear, indicating that viewDidLoad and viewWillAppear are in the same loop.

3.3 Manually Releasing data

Let’s look at the manual release:

@implementation SSLPerson - (void)dealloc { NSLog(@"%s", __func__); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; @autoreleasepool { SSLPerson *person = [[[SSLPerson 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__); } @end  -[SSLPerson dealloc] -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[ViewController viewDidAppear:]Copy the code

As you can see, the AutoRelease object added manually to the specified @AutoReleasepool will be released when the @AutoReleasepool braces end, independent of RunLoop control.