ios内存管理-autoreleasepool

研究@autoreleasepool之前,我们先来看下他的基本构成和实现。
main.m文件中添加如下代码:

int main(int argc, char * argv[]){
    @autoreleasepool {
        
    }
    return 1;
}

clang编译后:

int main(int argc, char * argv[]){
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 1;
}

全局搜索__AtAutoreleasePool

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

在main.cpp文件中有一个__AtAutoreleasePool结构体,里面包含了构造函数和和析构函数。
下面我们根据objc源码看下具体实现。

一、autoreleasePool数据结构

通过objc_autoreleasePoolPush方法,我们会发现一个结构体AutoreleasePoolPage,在这里有一个注释,很重要:

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. 

AutoreleasePoolPageData属性的基本构成:

class 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)
    {
    }
};

struct magic_t {
    static const uint32_t M0 = 0xA1A1A1A1;
#   define M1 "AUTORELEASE!"
    static const size_t M1_len = 12;
    uint32_t m[4]; //4*4
……
}
  • magic 检查校验完整性的变量
  • next 指向新加入的autorelease对象下一个位置
  • thread page当前所在的线程
  • parent 父节点 指向前一个page
  • child 子节点 指向下一个page
  • depth 链表的深度,节点个数
  • hiwat high water mark 数据容纳的一个上限

其他相关属性:

  • EMPTY_POOL_PLACEHOLDER 空池占位
  • POOL_BOUNDARY 是一个边界对象 nil,之前的源代码变量名是 * POOL_SENTINEL哨兵对象,用来区别每个page即每个 AutoreleasePoolPage 边界
  • PAGE_MAX_SIZE =4096, 为什么呢?其实就是虚拟内存每个扇区4096个字节,也就是4K对齐

关于构造函数AutoreleasePoolPageData,第一个next是什么呢,跳转后定位到下面:

id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

lldb调试,打印发现 sizeof(*this) = 56,也就是一个AutoreleasePoolPageData的大小,这个在前面已经标注。
注意一点:结构体指针为8,结构体的大小要根据内部属性计算,static修饰的变量空间不在结构体内
所以,page从56字节的位置开始装对象,因此一页可以装的数量:(4096-56)/8 = 505,下面来验证一下:

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {(int i=0; i<1+504 +505; i++) {
            NSObject *objc = [[NSObject alloc] autorelease];
        }
        _objc_autoreleasePoolPrint();
        
        sleep(3);//防止打印没结束,当前线程以及结束
    }
    return 0;
}

注意:第一个页数据504+1,这个1是边界即哨兵,比较特殊。具体可以参考_objc_autoreleasePoolPrint实现。

二、push

static inline id *autoreleaseFast(id obj)
    {
       AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            //当前page存在,且不满,则add
            return page->add(obj);
        } else if (page) {
            //当前page存在,且满
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

添加对象:

id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }
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.
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }
static __attribute__((noinline)) id *autoreleaseNoPage(id obj)
{
    assert(!hotPage());
    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) { //是否存在未使用的空池
        pushExtraBoundary = true;
    } else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        //obj 不是哨兵对象时, 说明上一次的 push() 没有成功, 如果打开了丢失 pools 的调试
        _objc_inform(...); //输出一些调试信息
        objc_autoreleaseNoPool(obj); //这个函数没有找到源码
        return nil;
    } else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        //如果 obj 是哨兵对象, 并且没有开启 pool 内存申请的调试
        return setEmptyPoolPlaceholder(); //将本自动释放池标记为未使用的空池
    }
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); //创建一个 page 节点, 这里会调用构造函数
    setHotPage(page); //将该节点设置为 hotPage
    if (pushExtraBoundary) { //如果本自动释放池是一个未使用的空池
        page->add(POOL_BOUNDARY); //加入哨兵对象
    }
    return page->add(obj); //将 obj 加入自动释放池
}
static inline bool haveEmptyPoolPlaceholder()
{
     id *tls = (id *)tls_get_direct(key); //取出本线程对应的 hotPage
     return (tls == EMPTY_POOL_PLACEHOLDER); //如果为空池标识符则返回 true
}

