iOS 性能优化-自动释放池

作者: 淡定的笨鸟 | 来源:发表于2019-05-06 19:48 被阅读54次

    AutoreleasePool是OC中的一种自动回收机制,在ARC的模式下已经很少能看到autorelease了,它可以延迟变量release的时机。在OC的main.m中就有一个autoreleasepool,本篇结合runtime研究一下autoreleasepool的底层是如何实现的。

    问答模式

    问:什么时候需要使用自动释放池?

    官方解释:基本分为如下三点
    1、当我们需要创建大量的临时变量的时候,可以通过@autoreleasepool 来减少内存峰值。
    2、创建了新的线程执行Cocoa调用。
    3、如果您的应用程序或线程是长期存在的,并且可能会生成大量自动释放的对象,那么您应该定期清空并创建自动释放池(就像UIKit在主线程上所做的那样);否则,自动释放的对象会累积,内存占用也会增加。但是,如果创建的线程不进行Cocoa调用,则不需要创建自动释放池。

    问:为什么会减少内存峰值?
    答:借用YYImage的代码打个比方。
    比如业务需要在一个代码块中需要创建大量临时变量,或临时变量足够大,占用了很多内存,可以在临时变量使用完以后就立即释放掉,在ARC的环境下只能通过自动释放池实现。

    if ([UIDevice currentDevice].isSimulator) {
            @autoreleasepool {
                NSString *outPath = [NSString stringWithFormat:@"%@ermilio.gif.png",IMAGE_OUTPUT_DIR];
                NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:gif]);
                [outData writeToFile:outPath atomically:YES];
                [gif writeToFile:[NSString stringWithFormat:@"%@ermilio.gif",IMAGE_OUTPUT_DIR] atomically:YES];
            }
            @autoreleasepool {
                NSString *outPath = [NSString stringWithFormat:@"%@ermilio.apng.png",IMAGE_OUTPUT_DIR];
                NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:apng]);
                [outData writeToFile:outPath atomically:YES];
                [apng writeToFile:[NSString stringWithFormat:@"%@ermilio.png",IMAGE_OUTPUT_DIR] atomically:YES];
            }
            @autoreleasepool {
                NSString *outPath = [NSString stringWithFormat:@"%@ermilio_q85.webp.png",IMAGE_OUTPUT_DIR];
                NSData *outData = UIImagePNGRepresentation([YYImageDecoder decodeImage:webp_q85 scale:1]);
                [outData writeToFile:outPath atomically:YES];
                [webp_q85 writeToFile:[NSString stringWithFormat:@"%@ermilio_q85.webp",IMAGE_OUTPUT_DIR] atomically:YES];
            }
    }
    

    再比如在循环的场景下,如果创建大量的临时变量,会使内存峰值持续增加,加入自动释放池以后,在每次循环结束时,超出自动释放池的作用域,使得内部的大量临时变量被释放,从而大大降低了内存的使用。

    for (int i = 0; i < count; i++) {
            @autoreleasepool {
                id imageSrc = _images[i];
                NSDictionary *frameProperty = NULL;
                if (_type == YYImageTypeGIF && count > 1) {
                    frameProperty = @{(NSString *)kCGImagePropertyGIFDictionary : @{(NSString *) kCGImagePropertyGIFDelayTime:_durations[i]}};
                } else {
                    frameProperty = @{(id)kCGImageDestinationLossyCompressionQuality : @(_quality)};
                }
    }
    

    上述这几种情况如果没必要就别这么写,毕竟创建自动释放池也需要耗费内存。

    自动释放池的实现原理

    在开始之前先看一下自动释放池的大致结构图

    自动释放池结构图.png

    上图就是自动释放池的结构图,可能现在看不懂,这里先有个概况继续往下看就明白了,不太会画图,反正意思表达出来了。

    查看main.cpp

    我们先在终端clang一下main.m,变成C++实现
    clang -rewrite-objc main.m -o main.cpp
    我们会得到一个main.cpp文件,打开这个文件翻到最底部会看到这个代码

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_vg_bngxds5x5q90wwst5gl1jq140000gn_T_main_1b100d_mi_0);
        }
        return 0;
    }
    

    发现原来autoreleasepool也是一个对象,我们在这个cpp文件中查找__AtAutoreleasePool,找到如下的结构体

    extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
    extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
    
    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
      void * atautoreleasepoolobj;
    };
    

    结构体只提供了一个构造函数和一个析构函数,里面分别调用了objc_autoreleasePoolPushobjc_autoreleasePoolPop,这个objc前缀告诉我们,是不是能到runtime里面搜索一下,在rumtime源码中全局搜索objc_autoreleasePoolPush,找到这个函数

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

    我们发现了正主,是一个类AutoreleasePoolPage

    AutoreleasePoolPage

    class AutoreleasePoolPage 
    {
       ···
       //当自动释放池为空时的一个占位符
    #   define EMPTY_POOL_PLACEHOLDER ((id*)1)
        //边界符,用来区别每个AutoreleasePoolPage的边界
    #   define POOL_BOUNDARY nil
        //线程的key,通过key值寻找线程下的AutoreleasePoolPage
        static pthread_key_t const key = AUTORELEASE_POOL_KEY;
        //4096个字节,表示每个page的大小,因为虚拟内存每个扇区4096个字节
        PAGE_MAX_SIZE;  
        //一个page里面的对象数量
        static size_t const COUNT = SIZE / sizeof(id);
        //共需要占用56个字节
        magic_t const magic;                   // 16字节,校验完整性的变量
        id *next;                              // 8字节,指向下一个对象的指针
        pthread_t const thread;                // 8字节,所属线程,page和thread是一一对应关系
        AutoreleasePoolPage * const parent;    // 8字节,父节点,指向上一个page
        AutoreleasePoolPage *child;            // 8字节,子节点,指向下一个page
        uint32_t const depth;                  // 4字节,表示链表一共有多少个节点
        uint32_t hiwat;                        // 4字节,high water marks表示自动释放池中最多能存放的对象个数
        ···
    }
    

    从这个类中我们得到了以下内容

    • EMPTY_POOL_PLACEHOLDER:
      当自动释放池为空时的一个占位符,就是在第一次push时,先用这个字段把AutoreleasePoolPage的位置占上。
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
    
    • POOL_BOUNDARY :
      边界符,用来区别每个AutoreleasePoolPage的边界,我们从创建page的时候可以得知
    // 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);
       }
    
    • key:
      线程的key,通过key值寻找线程下的AutoreleasePoolPage
    • PAGE_MAX_SIZE:
      4096个字节,表示每个page的大小,因为虚拟内存每个扇区4096个字节
    • COUNT:
      一个page里面的对象数量
    • magic:
      校验完整性的变量,占用16字节
    • next:
      指向下一个对象的指针,占用8字节
    • thread:
      所属线程,page和thread是一一对应关系,占用8字节
    • parent:
      父节点,指向上一个page,占用8字节,看到这里我们发现这个自动释放池其实是个双向链表,不过是以栈的形式存取的
    • child:
      子节点,指向下一个page,占用8字节
    • depth:
      表示链表一共有多少个节点,占用4字节
    • hiwat:
      high water marks表示自动释放池中最多能存放的对象个数,占用4字节

    从上面的分析我们可以得知,page本身占用了56个字节,而一个AutoreleasePoolPage一共4096个字节,也就是说我们还剩下4040个字节可以用来放对象。接下来看看它的push和pop的过程。

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

    这里我们不看Debug,直接找autoreleaseFast函数

    static inline id *autoreleaseFast(id obj)
        {
            //获取当前活跃的page
            AutoreleasePoolPage *page = hotPage();
            if (page && !page->full()) {
                return page->add(obj);
            } else if (page) {
                return autoreleaseFullPage(obj, page);
            } else {
                return autoreleaseNoPage(obj);
            }
        }
    

    从代码中我们得知,先调用hotPage()函数获取page,当page不满时,我们调用add()函数;当对象满了时,调用了autoreleaseFullPage()函数;当没获取到page时,调用autoreleaseNoPage()函数。接下来我们看看这几个函数都做了什么

    1.1、hotPage()
    static inline AutoreleasePoolPage *hotPage() 
        {
            //从一个键值对中获取当前page
            AutoreleasePoolPage *result = (AutoreleasePoolPage *)
                tls_get_direct(key);
            if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
            if (result) result->fastcheck();
            return result;
        }
    

    从代码中我们得知hotPage函数是从一个键值对中获取当前活跃的page,而这个key就是上面我们看到的

    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    
    1.2、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;
        }
    

    从源码中我们得知,add()是向链表中增加一个对象,简单的改变了指针的指向,这不必细说。

    1.3、autoreleaseFullPage(obj, page)
    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);
        }
    

    从源码中我们得知,这个page满了以后,循环遍历自动释放池中的page,直到找到一个page不满时,我们把对象添加进去。

    1.4、autoreleaseNoPage(obj)
    id *autoreleaseNoPage(id obj)
        {
            bool pushExtraBoundary = false;
            //判断是否有空池占位符
            if (haveEmptyPoolPlaceholder()) {
                pushExtraBoundary = true;
            } else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
                //没有可用pool
                objc_autoreleaseNoPool(obj);
                return nil;
            }
            else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
                //当前page还没有空池占位符,先加上占位符
                return setEmptyPoolPlaceholder();
            }
           //如果执行到这里,表示目前没有可有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);
            }
             //把autorelease对象添加进来
            return page->add(obj);
        }
    
    1.5、Push总结
    AutoreleasePoolPage-push.png

    流程基本如上图所示

    2、Pop

    在Pop时,会传入当前的token,token就是

    static inline void pop(void *token) 
        {
            //token就是边界符
            if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
                if (hotPage()) {
                    pop(coldPage()->begin());
                } else {
                    setHotPage(nil);
                }
                return;
            }
            //找到最上边的page,即当前的page
            page = pageForPointer(token);
            stop = (id *)token;
            if (*stop != POOL_BOUNDARY) {
                if (stop == page->begin()  &&  !page->parent) {
                    //讲道理,如果token不等于POOL_BOUNDARY,pageForPointer()计算过后,理论上是一定会进入这里的
                } else {
                    //走这就出问题了
                    // Error. For bincompat purposes this is not 
                    // fatal in executables built with old SDKs.
                    return badPop(token);
                }
            }
            
            //更新当前自动释放池最大存储数
            if (PrintPoolHiwat) printHiwat();
            //清空token之前的autorelease对象
            page->releaseUntil(stop);
            //清空操作
            if (page->lessThanHalfFull()) {
                page->child->kill();
            } else if (page->child->child) {
                page->child->child->kill();
           } 
        }
    

    从源码中我们看到了Pop的过程分成了三步,
    1、判断token是否等于EMPTY_POOL_PLACEHOLDER
    首先我们要知道token实际上是个边界符,通常情况下等于POOL_BOUNDARY,其次我们要记得上面说过自动释放池其实是个双向链表,不过是以栈的形式存取的,所以当执行这个判断条件时,实际上就是Pop到了最后一步了。
    2、当token不等于POOL_BOUNDARY时
    这一步一般是不会进来的,只有在没有自动释放池且调用了autorelease时才会出现。但生活还是要继续的...
    在做接下来的操作前,先获取最新的page,即当前page

    page = pageForPointer(token);
    static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
        {
            AutoreleasePoolPage *result;
            uintptr_t offset = p % SIZE;//size就是4096,每个page最大size
    
            result = (AutoreleasePoolPage *)(p - offset);
            result->fastcheck();
    
            return result;
        }
    

    下面这个操作知识为了确保这个token拿到的page没问题。

    stop = (id *)token;
            if (*stop != POOL_BOUNDARY) {
                if (stop == page->begin()  &&  !page->parent) {
                    //讲道理,如果token不等于POOL_BOUNDARY,pageForPointer()计算过后,理论上是一定会进入这里的
                    // 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);
                }
            }
    

    3、最后一步释放page里面的对象。
    在释放操作之前,更新当前自动释放池最大存储数。

    if (PrintPoolHiwat) printHiwat();
    
    static void printHiwat()
        {
            AutoreleasePoolPage *p = hotPage();
            uint32_t mark = p->depth*COUNT + (uint32_t)(p->next - p->begin());
            if (mark > p->hiwat  &&  mark > 256) {
                for( ; p; p = p->parent) {
                    p->unprotect();
                    p->hiwat = mark;
                    p->protect();
                }
    

    这一步操作释放token之前的autorelease对象。

    //释放token之前的autorelease对象
    page->releaseUntil(stop);
    
    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) {
                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);
                }
            }
    

    kill操作,如果当前page小于当前page的一半时,则把当前页的所有子节点都kill掉,否则从子节点的子节点开始kill。

    //清空操作
            if (page->lessThanHalfFull()) {
                page->child->kill();
            } else if (page->child->child) {
                page->child->child->kill();
           } 
    

    到目前为止,我们明白了autorelease对象的释放是在autoreleasePool释放之前。

    参考资料

    autorelease和autoreleasePoolPage--你真的了解么?
    OC源码 —— autoreleasepool
    官方runtime源码

    相关文章

      网友评论

        本文标题:iOS 性能优化-自动释放池

        本文链接:https://www.haomeiwen.com/subject/wsiroqtx.html