The introduction

Start with the following code and think about the output and why.

// DemoObject
@interface DemoObject : NSObject
@end

@implementation DemoObject

+ (DemoObject*)object {
    DemoObject *object = [[DemoObject alloc]init];
    return object;
}

@end

// AutoreleasePoolDemoVC
@interface AutoreleasePoolDemoVC ()
@end

@implementation AutoreleasePoolDemoVC

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak id object1;
    {
        object1 = [DemoObject object];
    }
    NSLog(@"object1 = %@", object1);
    
    __weak id object2;
    {
        object2 = [[DemoObject alloc]init];
    }
    NSLog(@"object2 = %@", object2);
}
Copy the code

The output is as follows:

 object1 = <DemoObject: 0x2811d0370>
 object2 = (null)
Copy the code

Comparing the above code can be found in addition to fetchingDemoObjectThe method of instance is not the same as the other one is exactly the same, so that’s the problem. In fact, the system has also given a hint for the second way:So whyobject1Not released? And that’s what we’re going to analyze todayAutoreleasePool Automatic release pool.

Introduction to AutoreleasePool

Here’s an excerpt from apple’s official documentation:

In reference counting (as opposed to garbage collection) environments, automatic release pools contain acceptautoreleaseMessage object when pooldumpingIs sent to every object in itreleaseThe message. So send to an objectautoreleaseMessage rather thanreleaseMessage that can increase the life cycle of an object until it is inAutoreleasePoolbePour out.

In a reference counting environment,CocoaExpect to have a validAutoreleasePoolOtherwise,Autorelease objectsCan’t be freed and cause a memory leak. In the code above, we can see thatpthedeallocMethod is not called. And we use_objc_autoreleasePoolPrintPrint the contents of the current auto release pool, found to existpIt is in the auto-release pool, but because the auto-release pool is not triggeredpopOperation, thereforepCan’t callreleaseMethod, causing a memory leak, but in the above case, after executing the code directly exit, should not be a problem, just for example reference.

There is another problem here. We did not create the auto-release pool, but we did print the result. When was this created? This will be explained later in the analysis.

Source code analysis

Rewrite the CPP file

To understand the underlying implementation of auto-release pooling, we need to use the Clang directive

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Copy the code

We rewrite the above file with the following instruction:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

Finally, a CPP file is obtained, and only the useful parts are extracted as follows:

// main int main(int argc, char * argv[]) { NSString * appDelegateClassName; /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))); } return UIApplicationMain(argc, argv, __null, appDelegateClassName); } struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush(); } ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj); } void * atautoreleasepoolobj; };Copy the code

As you can see in the rewritten file, @Autoreleasepool becomes a variable of type __AtAutoreleasePool. The initializer and destructor of the __AtAutoreleasePool structure are shown above, essentially inserting the following two lines of code at the beginning and end of our code segment:

atautoreleasepoolobj = objc_autoreleasePoolPush(); // Own code... objc_autoreleasePoolPop(atautoreleasepoolobj);Copy the code

Obviously, these two lines of code are the core of the memory free pool, so let’s take a look at the source code. The auto-release pool source code is in LibobJC, currently objC4-781.2

objc_autoreleasePoolPush

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

You can see that the AutoreleasePoolPage push function is called. The autorelease pool should be related to the Autorelease EpoolPage. So let’s first look at the structure of AutoreleasePoolPage

AutoreleasePoolPage

struct AutoreleasePoolPageData { magic_t const magic; // 16 __unsafe_unretained id *next; // 8 pthread_t const thread; // 8 AutoreleasePoolPage * const parent; // 8 AutoreleasePoolPage *child; // 8 uint32_t const depth; // 4 uint32_t hiwat; // 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) { } }; class AutoreleasePoolPage : private AutoreleasePoolPageData { 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(); } static void * operator new(size_t size) { return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE); }... #define PAGE_SIZE I386_PGBYTES #define I386_PGBYTES 4096 }Copy the code

The structure is shown in the figure below:It can be seen thatAutoreleasePoolPageIs a bidirectional linked list structure, and inside is a stack structure. althoughAutoreleasePoolPageThere were only seven variables inside, 56 bytes in size, but they were overwrittennewMethod, every time will open up4096(4k)The size of the space several core variables are as follows:

