preface

Hi Coder, I’m CoderStar!

In the MRC era, we probably used AutoreleasePool a lot to help us manage memory. In the ARC era, some of the memory management operations were replaced by the compiler. Manual release and autorelease operations were no longer needed. But AutoreleasePool is still behind the scenes, and there are situations where you need to use it explicitly. Today we’ll talk about AutoreleasePool.

The following source code is the Runtime objc code, which may vary from version to version, but the general principle should be the same.

Use the form

// OC
@autoreleasepool {
    // Generate auto-release objects
}

// swift
autoreleasepool {
    // Generate auto-release objects
}
Copy the code

The basic principle of

When the code associated with the @Autoreleasepool package is compiled, the compiler automatically compiles it into the following form.

Void *poolSentinelObj = objc_autoreleasePoolPush(); Objc_autoreleasepoolPop (poolSentinelObj);Copy the code

If you dig a little deeper, there is an __AtAutoreleasePool that encapsulates the two methods.

Objc_autoreleasePoolPush objc_autoreleasepoolPop objc_autoreleasePoolPush objc_autoreleasepoolPop

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

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

Above we can see a core class, AutoreleasePoolPage.

AutoreleasePoolPagestructure

AutoreleasePoolPage is a C++ class defined in nsobject. mm as follows:

Class AutoreleasePoolPage {// Check the integrity of the current AutoreleasePoolPage magic_t const magic; // Points to the location where the next autoreleased object will be stored (when next == begin(), AutoreleasePoolPage is empty; If next == end(), AutoreleasePoolPage is full id *next; Pthread_t const thread; pthread_t const thread; // point to the parent node, the first node's parent value is nil; AutoreleasePoolPage * const parent; // Points to child nodes, and the last node's child value is nil; AutoreleasePoolPage *child; The depth of the first page is 0, and the depth of each subsequent page is increased by 1. uint32_t const depth; // uint32_t hiwat; };Copy the code

Note the comments

As you can see from the above structure, each AutoreleasePool is connected in a bidirectional linked list with an AutoreleasePoolPage node.

Each AutoreleasePoolPage object has 4096 bytes of storage, except for its own member variables (56 bytes, 8 bytes each), and the remaining space is used to store subsequent AutoRelease objects.

Why is each AutoreleasePoolPage set to 4096 bytes? Because 4096 is the size of a page of virtual memory.

A flowchart

  • When entering@autoreleasepoolWhen scoped,objc_autoreleasePoolPushMethod is called,runtimeWill be towards the currentAutoreleasePoolPageAdd a nil object as the sentinel object and return the address of the sentinel object;
  • The objectautoreleaseMethod will be added to the correspondingAutoreleasePoolPageIn go,nextA pointer is like a cursor, constantly changing to record its position. If the object added exceeds the size of a page, a new page is automatically added.
  • When leaving@autoreleasepoolWhen scoped,Objc_autoreleasePoolPop (Sentinel object address)Method is called, which looks up from the last element of the next metric on the current page until the last sentinel object is sent to objects in the rangereleaseMessage;

Because of the existence of the sentinel object, the nesting of the auto-release pool is also satisfied, whether nested or nested auto-release pool, find their own sentinel object.

Look at the specific source code process analysis.

Source code flow analysis

Push function

#define POOL_BOUNDARY nil static inline void *push() {id *dest; if (slowpath(DebugPoolAllocation)) { // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else {// Add a sentinel object to the automatic release pool dest = autoreleaseFast(POOL_BOUNDARY); }... return dest; } static inline ID *autoreleaseFast(id obj) {// Get hotPage: AutoreleasePoolPage * Page = hotPage(); // If there is a page and the page is not occupied if (page &&! Page ->full()) {return page->add(obj); } else if (page) {// Add an object return autoreleaseFullPage(obj, page); } else {// Create a page if there is no page return autoreleaseNoPage(obj); Id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {// Create a new page and add child to the new page. do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); } // Create a new page id *autoreleaseNoPage(id obj) {... AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); . // Push the requested object or pool. return page->add(obj); }Copy the code

Pop function

Void releaseUntil(id) void releaseUntil(id) void releaseUntil(id) void releaseUntil(id) void releaseUntil(id *stop) {// Loop sends release messages to autoRelease objects in turn while (this->next! // AutoreleasePoolPage = hotPage(); // AutoreleasePoolPage = hotPage(); // AutoreleasePoolPage = hotPage(); // AutoreleasePoolPage = hot; // If empty, use the parent pointer to point to its parent node and set the parent node to the current page while (page->empty()) {page = page->parent; setHotPage(page); } page->unprotect(); Obj = *-- Page ->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); // Starting from the last element of next, look up and send a release message if (obj! = POOL_BOUNDARY) { objc_release(obj); } } setHotPage(this); }Copy the code

Autorelease function

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

Perform type

AutoreleasePool typically includes two execution types:

  • The mainRunLoopautojoinAutoreleasePool;
  • Manually addAutoreleasePool;

The mainRunloopAutomatically add

The main thread in the Runloop registered two Observer, the callback is _wrapRunLoopWithAutoreleasePoolHandler (). The two observers are as follows:

  • monitoringEntryEvent, the automatic release pool is automatically created in the callback,orderfor- 214748364., with the highest priority, ensuring that the release pool is created before all other callbacks;
  • monitoringBeforeWaitingExitEvents;
    • BeforeWaitingCalled when the_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()Release the old pool and create a new pool.
    • ExitCalled when the_objc_autoreleasePoolPop()To release the automatic release pool. The Observerorder2147483647, has the lowest priority and ensures that its release pool occurs after all other callbacks.

The system’s automatic release pool is also not always released at BeforeWaiting and Exit, and may be released after Timer and Source events are processed.

AutoreleasePool is also automatically added to some of the system methods, such as:

  • When using the block version of the container’s enumerator, an AutoreleasePool is automatically added internally;

    [array enumerateObjectsUsingBlock: ^ (id obj, NSUInteger independence idx, BOOL * stop) {/ / here is surrounded by a local @ autoreleasepool}].Copy the code
  • Using NSThread detachNewThreadSelector: toTarget: withObject: method to create a new thread, new threads with AutoreleasePool automatically;

  • .

Manually add

If autoReleasepool is manually added, it is released when the scope braces end.

In what scenarios do we add them manually?

CLI program

Since GUI applications have RunLoop mechanisms that are released every cycle, AutoreleasePool may be less of a concern, but CLI applications may need more attention.

Traversal generates a large number ofAutoreleaseA local variable

A large number of Autorelease local variables are generated during the traversal process, which will lead to a high memory peak. Manually adding AutoreleasePool can reduce the peak memory usage.

func loadBigData(a) {
    if let path = NSBundle.mainBundle().pathForResource("big", ofType: "jpg") {
        for i in 1.10000 {
            autoreleasepool {
                let data = NSData.dataWithContentsOfFile(path, options: nil, error: nil)
                NSThread.sleepForTimeInterval(0.5)}}}}Copy the code

To extend this slightly, not all objects generated in this way can be used to reduce the memory peak, because we can make it clear that only objects of type Autorelease are managed by AutoreleasePool.

So what kind of object is an Autorelease?

  • The compiler checks if the method name isalloc.new.copy.mutableCopyIf not, the object that returns the value is automatically registered toAutoreleasePool, such as some class methods;

    There is a point in this. If your custom method starts with these key words, clang will not follow the logic when compiling. We can use clang attribute to handle this. – (id)allocObject __attribute__((objC_method_family (none))), which treats allocObject as a normal object.

  • IOS 5 and previous compilers, keywords__weakThe modified object is automatically addedAutoreleasePool. For iOS 5 and later compilers, it’s called directlyrelease, will not joinAutoreleasePool;
  • Id pointer (id *) and object Pointers (NSError *), the keyword is automatically added__autorealeasingTo joinAutoreleasePool;

We can actually use the objc_autoreleaseReturnValue function to indicate whether an object has been added to the AutoreleasePool. The objc_autoreleaseReturnValue function checks the list of commands executed by the method using the function or by the caller of the function. If the caller of the method or function after calling a method or function and then calling objc_retainAutoreleasedReturnValue () function, then it will not return the object to register AutoreleasePool, but is passed directly to the method or function of the caller.

arc-runtime-objc-autoreleasereturnvalue

Permanent thread

Runloop is not enabled for child threads by default. AutoreleasePool is created automatically.

The autoreleaseNoPage method is called when a child thread does not create an AutoreleasePool but produces an Autorelease object. In this method, a hotPage is automatically created for you. By default, an AutoreleasePoolPage is generated to add the Autorelease object. Objects in AutoreleasePool are released after the thread is destroyed. At this point, we need to pay attention to resident threads. If you are a resident thread, it is easy to delay releasing all Autorelease objects in the thread, so you need to manually add AutoreleasePool so that related objects can be released in a timely manner.

class KeepAliveThreadManager {
    private init(a) {}
    static let shared = KeepAliveThreadManager(a)private(set) var thread: Thread?

    /// start the resident thread
    public func start(a) {
        if thread ! = nil, thread!.isExecuting {
            return
        }
        thread = Thread {
            autoreleasepool {
                let currentRunLoop = RunLoop.current

                // If you want to add the status observation of the RunLoop, you need to add it after fetching, not after starting.

                currentRunLoop.add(Port(), forMode: .common)
                currentRunLoop.run()
            }
        }
        thread?.start()
    }

    /// Close the resident thread
    public func end(a) {
        thread?.cancel()
        thread = nil}}class Test: NSObject {
    func test(a) {
        if let thread = KeepAliveThreadManager.shared.thread {
            perform(#selector(task), on: thread, with: nil, waitUntilDone: false)}}@objc
    func task(a) {
        /// Add a layer of autoreleasepool to the task
        autoreleasepool {

        }
    }
}

Copy the code

main.mIt’s in the file

The @AutoReleasepool in main is only responsible for managing autoRelease objects in its scope.

Prior to Xcode11, the entire application was run inside @autoreleasepool, which was theoretically a bit of an afterthought because of RunLoop.

Xcode before 11

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

Xcode 11

After Xcode 11, the UIApplicationMain function that triggers the main thread RunLoop is placed outside of @Autoreleasepool, This ensures that the AutoRelease object in @AutoReleasepool is released immediately after the program starts. As the new version of @Autoreleasepool notes, “Setup code that might create autoreleased objects goes here.” Autoreleasepool is used to handle any AutoRelease objects that may be generated before entering UIApplicationMain.

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

The last

AutoreleasePool gives you a quick overview of all the points involved in AutoreleasePool.

Try harder!

Let’s be CoderStar!

  • Autoreleasepool —- In-depth analysis of autoreleasepool
  • Principles of iOS autoreleasePool
  • Autorelease behind the black screen
  • Autoreleasepool inquiry
  • Transitioning to ARC Release Notes
  • IOS – Talk about AutoRelease and @Autoreleasepool