iOS 底层学习-AutoreleasePool

一 概念:

AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行

1.1 MRC下使用自动释放池

在MRC环境中使用自动释放池需要用到NSAutoreleasePool对象,其生命周期就相当于C语言变量的作用域。对于所有调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:

//MRC环境下的测试:
//第一步:生成并持有释放池NSAutoreleasePool对象;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

//第二步:调用对象的autorelease实例方法;
id obj = [[NSObject alloc] init];
[obj autorelease];

//第三步:废弃NSAutoreleasePool对象;
[pool drain];   //向pool管理的所有对象发送消息,相当于[obj release]

//obi已经释放,再次调用会崩溃(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
NSLog(@"打印obj:%@", obj); 
1.2 ARC下使用自动释放池

ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是@autoreleasepool块和__autoreleasing修饰符

1244124-58029fa842865c94.png

如图所示,@autoreleasepool块替换了NSAutoreleasePoool类对象的生成、持有及废弃这一过程。而附有__autoreleasing修饰符的变量替代了autorelease方法,将对象注册到了Autoreleasepool。所以简化后的ARC代码如下:

int main(int argc, char * argv[]) {
   
    @autoreleasepool {
        
        NSLog(@"autorelease 的初探");
       
    }
    return 0;
}
//autoreleasepool代码简化如下:
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

    return 0;
}

ARC对象被注册到释放池中。主要包括以下几种情况:

  • 编译器会进行优化,检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回对象注册到Autoreleasepool;
  • 访问附有__weak修饰符的变量时,实际上必定要访问注册到Autoreleasepool的对象,即会自动加入Autoreleasepool;
  • id的指针或对象的指针(id*,NSError **),在没有显式地指定修饰符时候,会被默认附加上__autoreleasing修饰符,加入Autoreleasepool

二 AutoreleasePool数据结构和实现:

AutoreleasePool是以AutoreleasePoolPage对象为节点以双向链表组成的栈结构。parent和child就是用来构造双向链表的指针。parent指向前一个page, child指向下一个page。一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

5015685-373fe629a22b63cb.png
2.1 AutoreleasePoolPage

AutoreleasePoolPage是一个C++中的类,打开Runtime的源码工程,在NSObject.mm文件中可以找到它的定义如下:

//大致在641行代码开始
class AutoreleasePoolPage {
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  //空池占位
#   define POOL_BOUNDARY nil                //边界对象(即哨兵对象)
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);
    magic_t const magic;                  //16字节,校验AutoreleasePagePoolPage结构是否完整
    id *next;                             //8字节,指向新加入的autorelease对象的下一个位置,初始化时指向begin()
    pthread_t const thread;               //8字节,当前所在线程,AutoreleasePool是和线程一一对应的
    AutoreleasePoolPage * const parent;   //8字节指向父节点page,第一个结点的parent值为nil
    AutoreleasePoolPage *child;           //8字节,指向子节点page,最后一个结点的child值为nil
    uint32_t const depth;                 //4字节,链表深度,节点个数
    uint32_t hiwat;                       //4字节,数据容纳的一个上限
    //......
};
  • 每一个AutoreleasePoolPage都是虚拟内存页面的大小4096(虚拟内存每个扇区4096个字节,内存对齐)
#define PAGE_MAX_SIZE           PAGE_SIZE
#define PAGE_SIZE       I386_PGBYTES
#define I386_PGBYTES        4096        /* bytes per 80386 page */
  • AutoreleasePoolPage中有56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的都是用来存储加入到自动释放池中的对象
  • 首地址+56开始的,所以可以一页中实际可以存储4096-56 = 4040字节,转换成对象是4040 / 8 = 505个,即一页最多可以存储505个对象,其中第一页有哨兵对象只能存储504个
  • id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
  • 多个线程直接不共享 page 对象,在多线程中使用 MRR 时要注意这个问题,免得对象多次被释放或未能完成释放;
  • 凡是增加或删除对象都从这个活跃的 page 对象开始操作。
2.2 AutoreleasePool的push,pop和autorelease流程

_objc_autoreleasePoolPush() 和 _objc_autoreleasePoolPop() 这两个函数,分别在 NSAutoreleasePool 对象实例化以及发送 -drain 消息时调用

  • objc_autoreleasePoolPush
    objc_autoreleasePoolPush最终调用的是 AutoreleasePoolPage的push方法,该方法的具体实现如下
void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

static inline void *push() {
   return autoreleaseFast(POOL_BOUNDARY);
}

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 {
1.        return autoreleaseNoPage(obj);
   }
}

//压栈操作:将对象加入AutoreleaseNoPage并移动栈顶的指针
id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}

//当前hotPage已满时调用
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

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

//当前hotpage不存在时调用
static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    return page->add(obj);
}

每次调用push其实就是创建一个新的AutoreleasePool,在对应的AutoreleasePoolPage中插入一个POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY 的内存地址。push方法内部调用的是autoreleaseFast方法,并传入边界对象(POOL_BOUNDARY)。hotPage可以理解为当前正在使用的AutoreleasePoolPage。
自动释放池最终都会通过page->add(obj)方法将边界对象添加到释放池中,而这一过程在autoreleaseFast方法中被分为三种情况:

  1. 当前page存在且不满,调用page->add(obj)方法将对象添加至page的栈中,即next指向的位置
  2. 当前page存在但是已满,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至page的栈中
  3. 当前page不存在时,调用autoreleaseNoPage创建一个hotPage,再调用page->add(obj) 方法将对象添加至page的栈中
  • autorelease
    autorelease 方法的调用栈:
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

在autorelease方法的调用栈中,最终都会调用autoreleaseFast方法,将当前对象加到AutoreleasePoolPage 中

