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 fetchingDemoObject
The 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 whyobject1
Not 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 acceptautorelease
Message object when pooldumping
Is sent to every object in itrelease
The message. So send to an objectautorelease
Message rather thanrelease
Message that can increase the life cycle of an object until it is inAutoreleasePool
bePour out
.
In a reference counting environment,Cocoa
Expect to have a validAutoreleasePool
Otherwise,Autorelease objects
Can’t be freed and cause a memory leak. In the code above, we can see thatp
thedealloc
Method is not called. And we use_objc_autoreleasePoolPrint
Print the contents of the current auto release pool, found to existp
It is in the auto-release pool, but because the auto-release pool is not triggeredpop
Operation, thereforep
Can’t callrelease
Method, 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 thatAutoreleasePoolPage
Is a bidirectional linked list structure, and inside is a stack structure. althoughAutoreleasePoolPage
There were only seven variables inside, 56 bytes in size, but they were overwrittennew
Method, 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:
hotpage
Present and not fullhotpage
Full ofhotpage
There is no
Next, analyze the situation
1. hotpage
Present 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. hotpage
Full 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. hotpage
There 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 articleobject1
How 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_autoreleaseReturnValue
Methods. 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 timehotpage
forpageN
, you need topop
thetoken
forPOOL_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