iOS基础(十二) - 自动释放池(NSAutoreleasePool)

BGImage.png

前言:因为所以,闲的蛋疼,搞搞事,撸一下NSAutoreleasePool。

autoreleasePool是什么?什么是autoreleasePool?😄

先来一段代码:

//MRC
id object;
{
  NSString *str1 = @"1";
  NSString *str2 = @"2";
  NSArray *array = @[str1, str2];
  NSLog(@"array: %ld", CFGetRetainCount(array));
  object = [NSArray arrayWithArray: array];
  NSLog(@"array2: %ld", CFGetRetainCount(array));
  NSLog(@"object: %ld", CFGetRetainCount(object));
  [array release];
}

2018-01-18 20:29:11.307512+0800 BasicDemo[50165:1729663] array: 1
2018-01-18 20:29:11.307789+0800 BasicDemo[50165:1729663] array2: 1
2018-01-18 20:29:11.307831+0800 BasicDemo[50165:1729663] object: 1

想想在 MRC 时代,需要手动插入 retainrelease,比如上面的代码在一个函数域中,array 是局部变量,object 是全局变量,一旦函数调用完毕,需要手动调用 release,释放掉 array, 但是,这里的全局变量并不持有 array,也就是说,一旦 array 被释放掉了,全局变量 object 也会变成 nil,这时候访问 object 会有 crash 的风险。那怎么办呢?
有两个方法:
方法一:对象object想长期持有array,可以直接retain数组array。
方法二:假如object只是想暂时持有array,这时候可以使用autoreleasePool来延时释放对象array。

所以,autoreleasePool 主要作用就是延时释放对象,释放对象也是调用release方法。在 ARC 时代,一般很少用autoreleasePool,因为,我们创建对象一般会用strong或者copy来修饰对象,这些修饰词会让对象持有所赋值对象或者新开辟一块内存,创建一个新的对象持有,所以,不必担心。

ARC与autoreleasePool

ARC和autoreleasePool其实是不同的机制,两者并没有冲突。无论MRC还是ARC都有autoreleasePool,autoreleasePool只是延时释放对象的一种机制。可以实现在不持有对象的所有权的情况下,使用对象,不持有也就不用担心对象的内存管理问题,只管使用就好。

ARC环境下,什么对象会加入autoreleasePool?

(1)alloc/new/copy/mutableCopy等持有对象的方法,不会加入autoreleasePool;其他不持有对象的方法通过objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue来判断是否需要加入autoreleasePool,这是编译器的优化。

id object = [[NSObject alloc] init];    //自己生成并持有对象,不需要加入autoreleasePool
id object2 = [NSMutableArray array];    //MRC:不是自己生成并且不持有对象,需要加入autoreleasePool
id object3 = [NSMutableArray array];
[object3 retain];    //MRC:不是自己生成,但持有对象,不需要加入autoreleasePool

(2)iOS5及之前的编译器,关键字__weak修饰的对象,会自动加入autoreleasePool;iOS5及之后的编译器,则直接调用的release,不会加入autoreleasePool。

//ARC源码
id obj = [[NSObject alloc] init];
{
    id __weak obj1 = obj;
    NSLog(@"%@", obj1);
}

/* iOS5及之前编译器做法 */
id obj1;
objc_initWeak(&obj1, obj);
-----------------------
//调用方法之前,先新生成一个临时对象retain,然后再将该临时对象加入autoreleasePool,这样可以避免对象提前释放掉
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
-----------------------
//对象释放掉,清除所有的弱引用,并将对象置为nil
objc_destroyWeak(&obj1);

/* 现在的编译器做法*/
id obj = objc_msgSend(NSObject, "new");
id obj1;
objc_initWeak(&obj1, obj);
//调用方法前,先新生成一个临时对象retain
-----------------------
id tmp = objc_loadWeakRetained(obj1);
NSLog(@"%@", obj1);
//方法调用完之后,再release该临时对象,没有加入autoreleasepool
objc_release(tmp);
-----------------------
//释放对象
objc_destroyWeak(&obj1);

(3)id指针和对象指针(id *,NSError **),会自动加上关键字__autorealeasing,加入autoreleasePool。
资料参考:
《黑幕背后的Autorelease》《iOS高级编程--内存管理》Why __weak object will be added to autorelease pool?《iOS高级编程--内存管理》

线程 & runLoop & autoreleasePool

如下图:

Thread & RunLoop & AutoreleasePool.png

ibireme 大神的深入理解RunLoop可以知道:
(1)在cocoa框架下,每个线程都会有一个默认不开启的runLoop(主线程的runLoop默认是开启)。
(2)每个runLoop会创建autoreleasePool。
(3)runLoop即将进入休眠或者runLoop即将退出时,会释放旧的autoreleasePool对象,创建新的autoreleasePool对象。
苹果官方文档Advanced Memory Management Programming Guide还可以知道:
(1)cocoa应用里每个线程都维护自己的一个autoreleasePool栈。
(2)如果循环产生很多临时变量,可以使用本地的autoreleasePool来降低内存的峰值。
(3)子线程,如果没有开启RunLoop,是不会自动创建autoreleasePool;但是,如果出现autorelease对象,则会自动创建autoreleasePool。
参考:does NSThread create autoreleasepool automatically now?以及Objective-C Autorelease Pool 的实现原理