inline id objc_object::rootAutorelease() {
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

__attribute__((noinline,used)) id objc_object::rootAutorelease2() {
    return AutoreleasePoolPage::autorelease((id)this);
}

static inline id autorelease(id obj) {
   id *dest __unused = autoreleaseFast(obj);
   return obj;
}
  1. autorelease函数和push函数一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,
  2. 不过push函数的入栈的是一个边界对象,而autorelease函数入栈的是需要加入autoreleasepool的对象。
  • objc_autoreleasePoolPop
    AutoreleasePool的释放调用的是objc_autoreleasePoolPop方法,此时需要传入边界对象作为参数。这个边界对象正是每次执行objc_autoreleasePoolPush方法返回的对象atautoreleasepoolobj
    objc_autoreleasePoolPop最终调用AutoreleasePoolPage的pop方法即
    AutoreleasePoolPage::pop()实现:

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

static inline void pop(void *token)   // token指针指向栈顶的地址
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);   // 通过栈顶的地址找到对应的page
    stop = (id *)token;
    if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
        // This check is not valid with DebugPoolAllocation off
        // after an autorelease with a pool page but no pool in place.
        _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                    token);
    }

    if (PrintPoolHiwat) printHiwat();   // 记录最高水位标记

    page->releaseUntil(stop);   // 从栈顶开始操作出栈,并向栈中的对象发送release消息,直到遇到第一个哨兵对象

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

/*
用一个 while 循环持续释放 AutoreleasePoolPage 中的内容,直到 next 指向了 stop 。使用 memset 将内存的内容设置成 SCRIBBLE,然后使用 objc_release 释放对象
*/
void releaseUntil(id *stop) {
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();

        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_SENTINEL) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

/*
将当前页面以及子页面全部删除
*/
void kill() {
    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);
}

该静态方法总共做了三件事情:

  1. 使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
  2. 调用 releaseUntil 方法释放栈中的对象,直到 stop
  3. 调用 child 的 kill 方法(除非是pop(0)方式调用,这样会清理掉所有page对象;
    否则,在当前page存放的对象大于一半时,会保留一个空的子page,
    这样估计是为了可能马上需要新建page节省创建page的开销)

三 AutoreleasePool释放时机:

Thread和AutoreleasePool的关系:包括主线程在内的所有线程都维护有它自己的自动释放池的堆栈结构。新的自动释放池被创建的时候,它们会被添加到栈的顶部,而当池子销毁的时候,会从栈移除。对于当前线程来说,Autoreleased对象会被放到栈顶的自动释放池中。当一个线程线程停止,它会自动释放掉与其关联的所有自动释放池

RunLoop和AutoreleasePool的关系:主线程的NSRunLoop在监测到事件响应开启每一次event loop之前,会自动创建一个autorelease pool,并且会在event loop结束的时候执行drain操作,释放其中的对象

3.1 AutoreleasePool在主线程上释放时机

主线程RunLoop中有两个与自动释放池相关的Observer,它们的 activities分别为0x1和0xa0这两个十六进制的数,转为二进制分别为1和10100000,对应CFRunLoopActivity的类型如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          //0x1,启动Runloop循环
    kCFRunLoopBeforeTimers = (1UL << 1),            
    kCFRunLoopBeforeSources = (1UL << 2),        
    kCFRunLoopBeforeWaiting = (1UL << 5),  //0xa0,即将进入休眠     
    kCFRunLoopAfterWaiting = (1UL << 6),   
    kCFRunLoopExit = (1UL << 7),           //0xa0,退出RunLoop循环  
    kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

结合RunLoop监听的事件类型,分析主线程上自动释放池的使用过程如下:

  1. App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler();
  2. 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。order = -2147483647(即32位整数最小值)表示其优先级最高,可以保证创建释放池发生在其他所有回调之前;
  3. 第二个Observer监视了两个事件BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop()来释放自动释放池。order = 2147483647(即32位整数的最大值)表示其优先级最低,保证其释放池子发生在其他所有回调之后;
  4. 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建AutoreleasePool了;
3.2 AutoreleasePool子线程上的释放时机

子线程默认不开启RunLoop,那么其中的延时对象该如何释放呢?其实这依然要从Thread和AutoreleasePool的关系来考虑:
每一个线程都会维护自己的 Autoreleasepool栈,所以子线程虽然默认没有开启RunLoop,但是依然存在AutoreleasePool,在子线程退出的时候会去释放autorelease对象

注:ARC会根据一些情况进行优化,添加__autoreleasing修饰符,其实这就相当于对需要延时释放的对象调用了autorelease方法。从源码分析的角度来看,如果子线程中没有创建AutoreleasePool ,而一旦产生了Autorelease对象,就会调用autoreleaseNoPage方法自动创建hotpage,并将对象加入到其栈中。所以,一般情况下,子线程中即使我们不手动添加自动释放池,也不会产生内存泄漏

四 实际应用:

4.1降低内存使用峰值:

这一点不用多说,当你使用类似for循环这样的逻辑需要产生大量的中间变量时,Autorelease Pool无意是最佳的一种解决方案;

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
        @autoreleasepool{
             NSObject *obj = [[NSObject alloc] init];
             NSLog(@"打印obj:%@", obj);
        }
    }
 }
4.2 如果是对NSArray操作,如果可以的话推荐使用OC提供的以下api(内部使用了autoreleasepool):
- (void)enumerateObjectsUsingBlock:

- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:

- (void)enumerateObjectsAtIndexes:(NSIndexSet *)s options:(NSEnumerationOptions)opts usingBlock:
4.3 按照苹果给的文档说的,如果采取一些非cocoa创建的一些线程,将不会自动生成autoreleasepool给你,你需要手动去创建它
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容