preface

Memory management has always been the focus of Objective-C. In the MRC environment, we delay the release of memory by calling [obj autoRelease]. In the current ARC environment, we all know that the compiler inserts release/autorelease statements in the appropriate place. We don’t even need to know about AutoReleases to manage memory well. While MRC is almost never used today, it’s still important to understand objective-C’s memory management mechanisms to see how the compiler can help us manage memory. This article is just to record my own study notes.

AutoreleasePool profile

1. What is AutoreleasePool

AutoreleasePool is a mechanism for automatic memory reclaim management in Objective-C development. In order to replace manual memory management by developers, the compiler essentially inserts release, autorelease, and other memory release operations in the appropriate place. When the autoRelease method is called, objects are released into the autorelease pool to delay dealloc release. When the cache pool needs to clear dealloc, these Autorelease objects are released.

2. When objects are released (ARC rule)

It is common to say that objects are freed when the current scope braces end. Here is a simple example in an ARC environment 🌰 : First create a ZHPerson class:

//// ZHPerson.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ZHPerson : NSObject

+(instancetype)object;
@end

////ZHPerson.m

#import "ZHPerson.h"
@implementation ZHPerson

-(void)dealloc
{
    NSLog(@"ZHPerson dealloc");
}
+(instancetype)object
{
    return [[ZHPerson alloc] init];
}
@end
Copy the code

Then import the header zhPerson. h in viewController.m and write code like this:

__weak id temp = nil;
{
    ZHPerson *person = [[ZHPerson alloc] init];
    temp = person;
}
NSLog(@"temp = %@",temp);
Copy the code

To explain this code: make one statement first__weakvariabletempBecause the__weakOne of the properties of a variable is that it doesn’t affect the life cycle of the object it points to, and then let the variabletempcreation-orientedpersonObject with the following output:This is beyondperson“, it is released, seems to be normal.

Change the method above to create an object:

__weak id temp = nil;
{
    ZHPerson *person = [ZHPerson object];
    temp = person;
}
NSLog(@"temp = %@",temp);
Copy the code

The output is as follows:herepersonThe object exists outside its scope and is released lazily, that is, it is called internallyautorelease Methods.

Summary:

Enquiries revealed that: Methods such as alloc, copy, mutableCopy, and new are marked __attribute((ns_returns_retained)) by default, An object created using these methods is appended by the compiler with a retain/release code around the calling method, so that it is released at the end of the scope. Retained as an __attribute((ns_returns_not_retained)), the compiler automatically adds an autorelease method inside the method, and the created object is registered with the autorelease pool. Release is delayed until destruction is completed.

3. Create the AutoreleasePool display

1. Create an MRC

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; Object = [[NSObject alloc] init]; //3. Call the object autoRelease method [object autoRelease]; //4. Discard the NSAutoreleasePool object and send the release message [pool drain] to the object in the release pool;Copy the code

2. Creation in ARC