autoreleasePool实现原理

可通过objc库的NSObject.mm来查看autorelease的实现(推荐一个编译Runtime源码的blog)。

这里会说一些理解的关键点,更详细的分析请移步:黑幕背后的AutoreleaseObjective-C Autorelease Pool 的实现原理

首先,使用clang命令,获得@autoreleasepool的cpp文件:

/* @autoreleasepool */ 
{
     __AtAutoreleasePool __autoreleasepool; 
}

__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_autoreleasePoolPush()
//析构函数调用的是objc_autoreleasePoolPop(atautoreleasepoolobj)

可以得出autoreleasepool的创建和释放代码如下:

void *atautoreleasepoolobj = objc_autoreleasePoolPush();
...
objc_autoreleasePoolPop(atautoreleasepoolobj)

因为这两个函数都是extern修饰的,想要查看具体的实现还需要去看objc的源码
在NSObject.mm源码可以找到AutoreleasePoolPage类:

autoreleasePoolPage.png
class AutoreleasePoolPage 
{
    ...
    #define POOL_BOUNDARY nil
    static size_t const SIZE = 4096;    //AutoreleasePoolPage的内存大小
    static size_t const COUNT = SIZE / sizeof(id);    

    magic_t const magic;  //一些校验信息
    id *next;    //当前加入autoreleasepool对象的下一个位置
    pthread_t const thread;  //当前使用autoreleasePool的线程
    AutoreleasePoolPage * const parent;  //上一个autoreleasePage
    AutoreleasePoolPage *child;  //下一个autoreleasePage
    uint32_t const depth;  //autoreleasePage的深度
    uint32_t hiwat;

    static inline void *push() {
     //创建并持有NSAutoreleasePool对象
    }

    static inline void pop(void *token) {
    //废弃NSAutoreleasePool对象,释放pool里面的所有对象
    }

    static inline id autorelease(id obj) {
        ...
        //将obj对象加入NSAutoreleasePool里面
        id *dest __unused = autoreleaseFast(obj);
        ...
        return obj;
    }

     static inline id *autoreleaseFast(id obj)
     {
         AutoreleasePoolPage *page = hotPage();
         if (page && !page->full()) {    //page存在并且没有满,则添加当前对象
             return page->add(obj);
         } else if (page) {    //page存在,但是page已满,则调用autoreleaseFullPage(obj, page)
             return autoreleaseFullPage(obj, page);
         } else {    //page不存在,则调用autoreleaseNoPage(obj)
             return autoreleaseNoPage(obj);
         }
     }

     static __attribute__((noinline))
     id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
     {
        ...
         do {
             if (page->child) page = page->child;
             else page = new AutoreleasePoolPage(page);
         } while (page->full());   //寻找到不为空的page,否则,新建一个空的page

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

     static __attribute__((noinline))
     id *autoreleaseNoPage(id obj)
     {
         ...
        
         // 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);
     }

     id *add(id obj) {
      //添加对象
     }

     id * begin() {
         //当前autoreleasePage开始存放对象的地址
         return (id *) ((uint8_t *)this+sizeof(*this));
     }

     id * end() {
          //当前autoreleasePage的最高地址
          return (id *) ((uint8_t *)this+SIZE);
     }

     bool empty() {
        //当前的autoreleasePage已满
         return next == begin();
     }

     bool full() { 
         //当前autoreleasePage为空
         return next == end();
     }

     void releaseAll() 
     {
     //释放当前缓冲池的所有对象
     }
}

autoreleasePool的组成结构:

autoreleasePool.png
注意几点:

1.autoreleasePool调用push返回来的是hotPage里面的POOL_BOUNDARY对象的位置。
2.每个线程独立管理自己的自动释放池。
3.autoreleasePool由NSAutoreleasePoolPage组成,以双向链表的形式。
4.autoreleasePool可以嵌套,一个autoreleasePage可以包含一个或者多个autoreleasePool,一个autoreleasePool可以跨越多个autoreleasePage。
5.autoreleasePool释放对象是从next-1的位置开始释放,直到POOL_BOUNDARY的位置。
6.autoreleasePool如果有多个嵌套,释放的时候要从最里面的autoreleasePool开始释放,直到最外面的autoreleasePool。
7.调用autorelease方法,如果没有NSAutoreleasePoolPage,则会创建一个。

参考
黑幕背后的Autorelease
Objective-C Autorelease Pool 的实现原理
iOS中autorelease的那些事儿
@autoreleasepool-内存的分配与释放
深入理解RunLoop
Advanced Memory Management Programming Guide
Objective-C高级编程 iOS与OS X多线程和内存管理
引用计数带来的一次讨论
各个线程 Autorelease 对象的内存管理
iOS高级编程--内存管理

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

推荐阅读更多精彩内容