第二十二篇:autoreleasepool自动释放池

autoreleasepool前言

下面是我们自己创建了一个autoreleasepool,创建的对象有自动添加到自动释放池,和手动添加到释放池的。当我们自己创建了autoreleasepool然后把对象添加进去,这个就是手动添加到释放池。每个autoreleasepool都会对应一个runloop。如果手动添加到autoreleasepool里这个就和runloop没有关系了,出了autoreleasepool作用域就会被释放。

WechatIMG9.jpeg

通过下图的写法发现内存会暴增

WechatIMG10.jpeg

通过下图的写法发现内存不会暴增,因为我们在其位置上添加了autoreleasepool,其出了这个autoreleasepool就会被释放掉。

WechatIMG11.jpeg

AutoreleasePool底层:

    首先我们写个使用@autoreleasepool的释放池,代码如下,然后进行clang命令进行一个操作。
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    
    return 0;
}

clang后的代码,在这里点击__AtAutoreleasePool进去发现其是一个struct的结构体。

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

__AtAutoreleasePool构造函数和析构函数。

struct __AtAutoreleasePool {
 构造函数 __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
析构函数  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

通过上面其实AutoreleasePool底层代码可以写成这几个:
objc_autoreleasePoolPush()
NSLog(@"Hello, World!")
objc_autoreleasePoolPop(atautoreleasepoolobj)

我们通过汇编调试也可以看到AutoreleasePool其实其是走objc_autoreleasePoolPush和objc_autoreleasePoolPop方法


WechatIMG91.jpeg

objc_autoreleasePoolPush方法

下面是objc_autoreleasePoolPush执行的代码,其中::符号是一个命名空间,就是说明AutoreleasePoolPage去调用push()方法
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

下面是官方对自动释放池的介绍:

 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. 

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.
每个指针要么是将要释放的对象,要么是⼀个POOL_BOUNDARY,作为⾃动释放池的边界。
(⾃动释放池⾥⾯的数据有俩种类型,⼀种是将要被释放的对象,另⼀种是
POOL_BOUNDARY,超出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.
有⼀个指向该池的 POOL_BOUNDARY 的指针。当池被弹出时,每个⽐哨兵更热的对象都会被
释放。(在调⽤pop函数的时候,整个⾃动释放池⾥⾯的对象都会被释放。)
The stack is divided into a doubly-linked list of pages. Pages are added and
deleted as necessary.
栈被分成⼀个双向链接的⻚⾯列表。根据需要添加和删除⻚⾯。(⾃动释放池是⼀个双向列表
的栈结构。它的每⼀个节点都是AutoreleasePoolPage。)
Thread-local storage points to the hot page, where newly autoreleased objects
are stored.
线程本地存储指向热⻚,其中存储了新的⾃动释放对象。(⾃动释放池⾥⾯的数据也有线程缓
存)

下面是AutoreleasePoolPage里的data数据,其内部里是存储一些数据。

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    struct AutoreleasePoolEntry {
        uintptr_t ptr: 48;
        uintptr_t count: 16;

        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
    static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif

    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

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

下面是AutoreleasePoolPageData里的成员变量意思:

magic 用来校验 AutoreleasePoolPage 的结构是否完整;
next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
thread 指向当前线程;
parent 指向父结点,第一个结点的 parent 值为 nil ;
child 指向子结点,最后一个结点的 child 值为 nil ;
depth 代表深度,从 0 开始,往后递增 1;
hiwat 代表 high water mark 最大入栈数量标记

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


 static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }

下面的是POOL_BOUNDARY也就是一个哨兵(⾃动释放池的边界)代码。

    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", 
                         objc_thread_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();
        }

当第一次进来时候,会进行AutoreleasePoolPage进行创建,然后把其加入到hotPage里,hotPage也就是我们进行设置的页。 page->add(POOL_BOUNDARY)就是把哨兵对象添加到page里。

    // 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不是第一次创建时候就会走下面的autoreleaseFast方法进行创建。也是通过hotPage进行创建,然后判断page是否满了,如果没有满了的话就进行一个添加到page里。

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

AutoreleasePoolPageData使用

  下面我们看到AutoreleasePoolPage其实就是调用AutoreleasePoolPageData里的数据,在AutoreleasePoolPageData里有个begin()参数,这个是begin()点进去其实是一个内存平移的操作。
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),
                                objc_thread_self(),
                                newParent,
                                newParent ? 1+newParent->depth : 0,
                                newParent ? newParent->hiwat : 0)

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

直接在autoreleasepool里创建p对象是不会进入到autoreleasepool进行管理的,其是通过ARC进行控制其释放和销毁。通过alloc,new ,copy,mutablecopy,allocwith这些创建的对象在autoreleasepool里是不会放到autoreleasepool里进行管理的。 那如果我们要添加到自动释放池,需要在对象前面加__autoreleasing,这样就会受autoreleasepool释放池进行管理( __autoreleasing HPWPerson *p = [ [HPWPerson alloc] init])。其中我们也可以通过系统提供的_objc_autoreleasePoolPrint()进行打印释放池里的对象。如果是通过stringwithformat这类创建的话就会自动添加到autoreleasepool里无需在代码前加入__autoreleasing,其中需要注意stringwithformat后面的string要大于9位数,如果小于等于9位数,那么这个就是taggerPoint小对象了,这个是不会添加到autoreleasepool里的。我们可以通过_objc_autoreleasePoolPrint函数进行打印验证。

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        // insert code here...
        HPWPerson *p =  [ [HPWPerson alloc] init];
    }
    
    return 0;
}