@autoreleasepool {//LLVM will insert the autoRelease method id object = [[NSObject alloc] init]; }Copy the code

As mentioned earlier, whenever an object calls the AutoRelease method, it actually puts the object into the current AutoreleasePool. When the current AutoreleasePool is released, The Release method is called one by one on the objects added to the AutoreleasePool. In an ARC environment, you don’t need to be particularly concerned about using Autoreleasepool because the system already handles it.

AutoreleasePool explores learning

To see what AutoreleasePool does, create a main.m File (Xcode -> File -> New Project -> macOS -> Command Line Tool -> main.m);

#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... NSLog(@"Hello, World!" ); } return 0; }Copy the code

Then, use the compiler clang to compile main.m into the main. CPP file (using clang-rewrite-objc main.m at the end of the file), and scroll to the end of the main. CPP file with this code:

This code is the one that takes@autoreleasePoolConvert to a__AtAutoreleasePool Type of a local private variable__AtAutoreleasePool __autoreleasepool;

Then, inmain.cppIn-file query__AtAutoreleasePool, let’s see how it is implemented:You can see__AtAutoreleasePoolIs a structure type and implements two functions: constructors__AtAutoreleasePool()And destructor~__AtAutoreleasePool().

That is, when the __autoreleasepool variable is declared, the constructor __AtAutoreleasePool() is called, which calls atAutoReleasepoolobj = objc_autoreleasePoolPush(); ; When out of the current scope, the destructor ~__AtAutoreleasePool() is called, that is, objc_autoreleasePoolPop(atAutoReleasepoolobj) is executed; The code in main.m above can be replaced with this:

#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { // @autoreleasepool { void *atautoreleasepoolobj = objc_autoreleasePoolPush(); // insert code here... NSLog(@"Hello, World!" ); objc_autoreleasePoolPop(atautoreleasepoolobj); } return 0; }Copy the code

Now what does the destructor and constructor do? We need one hereobjc_runtimeThe source (The source addressHere we use”Objc4-756.2. Tar. Gz: So these two functions are essentially called separatelyAutoreleasePoolPagethepushMethods andpopMethod (here :: yesC++The form of the call method, similar to point syntax).

1.AutoreleasePoolPage

AutoreleasePoolPage is a C++ class that implements AutoreleasePoolPage.

Class AutoreleasePoolPage: private AutoreleasePoolPageData {# define POOL_BOUNDARY nil... Partial code omissionCopy the code

AutoreleasePoolPageData; AutoreleasePoolPageData; AutoreleasePoolPageData; AutoreleasePoolPageData

class AutoreleasePoolPage; struct AutoreleasePoolPageData { magic_t const magic; // check the structure of 'AutoreleasePoolPage'; 16 id *next; // Points to the next location of the newly added 'autoreleased' object, initialized to 'begin()'; 8 pthread_t const thread; // point to the current thread; 8 AutoreleasePoolPage * const parent; // point to parent, the first node's 'parent' value is' nil '; 8 AutoreleasePoolPage *child; // point to a child, and the last child is nil; 8 uint32_t const depth; // represents the depth, starting from '0' and increasing by '1'; 4 uint32_t hiwat; // represents' high water mark '; 4 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

throughAutoreleasePoolPageDataThe definition found that the structure has internalAutoreleasePoolPageSo there is a relationshipAutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage.Through the source can know that this is a typical two-way list structure, soAutoreleasePoolIt’s made up of severalAutoreleasePoolPageIn the form of a bidirectional linked list.

The AutoreleasePoolPageData structure is 56 bytes in size. AutoreleasePoolPageData creates 4096 bytes of memory for each object. All of the remaining space is used to store the address of the AutoRelease object. Note that the first page of the AutoreleasePoolPage contains the sentinel object, which occupies 8 bytes. Now each added object is 8 bytes. The first page can store up to 504 objects, and the second page can store up to 505 objects. AutoreleasepoolPage stores each AutoRelease object (from low address to high address) in a pushdown mode. When the next pointer points to begin, AutoreleasePoolPage is null. When the next pointer points to End, the AutoreleasePoolPage is full. A new AutoreleasePoolPage object is created, and the new AutoRelease object is inserted into the new AutoreleasePoolPage. Similarly, the next pointer to the new AutoreleasePoolPage is initialized at the bottom of the stack (pointing to begin).

2.AutoreleasePoolPage::push()

Now that you know that autoRelease objects are pushed into AutoreleasePoolPage, it is obvious that the AutoreleasePoolPage push method handles the creation and insertion of the AutoreleasePoolPage.

Now look at the source of the push method:

static inline void *push() { id *dest; If (DebugPoolAllocation) {// Each AutoRelease pool starts on a new pool page autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }Copy the code

The POOL_BOUNDARY here can be understood as a sentinel object, or it can be understood as a boundary identifier, and this POOL_BOUNDARY has a value of 0, which is nil.

Check if there is a poolPage, call the autoreleaseNewPage method to create it, and press the sentinel object directly if there is one. Enter the autoreleaseNewPage method:

Static __attribute__((noinline)) id *autoreleaseNewPage(id obj) {// Get the current hotPage AutoreleasePoolPage *page = hotPage(); If (page) return autoreleaseFullPage(obj, page); Else return autoreleaseNoPage(obj); else return autoreleaseNoPage(obj); }Copy the code

AutoreleaseFullPage and autoreleaseNoPage are implemented as follows:

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); If (page->child) page = page->child; if (page->child) page = page->child; Else Page = new AutoreleasePoolPage(page); } while (page->full()); // Set to current hotPage setHotPage(page); Return page->add(obj); } 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 ASSERT(! hotPage()); bool pushExtraBoundary = false; // Check if it is an empty placeholder, if so, // We are pushing a second pool over the empty placeholder pool 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; } // If the object is not a sentinel object and there is no Pool, 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", objc_thread_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); // Set page to current hotPage setHotPage(page); // Push a boundary on behalf of the previously allocated pool. // Push a boundary on behalf of the previously allocated pool. If (pushExtraBoundary) {// pushExtraBoundary page->add(POOL_BOUNDARY); } return page->add(obj); }Copy the code

Now, let’s go back to the source of the push method and look at the autoreleaseFast method,

Static Inline ID *autoreleaseFast(id obj) {static Inline ID *autoreleaseFast(id obj) {static Inline ID *autoreleaseFast(id obj) {static Inline ID * AutoreleasePoolPage *page = hotPage(); if (page && ! page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { return autoreleaseNoPage(obj); }}Copy the code

We know that linked lists have space, so the above 👆 source can be understood as:

(1). If the current page exists and is not full, directly add the object to the current page, that is, next point to the position;

(2). If the current page exists and is full, create a new page, add objects to the newly created page, and then link the two list nodes.

(3). If the current page does not exist, create the first page and add the object to the newly created page.

Let’s focus on page->add(obj).

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

You can see what’s returned hereretActually,nextPointer to the address from the abovepushMethod source is available herepage->add(obj)The incomingobjIn fact, isPOOL_BOUNDARYThat is, callpushMethod, which inserts aPOOL_BOUNDARY.

3.autorelease

The objc_autoreleasePoolPush constructor creates the AutoreleasePoolPage and inserts the sentinel object POOL_BOUNDARY. How does an object get inserted into an AutoreleasePoolPage by calling autoRelease? Here is the source code implementation of autoRelease:

__attribute__((aligned(16), flatten, noinline)) id objc_AutoRelease (id obj) {if it is not an object, return if (! obj) return obj; // If (obj->isTaggedPointer()) return obj; return obj->autorelease(); }Copy the code

If I go to obj-> autoRelease (),

inline id objc_object::autorelease() { ASSERT(! isTaggedPointer()); if (fastpath(! ISA()->hasCustomRR())) { return rootAutorelease(); } return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease)); }Copy the code

Then enter the rootAutorelease() method:

inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
Copy the code

Then enter the rootAutorelease2() method:

__attribute__((noinline,used)) id objc_object::rootAutorelease2() { ASSERT(! isTaggedPointer()); return AutoreleasePoolPage::autorelease((id)this); }Copy the code

The AutoRelease () method of AutoreleasePoolPage is called, where this is passed in as the object to be pushed. Then go to the autoRelease () method of AutoreleasePoolPage:

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

You’ll find that you’re in autoreleaseFast(obj); Method, again autoreleaseFast(obj); The method used to insert the sentinel object is the same as the method used by AutoreleasePoolPage to insert the sentinel object, except that the POOL_BOUNDARY is inserted by the push operation. The autoRelease operation inserts a specific Autoreleased object, so no further analysis is needed here.

From the analysis at 👆 above, you have a general idea of what an AutoreleasePool construct is and how it implements the pushguard object and pushobject internally.

4.AutoreleasePoolPage::pop(ctxt)

The objc_autoreleasePoolPush constructor returns the address of the sentinel object. The destructor objc_autoreleasePoolPop is passed the address of the sentinel object. With the method called step by step, let’s look at the implementation of the POP method for AutoreleasePoolPage:

static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; if (token == (void*)EMPTY_POOL_PLACEHOLDER) { if (hotPage()) { pop(coldPage()->begin()); } else { setHotPage(nil); } return; } page = pageForPointer(token); Stop = (id *)token; stop = (id *)token; if (*stop ! = POOL_BOUNDARY) { if (stop == page->begin() && ! page->parent) { } else { return badPop(token); } } if (PrintPoolHiwat) printHiwat(); page->releaseUntil(stop); If (DebugPoolAllocation && page->empty()) {// If (DebugPoolAllocation && page->empty()) { AutoreleasePoolPage *parent = page->parent; page->kill(); setHotPage(parent); } else if (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(); }}}Copy the code

Look at the page->releaseUntil(stop) method:

void releaseUntil(id *stop) { while (this->next ! = stop) { AutoreleasePoolPage *page = hotPage(); while (page->empty()) { page = page->parent; setHotPage(page); } page->unprotect(); id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); if (obj ! = POOL_BOUNDARY) { objc_release(obj); } } setHotPage(this); }Copy the code

Stop is also the address of POOL_BOUNDARY.

(1). The external loop iterates over the Autoreleased objects one by one until the sentinel object POOL_BOUNDARY is iterated.

(2). If the current page has no POOL_BOUNDARY and is empty, set hotPage to the parent node of the current page.

(3). Send a release message to the current AutoReleased object.

(4). Finally configure hotPage again.

5. AutoreleasePool nested

The same principle applies to nested AutoreleasePool. When a pop is released, the object is always released to the location of the last push, the sentinel position. A multi-tier pool simply inserts multiple sentinels and releases them according to the sentinels, like peeling an onion layer by layer.

The question is, what if the object is the same in AutoreleasePool multi-layer nesting? Here is a small example 🌰 :

@autoreleasepool { ZHPerson *person = [ZHPerson object]; NSLog(@"current count %d",_objc_rootRetainCount(person)); @autoreleasepool { ZHPerson *person1 = person; NSLog(@"current count %d",_objc_rootRetainCount(person)); @autoreleasepool { ZHPerson *person2 = person; NSLog(@"current count %d",_objc_rootRetainCount(person)); }}}Copy the code

The print result is as follows:heredeallocThe method is called only once, as the code above tells us: currentlyperson1andperson2Is thepersonIf the system inserts one automatically for each referenceautorelease, then the object is performing the firstautoreleaseIs calledobjc_release(obj)To release the current object, then when calledrootRelease()The current object has already been released, which means that the referenced object will only be released once. The same object cannot be repeatedautorelease)

NSthread, NSRunLoop, and AutoReleasePool

1. The NSthread and AutoReleasePool

Let’s start with a simple example:tempSet a breakpoint and enter it on the consolewatchpoint set variable temp.After this thread completes, take a look at the left sidebar:When performing theNSLog(@"thread end");So this code, this means that the thread is finished executing, and in this case, the thread actually calls first[NSthread exit]And then execute_pthread_tsd_cleanup, clears the related resources of the current thread, and then callstls_dealloc, that is, to associate the current threadAutoReleasePoolRelease, and finally callweak_clear_no_lockClear the pointer.

The AutoReleasePool corresponding to the NSThread is automatically emptied after the NSThread exits, so when a thread terminates, the automatically freed objects in ♻️AutoReleasePool are reclaimed.

Conclusion:

Each thread maintains its ownAutoReleasePoolAnd every one of themAutoReleasePoolThere is a single thread, but there can be multiple threadsAutoReleasePool.

2. NSRunLoop and AutoReleasePool

forNSThreadIt’s just a simple thread. What if it was replaced with a resident thread?So let’s create aNSTimerAnd keep it permanent. In the same way,watchpoint set variable tempTo:

You can see hereNStimerIs added to the child thread, but in the child thread, we don’t write aboutAutoReleasePoolWe only knowtestDo theautoreleaseOperation. Back to the source code:

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; } 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 *autoreleaseNoPage(id obj) { AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); } // Part of the code is omitted hereCopy the code

So we can conclude from the above source code that when the child thread uses an AutoRelease object, it will lazily load an AutoreleasePoolPage and insert the object into it.

So again, when does the AutoRelease object get released? When does AutoreleasePoolPage call the POP method?

When you create an NSThread, you release the current resource when you call [NSThread Exit], which releases the autoReleasePool associated with the current thread. In this case, you also execute the pop method when the RunLoop completes and exits. This explains why we don’t explicitly call pop in the child thread, which also frees the current AutoreleasePool resource.

3. NSRunLoop and AutoReleasePool of the main thread

So when does the RunLoop on the main thread release the object?

Let’s make it simple. Go straight through the consolepo [NSRunloop currentRunloop]Prints the main threadRunLoop:

Here, the system registers two observers in the main thread RunLoop, Callbacks are _wrapRunLoopWithAutoreleasePoolHandler, the first is the state of the Observer’s activities = 0 x1, the second is the state of the Observer’s activities = 0xa0, what do these two states mean?

Insert a bit of RunLoop content here:

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 represents kCFRunLoopEntry, which means that the first Observer monitors an Entry event (just before it enters the Loop) whose callback will call _objc_autoreleasePoolPush() to create an automatic release pool. Its order priority is -2147483647, the highest priority, ensuring that the automatic release pool is created before all other callbacks.

0xa0 corresponds to kCFRunLoopBeforeWaiting and kCFRunLoopExit, that is, the second Observer monitors two events: KCFRunLoopBeforeWaiting is ready to go to sleep and kCFRunLoopExit is about to exit RunLoop. Call _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() on the kCFRunLoopBeforeWaiting event torelease the old auto-release pool and create a new auto-release pool; _objc_autoreleasePoolPop() is called on the kCFRunLoopExit event torelease the auto-release pool, and the order of this Observer is 2147483647 with the lowest priority. Ensure that its release of the automatic release pool occurs after all other callbacks.

So in cases where AutoreleasePool is not manually added, the Autorelease object is released at the end of the current Runloop iteration, which is possible because of the automatic release pool push and POP operations added to each runloop iteration.

Conclusion:

You should create your own for different threadsAutoReleasePool. If the application is long-term, it should be regulardrainAnd create a newAutoReleasePool.AutoReleasePoolwithRunLoopIs a one-to-one correspondence with threads,AutoReleasePoolinRunLoopDo it at the beginning of the iterationpushOperation, theRunLoopSleep or at the end of an iterationpopOperation.

Application scenarios of AutoreleasePool

Normally we don’t need to create AutoreleasePool manually, but there are some special ones:

  1. Write programs that are not based on UI frameworks, such as command line programs.

  2. To reduce peak memory usage when creating a large number of temporary objects in a loop.

  3. Create a new thread outside of the main thread and create your own AutoreleasePool when the new thread starts executing, otherwise it will cause a memory leak.

Here’s a quick look at the second case, which goes straight to the for loop:

for (int i = 0; i < 100000000; i ++) { NSString * str = [NSString stringWithFormat:@"noAutoReleasePool"]; NSString *tempstr = str; }}Copy the code

Take a look at Memory usage:

Instead, if you add AutoreleasePool, look at this:

for (int i = 0; i < 100000000; i ++) { @autoreleasepool { NSString * str = [NSString stringWithFormat:@"AutoReleasePool"]; NSString *tempstr = str; }}Copy the code

Consider the Memory usage in this case:

The contrast is clear.

Here’s a quick note: The @Autoreleasepool in the main.m file does not have a significant impact on Memory usage if you test the use of the @Autoreleasepool in the for loop to create a large number of temporary objects.

conclusion

AutoReleasePool creation and release are not normally a concern, but understanding AutoReleasePool will help you understand how the system manages memory in ARC mode.

If the content of the article is inappropriate, please also point out, thank you!