我的iOS开发小屋

AutoreleasePool从入门到放弃

2018-06-12  本文已影响537人  KeepMoveingOn
funny.png

1. AutoreleasePool介绍
2. AutoreleasePool使用场景
3. AutoreleasePool源码解析
4. AutoreleasePool的未解之谜

1. AutoreleasePool介绍

Autorelease pool blocks provide a mechanism whereby you can relinquish ownership of an object, but avoid the possibility of it being deallocated immediately (such as when you return an object from a method). Typically, you don’t need to create your own autorelease pool blocks, but there are some situations in which either you must or it is beneficial to do so

AutoreleasePool提供了一种延长对象生命周期和避免立即销毁的机制,比如函数Return对象时。通常你不用自己建立AutoreleasePool,但在某些场景下你必须自己创建或是能从使用AutoreleasePool中受益。

我们简单的用代码说明下为什么需要AutoreleasePool,下面我们来模拟下[NSArray array]的实现

+ (instancetype)array {
    
    NSArray *array = [[NSArray alloc] init];
    return array;         //[array autorelease]编译器自动插入autorelease操作
}

array作为局部变量不是应该在出函数作用域时就销毁了吗?但是为什么我们依旧能得到初始化后的对象?这里就是编译默认为我们插入了autorelease操作,使array加入最顶层的AutoreleasePool从而避免了变量在出作用域后立马释放的尴尬情况。

上述文档提到我们通常不需自己创建AutoreleasePool①;但在some situations我们又不得不创建;如果在特定场景下使用AutoreleasePool我们还能从中受益,这里的benefit又指的是什么呢

下述给出官方文档的解释:

The AppKit and UIKit frameworks process each event-loop iteration (such as a mouse down event or a tap) within an autorelease pool block. Therefore you typically do not have to create an autorelease pool block yourself, or even see the code that is used to create one
① AppKitUIKit在每次Runloop迭代时会默认在autorelease pool下执行

If you are writing a program that is not based on a UI framework, such as a command-line tool.
如果你是基于非UI framework的命令行编程,你必须自己创建autorelease pool,否则可能造成autoreleased对象的内存泄漏

If you spawn a secondary thread.You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)
如果你自己创建了子线程,你必须创建autorelease pool,否则可能造成autoreleased对象的内存泄漏

If you write a loop that creates many temporary objects.You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.
如果你在循环中创建了大量的临时对象(autoreleased对象),你应该在每次循环迭代间使用autorelease pool,这样能在下次循环前对对象进行销毁从而有效地降低系统内存使用峰值

官方文档: Using Autorelease Pool Blocks
官方文档: NSAutoreleasePool

2. AutoreleasePool使用场景

2.1降低系统内存峰值

如果在循环中大量创建autoreleased对象,而这些autoreleased对象只会在AutoreleasePool执行Pop操作(通俗点就是出了@AutoreleasePool block作用域)时才会销毁,通常情况下默认每个线程都有其对应的AutoreleasePool,而其执行Pop操作是在每次Runloop迭代时。而这个过程中未及时释放的autoreleased对象就会造成大量的内存堆积从而对性能造成一定的影响。

    for (NSInteger count = 0; count < 66666666; count++) {
        
            NSMutableString *autoreleasePool = [NSMutableString stringWithFormat:@"autoreleasePool"];
            NSMutableString *memoryMagic = [NSMutableString stringWithFormat:@"memoryMagic"];
    }

这里着重说明下,内存堆积是因为autoreleased对象未及时释放造成的,那么什么样的初始化方式创建出来的属于autoreleased对象呢?在ARC时代,编译器会根据初始化方法的命名规则来自动为我们插入相关的内存管理语句。非alloc/new/copy/mutableCopy开头的类方法默认返回的是autoreleased对象。

我们可以通过在代码中引入AutoreleasePool调试函数来查看当前释放池中的autoreleased对象来验证上诉结论。

#import "ViewController.h"

extern _objc_autoreleasePoolPrint();

@interface ViewController ()

[编译器优化.png-481e68-1528701766440-0)] 未经编译优化.png