  • next id*Type pointing to the next pluggable position in the current page.
  • Thread Specifies the current page thread
  • Parent Indicates the previous page
  • Child the following page
  • Depth Indicates the current page position in the entire bidirectional list

With an overview of the structure of AutoreleasePoolPage, let’s move on to the push function

static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    
#   define POOL_BOUNDARY nil
Copy the code

Where POOL_BOUNDARY can be understood as a sentinel object and is nil.

autoreleaseFast

Let’s go straight to the implementation of autoreleaseFast

Static inline ID *autoreleaseFast(id obj) {// Get the current hotPage AutoreleasePoolPage *page = hotPage(); // Page exists and is not full if (page &&! page->full()) { return page->add(obj); } else if (page) {return autoreleaseFullPage(obj, page); } else {// page does not return autoreleaseNoPage(obj); }}Copy the code

The current hotPage is first retrieved from the current thread-local storage TLS, which was mentioned in the previous article in multithreaded @synchronized locks. Next, there are three cases to insert obj. In our current process, the inserted obj is the sentinel object POOL_BOUNDARY:

  1. hotpagePresent and not full
  2. hotpageFull of
  3. hotpageThere is no

Next, analyze the situation

1. hotpagePresent and not full

In this case, just insert it into hotPage

id *add(id obj) { ASSERT(! full()); unprotect(); Id *ret = next; Better than 'return next-1' because of aliasing protect(); Return ret; }Copy the code

2. hotpageFull of

According to the full() function above, when next points to the end of the current page, the current page is full, and a new page needs to be opened to store obj

autoreleaseFullPage

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. Step into the next dissatisfied page and add a new page if necessary. Next, insert obj into the new page ASSERT(page == hotPage()); ASSERT(page->full() || DebugPoolAllocation); If (page->child) page = page->child; Else Page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }Copy the code

AutoreleasePoolPage

AutoreleasePoolPage(AutoreleasePoolPage *newParent) : AutoreleasePoolPageData(begin(), objc_thread_self(), newParent, newParent ? 1+newParent->depth : 0, newParent ? newParent->hiwat : 0) { if (parent) { parent->check(); ASSERT(! parent->child); parent->unprotect(); parent->child = this; parent->protect(); } protect(); }Copy the code

Initialize the new page and establish the relationship between the parent and the current page.

After initialization, you need to set the current page to hotPage, save it to TLS, and then call the add method to add object

3. hotpageThere is no

Static __attribute__((noinline)) id *autoreleaseNoPage(id obj) { // Install the first page. AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); . // Push the requested object or pool. return page->add(obj); }Copy the code

If hotPage does not exist in TLS, it should be the first time that an object is added to the auto-release pool. In this case, the page needs to be created, and there is no parent, and the hotPage needs to be set.

Now that the objc_autoreleasePoolPush function is analyzed, the main operations in the current scenario are as follows: Create an AutoreleasePoolPage and set it to hotPage. Insert the sentinel object POOL_BOUNDARY into the current HOTPage. Return the position of the inserted object in the automatic release pool.

autorelease

We know the automatic release poolpush()Will insert the sentinel object, so in the example code at the beginning of the articleobject1How do you insert it? Break point as shown:To view the assembly code in Xcode:You can see that the [DemoObject Object] method is finally calledobjc_autoreleaseReturnValueMethods. The source code is as follows:

// Prepare a value at +1 for return through a +0 autoreleasing convention.
id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}
Copy the code

objc_autorelease()->objc_object::autorelease()->rootAutorelease()->objc_object::rootAutorelease2()->AutoreleasePoolPage: :autorelease()

The final call to the autoreleaseFast method inserts our object into the automatic release pool.

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 is also explained at the beginning of the article, we do not show that the push method is called, but the automatic release pool already exists. Because AutoRelease calls the autoreleaseFast function, the parameter is the object to be released lazily. Objc_autoreleasePoolPush essentially calls autoreleaseFast with POOL_BOUNDARY as the sentinel object.

objc_autoreleasePoolPop

objc_autoreleasePoolPop(atautoreleasepoolobj); NEVER_INLINE void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); } static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; If (token == (void*)EMPTY_POOL_PLACEHOLDER) { } else { page = pageForPointer(token); } stop = (id *)token; // Ignore irrelevant code... return popPage<false>(token, page, stop); }Copy the code

The pop function needs to find the page where the current token is and then execute the popPage method.

pageForPointer

static AutoreleasePoolPage *pageForPointer(const void *p) 
    {
        return pageForPointer((uintptr_t)p);
    }

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

        ASSERT(offset >= sizeof(AutoreleasePoolPage));

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

        return result;
    }
Copy the code

P is the position of the token, and the relative position of the current page can be obtained by complementing the SIZE of p. P-offset is the starting position of the current page, that is, the page where the token is located

popPage

template<bool allowDebug> static void popPage(void *token, AutoreleasePoolPage *page, id *stop) { ... page->releaseUntil(stop); . 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
  • Release the page until the Stop flag is found.
  • Operations are performed based on the current page capacity.

releaseUntil

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(); Obj = *--page->next; obj = *--page->next; // SCRIBBLE memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); if (obj ! = POOL_BOUNDARY) { objc_release(obj); } } setHotPage(this); . }Copy the code

The above picture is an example, at this timehotpageforpageN, you need topopthetokenforPOOL_BOUNDARY. The operations described above are as follows:

Check whether pageN is empty, otherwise loop to find the parent of pageN and assign the value to hotPage 3. Starting with hotPage next minus 8 bytes, the last autoRelease object in the page, release successively and assign the current position in the page to 'SCRIBBLE' 4. If the token is not POOL_BOUNDARY, release operation 5 has already been performed. Current page is set to HotPageCopy the code

After the operation, the structure of page1 is as follows:Let’s verify this with the code:

Among them:

  • 0x7FF23E81e000 is the start address of the current AutoreleasePoolPage. Read the memory of 0x7FF23E81e000 to see more data

  • 0x7FF23E81e048 is the current page-> Next point

  • 0x10E991dc0 is the thread where the current page resides

  • The depth and hiwat values are 0, so 0x7FF23e81e020 through 0x7FF23e81e030 are 0

  • 0x7FF23e81e038 is the address of the sentinel object POOL_BOUNDARY inserted in the current page when push is inserted. It can be seen that the difference between the initial address and the sentinel object is 0x38 (56), which is consistent with the variable size in AutoreleasePoolPage structure. POOL_BOUNDARY is essentially nil, so it’s also 0

  • 0x6000006145A0 is the address of Object

kill

Then the popPage function above does different processing for different page sizes.

void kill() { // Not recursive: we don't want to blow out the stack // if a thread accumulates a stupendous amount of garbage 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

reference

Autorelease behind the black screen

Autoreleasepool —- In-depth analysis of autoreleasepool