下面是通过_objc_autoreleasePoolPrint的打印,发现其值添加到page里了,其中用的是__autoreleasing进行修饰。


WechatIMG125.jpeg

下面是没有__autoreleasing进行修饰,发现其没有加入到autoreleasepool里


WechatIMG126.jpeg

其实添加__autoreleasing这个也就是在底层操作调用obje_autorelease,在其里面最终调用的是autroreleaseFast这个函数。autroreleaseFast这个函数我们之前说过,其会调用创建page然后进行一个添加操作,也就是添加到autoreleasepool里。

思考1:当我们把下面红圈里的代码放到我们手写的自动释放池autoreleasepool外,想想这句代码会被放到自动释放迟里吗?

WechatIMG859.jpeg

我们打印后发现其String代码还是被加到了自动释放迟中,这个是为什么呢,其实这个是和runloop有关系,当我们程序运行的时候,其就会有开启一个runloop,这个runloop会自动的帮我们创建自动释放池。


WechatIMG860.jpeg

下面是runloop运行的示意图,当APP启动的时候,主线程的runloop是自动开启的,在循环的时候,其会注册两个监听,一个监听是当我们runloop即将进入的时候(也或是从休眠状态进入的时候),就会 创建一个自动释放池,这个优先级最高。当要休眠的时候,其也会有一个监听的函数,监听runloop休眠的时候,也就是等runloop里任务完成后,也就是一次循环结束之前就会倾倒里面的释放池,销毁里面的对象和变量,这个优先级别是最低的,这个是因为其要等runloop里其他事件都完成后才进行此操作。


WechatIMG861.jpeg

通过上面的解释也就知道了为什么我们写在主线程里自己创建的autoreleasepool外面的程序也会被添加到自动释放池里。

上面我们是写在主线程中的,那如果我们写在子线程中,其会被加入到autoreleasepool中吗?下面我们看一个官方文档的介绍:

WechatIMG862.jpeg

上面也就是说,当放在子线程中时候,其也会放到子线程的autoreleasepool中,和主线程中的一致。

总结:子线程的autoreleasepool什么时候销毁,其是在子线程销毁的时候会释放其autoreleasepool,这个是在子线程中没有开启runloop的时候。 如果子线程开启了runloop也就和我们之前说的主线程那个一样了。

autoreleasepool对象在ARC中什么时候释放总结分两种情况

1.手动添加自动释放池里的对象,对象是出了autoreleasepool作用域就会释放
2.系统自动添加到释放池的autoreleasepool对象,这种情况下也要分两种,
1)如果在主线程中或者是在子线程中开启了runloop那就在runloop即将休眠的时候进行释放掉(如界面不动时候runloop也就会休眠)
2)如果在子线程中没有开启runloop,那就在子线程销毁的时候被释放。

下面我们探讨下一个hotPage的大小

一个hotPage的大小是4k,如下图1左移动12位就位4096,也就是4k


WechatIMG863.jpeg

那我们如何验证呢,4096 - 56(本身大小)- 8(哨兵对象) 为 4032。 4032/8 一个hotpage可存504个对象。下面就是对应的验证,我们创建505个对象,就会发现在505的地方会再开启一个新page,当前的page后面会显示hot,而前面放了504个对象的page就会显示cold。如果我们把505改成1009,大家想想会存多少,和开启几个page,其实会开启两个hotpage,第一个存504个对象,第二个存505个对象,这个是因为一个autorelease只有一个哨兵对象(占8字节),所以第二个hotpage就可以存505个对象。

WechatIMG864.jpeg

上面这些操作都是自动释放池push操作,那自动释放池pop操作又是做了些啥呢,我们下面看到再pop时候传了一个参数,其实这个参数就是之前push进来的哨兵对象。

WechatIMG865.jpeg

下面的token就是传进来的哨兵对象,哨兵对象是放在第一位的,通过这个哨兵对象找到哨兵对象所在的page。

8671669474582_.pic.jpg
8661669474556_.pic.jpg

下面的popPage里releaseUntil是进行释放,释放到哨兵对象为止,会从最后面的hot页往前面找,找到哨兵对象所在的page为止,然后调用objc_release(obj)进行释放。

8681669475204_.pic.jpg

那自动释放池我们可以用来做什么呢,可以防止内存短时间内爆涨

其是一个内存自动回收机制,如下,当我们把autoreleasepool去掉的时候,其内存会爆涨。


8691669475895_.pic.jpg

当我们一同autoreleasepool去包裹的时候,其内存变化就会很小,因为得到了及时的释放


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

推荐阅读更多精彩内容