童鞋们可能说打脸了吧?[NSArray array]初始化返回的变量明明没有加入到AutoreleasePool中,也就说明不是autoreleased对象。这里先暂且告诉你们这是编译器在ARC中自己根据函数后续执行列表进行的TLS(Thread Local Storage)优化,后续再为大家解读优化的目的与原理。这里我们看未经编译优化的截图即可。

    for (NSInteger count = 0; count < 66666666; count++) {
    
        NSMutableString *autoreleasePool = [[NSMutableString alloc] initWithString:@"autoreleasePool"];
        NSMutableString *memoryMagic = [[NSMutableString alloc] initWithString:@"memoryMagic"];
        //注意初始化方式,这里非autoreleased对象不会造成内存积压
    }

可能大家或多或少看过几篇大牛关于AutoreleasePool分析的文章 Objective-C Autorelease Pool 的实现原理黑幕背后的Autorelease等,这里我要着重说明下NSString类已经被苹果过度优化了,在内存分析时要特别注意。上述文章中的测试例子将会生成nstaggedpointerstring对象,这是一种利用指针内存地址存储数据的优化手段,生成的并非真正的对象。所以在分析内存管理上会存在不准确的问题。关于nstaggedpointerstring的可以看下这篇文章【译】采用Tagged Pointer的字符串

2.2延长对象生命周期

2.2.1函数返回对象时
2.2.2函数参数为地址时

通过变量地址变向的从函数返回对象时同样也需要遵循ARC下的内存管理原则

    NSFileManager *manage = [NSFileManager defaultManager];
    [manage removeItemAtPath:filePath error:(NSError * _Nullable __autoreleasing * _Nullable)];

在我们常用的文件管理操作中,对于NSError异常的捕获其实也就是从函数中变向的返回了一个NSError对象。在上述参数中我们能看到对应NSError的二级指针用了__autoreleasing内存管理字段来显式的对该对象执行了一个入池操作来延长对象生命周期。

3. AutoreleasePool源码解析

AutoreleasePool源码可以从Runtime源码中获得。下载好我们可以在NSObject.mm中看到AutoreleasePool的实现原理和结构说明:

Autorelease pool implementation
A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased objects are stored.

AutoreleasePool其实是一个存贮指针的栈,内部指针代表延迟释放的对象或者是表示不同AutoreleasePool之间的边界对象。
AutoreleasePooltoken指针名为POOL_BOUNDARY,当AutoreleasePool执行Pop操作时后于POOL_BOUNDARY入栈的对象将被释放。
AutoreleasePool是由多页的双向列表页组成,列表页在必要时可添加与删除。
TLS(Thread-local storage)指向autoreleased最近存储的AutoreleasePool

class AutoreleasePoolPage 
{
    ...
    magic_t const magic;                   //校验AutoreleasePoolPage完整性
    id *next;                              //存放下一个autoreleased对象地址
    pthread_t const thread;                //对应线程
    AutoreleasePoolPage * const parent;    //父AutoreleasePoolPage
    AutoreleasePoolPage *child;            //子AutoreleasePoolPage
    uint32_t const depth;                  //当前AutoreleasePoolPage索引index
    uint32_t hiwat;                        //存储autoreleased对象峰值数                         
    ...
}

接下来我们分析在使用@autoreleasepool block时代码的具体执行。我们先从每个Cocoa Touch工程都包含的Main.m文件入手,cdMain.m路径下再通过命令行来将代码转成c++形式

clang -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.3.sdk main.m

我们可以看到原本的代码被转换成:

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

而结构体__AtAutoreleasePool的定义我们也可以在转换代码中找到:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

由此我们可以知道__AtAutoreleasePool结构体在初始化和析构时分别执行了objc_autoreleasePoolPushobjc_autoreleasePoolPop(atautoreleasepoolobj)函数。我们先从这两个函数开始分析:(函数源码可以在之前的NSObject.mm文件中查找到)

objc_autoreleasePoolPush(void)

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

push()

static inline void *push() 
    {
        id *dest;
        if (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;
    }

返回空池标志位或者AutoreleasePool边界对象

autoreleaseNewPage(id obj)

static __attribute__((noinline)) id *autoreleaseNewPage(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }

获取当前AutoreleasePoolPage,如果存在调用autoreleaseFullPage,否则调用autoreleaseNoPage

autoreleaseFullPage(id obj, AutoreleasePoolPage *page)

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);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