pop 源码:

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) { //如果 token 为空池标志
        if (hotPage()) { //如果有 hotPage, 即池非空
            pop(coldPage()->begin()); //将整个自动释放池销毁
        } else {
            setHotPage(nil); //没有 hotPage, 即为空池, 设置 hotPage 为 nil
        }
        return;
    }
    page = pageForPointer(token); //根据 token 找到所在的 节点
    stop = (id *)token; //token 转换给 stop
    if (*stop != POOL_BOUNDARY) { //如果 stop 中存储的不是哨兵节点
        if (stop == page->begin()  &&  !page->parent) {
            //存在自动释放池的第一个节点存储的第一个对象不是哨兵对象的情况, 有两种情况导致:
            //1. 顶层池呗是否, 但留下了第一个节点(有待深挖)
            //2. 没有自动释放池的 autorelease 对象(有待深挖)
        } else {
            //非自动释放池的第一个节点, stop 存储的也不是哨兵对象的情况
            return badPop(token); //调用错误情况下的 badPop()
        }
    }
    return popPage<false>(token, page, stop);
}

static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop){
    page->releaseUntil(stop); //将自动释放池中 stop 地址之后的所有对象释放掉
    if (...) {
        //这一段代码都是调试用代码
    } else if (page->child) { //如果 page 有 child 节点
        if (page->lessThanHalfFull()) { //如果 page 已占用空间少于一半
            page->child->kill(); //kill 掉 page 的 child 节点
        } else if (page->child->child) { //如果 page 的占用空间已经大于一半, 并且 page 的 child 节点有 child 节点
            page->child->child->kill(); //kill 掉 child 节点的 child 节点
        }
    }
}

void releaseUntil(id *stop)
{
    // 释放当前page中的autorelease对象
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();

        // 如果当前page都释放完了还没遇到POOL_BOUNDARY,就继续释放上一个page的
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        // 依次取出autorelease对象
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        // 释放autorelease对象
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

三、嵌套

int main(int argc, char * argv[]){
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] autorelease];
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] autorelease];
            @autoreleasepool {
                NSObject *obj = [[NSObject alloc] autorelease];
                _objc_autoreleasePoolPrint();
            }
        }
    }
    
    while (1) {
        
    }
}

打印结果:

objc[15807]: ##############
objc[15807]: AUTORELEASE POOLS for thread 0x1199565c0
objc[15807]: 7 releases pending.
objc[15807]: [0x7fa191006000]  ................  PAGE  (hot) (cold)
objc[15807]: [0x7fa191006038]    0x6000016040a0  NSObject
objc[15807]: [0x7fa191006040]  ################  POOL 0x7fa191006040
objc[15807]: [0x7fa191006048]    0x600001608190  NSObject
objc[15807]: [0x7fa191006050]  ################  POOL 0x7fa191006050
objc[15807]: [0x7fa191006058]    0x600001608140  NSObject
objc[15807]: [0x7fa191006060]  ################  POOL 0x7fa191006060
objc[15807]: [0x7fa191006068]    0x6000016081b0  NSObject
objc[15807]: ##############

push函数会把POOL_BOUNDARY存储到AutoreleasePoolPage对象中,用来标记当前@autoreleasepool的边界。一个{}对应一个POOL_BOUNDARY,pop时,会依次释放对象,直到遇到POOL_BOUNDARY

四、宏定义解释

EMPTY_POOL_PLACEHOLDER

根据注释以及代码分析, 可以大致得出这个宏定义用作 pool 中没有 add 入对象时的标记。当一个自动释放池被创建但是没有加入任何 Autorelease 对象时, 会让这个自动释放池的句柄等于 EMPTY_POOL_PLACEHOLDER, 并不为其分配内存。
注释说在顶层(即 libdispatch)进行 pool 的 push 和 pop 但并没有使用这个池子的时候, 能节省空间, OC类中有许多自带 block 的方法, 比如 UIView 的 animation 系列方法, 和 NSArray 以及 NSDictionary 的 enumerat 系列方法中, 自带的 block 会内嵌 AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        //这里的代码已经处于内嵌的 @autoreleasepool {} 中了
    }];

