oc篇-深入理解@autoreleasepool

一、@autoreleasepool到底是干什么的?

使用clang -rewrite-objc main.m

@autoreleasepool {
}

翻译成C++文件可知

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

全局搜索可以得到

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

看代码可知其实是个结构体,上面的代码可以写成

atautoreleasepoolobj = objc_autoreleasePoolPush()
...
objc_autoreleasePoolPop(atautoreleasepoolobj)

二、那objc_autoreleasePoolPushobjc_autoreleasePoolPop又是什么呢?

查看runtime的objc4-646目录下的NSObject.mm源代码可以知道具体的实现

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

  • magic用来校验AutoreleasePoolPage结构是否完整;
  • next指向第一个可用的地址;
  • thread指向当前的线程;
  • parent指向父类
  • child指向子类
id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }    
id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

这两个方法快速获得可用的地址范围

每一个autoreleasepool都是由一个或多个AutoreleasePoolPage的双向链表组成的

    static size_t const SIZE       PAGE_MAX_SIZE;  // size and alignment, power of 2
    static size_t const COUNT = SIZE / sizeof(id);

展开就是

#define I386_PGBYTES        4096        /* bytes per 80386 page */
#define PAGE_MAX_SIZE           PAGE_SIZE

所以:AutoreleasePoolPage的大小都是一样的4096

关于AutoreleasePoolPage的数据结构借用一下Draveness的图

1975281-366d56087b4600cf.png

大致搞清楚了autoreleasepool的结构我们再下面从autoreleasepool的创建和对象如何加到autoreleasepool来具体说说中间都发生了什么

三、autoreleasepool的创建

当我们使用@autoreleasepool { }的时候,由上面知道实际是先调用了objc_autoreleasePoolPush方法,我们先看看objc_autoreleasePoolPush的实现

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

展开其实就是

static inline void *push() 
    {
        id *dest = autoreleaseFast(POOL_SENTINEL);
        assert(*dest == POOL_SENTINEL);
        return dest;
    }

相信大家都看到了autoreleaseFast()这个方法

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

这个方法就是AutoreleasePoolPage具体初始化的地方,看实现知道有三种情况

  • hotPage存在并且容量没有满,直接添加对象
  • hotPage存在但是容量已经满了,调用autoreleaseFullPage方法,初始化一个AutoreleasePoolPage并把page传入,并标记为hotPage;
  • hotPage不存在,调用autoreleaseNoPage创建一个AutoreleasePoolPage,并标记为hotePage,并且添加一个POOL_SENTINEL(哨兵对象)

上面的autoreleaseFast方法就是创建autoreleasepool的核心实现
另外看到这个关键字了吧inline,其实就是内联函数,可以提供执行速度,可以看看我之前的文章

总结:每次push会产生一个新的autoreleasepool,并生成一个POOL_SENTINEL

四、对象是如何加到autoreleasepool中去的

说完autoreleasepool的创建,接下来说对象是如何加到autoreleasepool中去的,当对象调用【object autorelease】的方法的时候就会加到autoreleasepool中

+ (id)autorelease {
    return (id)self;
}

// Replaced by ObjectAlloc
- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

展开为

inline id 
objc_object::rootAutorelease()
{
    assert(!UseGC);

    if (isTaggedPointer()) return (id)this;
    if (fastAutoreleaseForReturn((id)this)) return (id)this;

    return rootAutorelease2();
}

再展开

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

其实就是AutoreleasePoolPage的autorelease方法

   static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  *dest == obj);
        return obj;
    }

是不是觉得似曾相识?是的,和之前的push方法几乎一样,就是入参不一样。

  • push方法传的是POOL_SENTINEL
  • autorelease方法是传的具体需要autoRelease对象

细心的同学肯定发现了assert(!obj->isTaggedPointer());这个断言里面的isTaggedPointer了吧,这个是苹果后面为了提高内存利用率推出来的伪指针,要想深入了解可以点击此处

对比完autoreleasepool的创建和对象如何加到autoreleasepool我们可以得出下面的结论:

  1. 只有push操作会产生POOL_SENTINEL
  2. POOL_SENTINEL的个数就是autoreleasepool的个数,实际开发中会有嵌套使用的情况。

五、说完添加接着来说说移除pop

当执行到这个objc_autoreleasePoolPop方法的时候
autoreleasepool会向POOL_SENTINEL地址后面的对象都发release消息,直到第一个POOL_SENTINEL对象截止。

1433231260106282.jpg

我们来理解一下这个图:

  1. 首先看到有两个POOL_SENTINEL说明autoreleasepool还嵌套了一个autoreleasepool,当最新的autoreleasepool执行objc_autoreleasePoolPop方法的时候,最右边的AutoreleasePoolPage向里面的对象发release消息直到中间那个POOL_SENTINEL为止
  2. coldPage方法的定义可知最左边的那个是coldPage
 static inline AutoreleasePoolPage *coldPage() 
    {
        AutoreleasePoolPage *result = hotPage();
        if (result) {
            while (result->parent) {
                result = result->parent;
                result->fastcheck();
            }
        }
        return result;
    }
1433231268131918.jpg

依次类推。。。

六、实践

理论说完了,究竟在什么地方用这个呢,看苹果官方文档

  1. 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  2. 如果你编写的循环中创建了大量的临时对象;
  3. 如果你创建了一个辅助线程。

官方提供了一个例子

NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
 
    @autoreleasepool {
        NSError *error;
        NSString *fileContents = [NSString stringWithContentsOfURL:url
                                         encoding:NSUTF8StringEncoding error:&error];
        /* Process the string, creating and autoreleasing more objects. */
    }
}

这里特地写了一个demo测试这个

static const int kStep = 50000;
static const int kIterationCount = 10 * kStep;
//查看app运行内存
- (void)obserMemoryUsage{
    NSNumber *num = nil;
    NSString *str = nil;
    for (int i = 0; i < kIterationCount; i++) {
        @autoreleasepool {
        
           num = [NSNumber numberWithInt:i];
          str = [NSString stringWithFormat:@"打哈萨克的哈克实打实的哈克时间的话大声疾呼多阿萨德爱仕达按时 "];
            
            //Use num and str...whatever...
            [NSString stringWithFormat:@"%@%@", num, str];
            
            if (i % kStep == 0) {
                double ff  =     getMemoryUsage();
                NSLog(@"%f",ff);
            }
        }
    }
}
double getMemoryUsage(void) {
    struct task_basic_info info;
    mach_msg_type_number_t size = sizeof(info);
    kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
    
    double memoryUsageInMB = kerr == KERN_SUCCESS ? (info.resident_size / 1024.0 / 1024.0) : 0.0;
    
    return memoryUsageInMB;
}

统一操作了3次我们对比一下内存占用情况

加了 @autoreleasepool {}

控制台

A0CC8527-0555-4744-A52A-B5D8005BCC01.png

不加 @autoreleasepool {}

788D40F9-EDDD-4728-AA58-69777BBC137C.png

同样是3次加了 @autoreleasepool {}内存稳定在73左右,而不加 @autoreleasepool {}的会出现暴增的情况。

@autoreleasepool {}平时不怎么关心,仔细研究起来还是挺有用处的。

reference:

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

推荐阅读更多精彩内容