遍历子AutoreleasePoolPage,如果有未满的AutoreleasePoolPage设置为当前AutoreleasePoolPage并调用add函数将对象入栈。否则将新创建的子AutoreleasePoolPage设置为当前AutoreleasePoolPage并调用add函数将对象入栈

add(id obj)

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

对象入栈操作

autoreleaseNoPage(id 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;
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or 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;
        }
        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", 
                         pthread_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);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
    }

创建AutoreleasePoolPage分页并设置为当前分页,根据上述情况判断是否需要插入AutoreleasePool边界标识。最后才入栈所需的对象。

autoreleaseFast(id 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);
        }
    }

objc_autoreleasePoolPop(void *ctxt)

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

pop(void *token)

static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else 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();
            }
        }
    }
pop.png

由转换的c++代码我们可以看出objc_autoreleasePoolPop的参数即objc_autoreleasePoolPush生成的变量,所以@autoreleasePool block在出了其作用域时都会将自身包裹的autoreleased对象进行出栈操作


objc_autoreleasePoolPush()
objc_autoreleasePoolPop(atautoreleasepoolobj)
两个函数我们都基本分析完了,现在我们看看在这个两个函数之间的对象调用autorelease的流程

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}
inline id objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

这里我们看到TaggedPointer是直接返回的,也验证了之前所说的NSString问题。
prepareOptimizedReturn属于TLS优化部分我们先放放

    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;
    }

我们看到autorelease方法其实也是调用之前的autoreleaseFast函数进行对象入栈操作的

3.1 TLS优化

MRC时代,autoreleased对象的创建与持有是以以下形式进行的:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    ViewController *obj = [ViewController viewController];
    [obj retain];
    [obj release];
}

+ (instancetype)viewController {
    
    ViewController *obj = [[ViewController alloc] init];
    
    return [obj autorelease];
}
quote.png

我们来看下代码在ARC时的汇编代码是如何的:

Assemble.png

[ViewController viewDidLoad]

image.png

[ViewController viewController]

image.png

原本的代码被转换成:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    id temp = objc_retainAutoreleasedReturnValue([ViewController viewController]);
    ViewController *obj = temp
}

+ (instancetype)viewController {
    
    ViewController *obj = [[ViewController alloc] init];
    
    return objc_autoreleaseReturnValue(obj);
}

我们再看看objc_retainAutoreleasedReturnValueobjc_autoreleaseReturnValue的具体实现

objc_autoreleaseReturnValue(id obj)

id objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

根据函数调用列表判断是否走优化流程,如果autorelease后紧接着调用objc_retainAutoreleasedReturnValue就直接返回对象,否则正常进行入池操作。

prepareOptimizedReturn(ReturnDisposition disposition)

static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition)
{
    assert(getReturnDisposition() == ReturnAtPlus0);

    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        if (disposition) setReturnDisposition(disposition);
        return true;
    }

    return false;
}

函数callerAcceptsOptimizedReturn根据__builtin_return_address(0)获取到调用方的函数地址,然后通过地址列表进行后续函数调用的判断。如果后续调用了objc_retainAutoreleasedReturnValue则设置优化标志位

objc_retainAutoreleasedReturnValue(id obj)

id objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

而该函数会根据prepareOptimizedReturn(ReturnAtPlus1)函数之前的优化标志位来判断是否需要对对象进行retain操作

文章最开始时也有使用调试函数分别打印下外部变量持有时和无外部变量持有时的池内对象的截图,可以用来验证上述TLS结论的正确性。(外部变量持有时会调用objc_retainAutoreleasedReturnValue,同学们通过可以去除外部变量持有后的汇编代码验证)

4. AutoreleasePool的未解之谜

    for (NSInteger count = 0; count < 66666666; count++) {
        
        @autoreleasepool {
            
            __autoreleasing UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
            _objc_autoreleasePoolPrint();
        }
    }
}

使用__autoreleasing字段强制对象入池,通过调试函数也确实能看到对象已经入池,但是AutoreleasePool迭代时内存依旧飙升未起到作用。但是注释掉调试函数后内存便可及时释放,有知道原理的童鞋麻烦告诉我一声为什么🤕


funny.jpg

好了我写炸了,如果看看官们能看到这里应该能学到一点东西。如果文中有理解错的地方也欢迎大家斧正。

上一篇下一篇

猜你喜欢

热点阅读