验证:

    //1
    __block __weak NSArray * weakArray;
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @autoreleasepool {
            NSArray * array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%@", self], nil];
            weakArray = array;
        }
    });
    NSLog(@"%@", weakArray);
    //2
    [UIView animateWithDuration:1. animations:^{
        NSArray * array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%@", self], nil];
        weakArray = array;
    } completion:^(BOOL finished) {
        NSLog(@"%@", weakArray);
    }];

以上两段代码输出都为 null, 但是如果将第一段代码中的 @autoreleasepool 删除, weakArray 将输出有值, 证明第二段代码的 block 中内嵌了 @autoreleasepool
如果内嵌了@autoreleasepool 的代码块中没有使用到 Autorelease 对象, 却为池子分配了节点, 无疑是对内存的浪费。
EMPTY_POOL_PLACEHOLDER 即是为了解决这种情况下发生的内存浪费而存在的。

POOL_BOUNDARY

POOL_BOUNDARY 会在建立新的自动释放池时作为第一个对象加入到池中, 被称为哨兵对象, 哨兵对象是自动释放池中非常巧妙而且重要的一环。
我们已经知道 @autoreleasepool {}是在作用域的开始使用 push()方法来创建自动释放池, 在作用域结束时, 使用 pop() 方法来销毁自动释放池。
在嵌套结构中 push() 方法不一定会创建新的 page 节点, 如果当前节点未满则会直接插入一个哨兵对象, 如果当前节点已满则创建一个新的 page 节点并且插入一个哨兵对象, push() 函数的返回值就是这个哨兵对象的地址(哨兵对象的值是 nil, 但哨兵对象的地址不为 nil), 然后在 pop() 方法调用时, 传入这个哨兵对象的地址, 对这个地址之后的 autorelease 对象发送 release 方法。

五、autoreleasepool、runloop和多线程的关系

5.1 autoreleasepool和runloop

  1. RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中
  2. 主线程的RunLoop会在初始化全局字典时创建
  3. 子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建
  4. RunLoop会在线程销毁时销毁

系统在主线程的runloop中注册了2个observer:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@", [NSRunLoop mainRunLoop]);
}

// 部分打印
observers = (
    "<CFRunLoopObserver 0x600001330320 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), 
      context = <CFArray 0x600002c607e0 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f9c54802048>\n)}}",
    "<CFRunLoopObserver 0x6000013303c0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), 
      context = <CFArray 0x600002c607e0 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f9c54802048>\n)}}"
)

关于CFRunLoopActivity:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),         // 1,进入runloop
    kCFRunLoopBeforeTimers = (1UL << 1),  // 2,即将处理timers
    kCFRunLoopBeforeSources = (1UL << 2), // 4,即将处理sources
    kCFRunLoopBeforeWaiting = (1UL << 5), // 32,即将休眠
    kCFRunLoopAfterWaiting = (1UL << 6),  // 64,刚被唤醒
    kCFRunLoopExit = (1UL << 7),          // 128,退出runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

第一个Observer:activities为0x1(十进制为1),它用来监听kCFRunLoopEntry的。当监听到kCFRunLoopEntry时,会调用objc_autoreleasePoolPush函数。
第二个Observer:activities为0xa0(十进制为160 = 32 + 128),它用来监听kCFRunLoopBeforeWaitingkCFRunLoopExit。当监听到kCFRunLoopBeforeWaiting时,会先调用objc_autoreleasePoolPop函数,再调用objc_autoreleasePoolPush函数。当监听到kCFRunLoopExit时,会调用objc_autoreleasePoolPop函数

autorelease对象在当前RunLoop休眠之前释放。局部对象在方法结束时释放,因为ARC生成的是release,而不是autorelease。

5.2 autoreleasepool和多线程

主线程我们前面已经讨论过,主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理。
在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage,并调用page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!
如果没有pool管理(没有pop),在线程退出时,也会释放资源,调用tls_dealloc方法,清空对象以及AutoreleasePoolPage。

参考:
iOS 各个线程 Autorelease 对象的内存管理
探索子线程autorelease对象的释放时机
子线程AutoRelease对象何时释放

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