iOS 内存管理--自动释放池底层原理

前言

在内存管理的学习中自动释放池的原理学习是必须的,作为一个合格的iOS开发者,必须要明白自动释放池的操作原理,这篇文章的目的就是探索自动释放池的底层原理。

准备工作

1. 自动释放池

1.1 相关概念

  • 如果在函数、方法的开始处将对象的引用计数加1,在函数、方法不需要该对象的时候将其引用计数减1,这思想基本OK
  • 有些函数、方法需要返回一个对象,而系统可能在该对象被返回之前,就已经销毁了对象。那么为了保证函数、方法返回的对象在被返回之前不被销毁,我们就要使用自动释放池进行延迟销毁(NSAutoreleasePool)。
  • 所谓自动释放池,是指它是一个存放对象的容器(集合),而自动释放池会保证延迟销毁该池中所有的对象。出于自动释放池的考虑,所有的对象都应该添加到自动释放池中,这样可以让自动释放池在销毁之前,先销毁池中的所有对象。
  • autorelease方法。该方法不会改变对象的引用计数,只是将该对象添加到自动释放池中。该方法会返回调用该方法的对象本身
  • 当程序在自动释放池上下文中调用某个对象的autorelease方法时,该方法只是将对象添加到自动释放池中,当该自动释放池释放时,自动释放池会让池中所有的对象执行release方法。
  • 自动释放池的销毁和其他普通对象相同,只要其引用计数为0,系统就会自动销毁自动释放池对象。系统会在调用NSAotoreleasePooldealloc方法时回收该池中的所有对象
  • NSAutoreleasePool还提供了一个drain方法来销毁自动释放池中的对象。与release不同,release会使自动释放池自身的引用计数变为0,从而让系统回收NSAutoreleasePool对象,在回收NSAutoreleasePool对象之前,系统会回收该池中的所有对象。而drain方法则只是回收池中的所有对象,并不会销毁自动释放池

1.2 运行逻辑

AutoReleasePoolOC内存自动回收机制,将加入到AutoReleasePool中的变量release时机延迟。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行,即使超出作用域也不会立即释放,直到runloop休眠或者超出AutoReleasePool作用域才会释放

运行逻辑

自动释放池的运行机制:

  • 程序启动到加载完成,主线程对应的Runloop处于休眠状态,直到用户点击交互唤醒Runloop
  • 用户每次交互都会启动一次Runloop用来处理用户的点击、交互事件
  • Runloop被唤醒后,会自动创建AutoReleasePool,并将所有延迟释放的对象添加到AutoReleasePool
  • 在一次完整的Runloop执行结束前,会自动向AutoReleasePool中的对象发送release消息,然后销毁AutoReleasePool

注意:AutoreleasePoolRunloop的运行机制和关系,在后面讲解Runloop时会详细说明。

1.3 使用效果分析

下面通过一个案例来说明自动释放池的作用。我们常用以下两种方式创建字符串

    // 方式1
    NSString * string1  = [[NSString alloc] initWithFormat:@"hello world..."];
    // 方式2
    NSString * string2  = [NSString stringWithFormat:@"hello world auto relase..."];

那么以上两种方式有什么不同呢?还是老规矩,查看汇编!!!

  • 方式1
NSString * string1  = [[NSString alloc] initWithFormat:@"hello world..."];

打开汇编断点查看流程如下:

方式1汇编流程

这个流程有点熟悉,就是标准的对象创建过程嘛。使用alloc出来的方式,字符串在调用release的时候被回收(假设该字符串没有被其他对象引用变量会在超出其作用域的时候release)。

  • 方式2
    NSString * string2  = [NSString stringWithFormat:@"hello world auto relase..."];

打开汇编断点查看流程如下:

方式2汇编流程

使用stringWith的方式,字符串在api内部会被设置成autorelease,不用手动释放,系统会回收,因此将会在最近的一个自动释放池drainrelease时被回收。

下面通过一个案例来深入了解自动释放池的作用。案例中,使用两种方式创建了字符串,并且把字符串赋值给__weak修饰的成员变量。

  • 案例1
__weak NSString *weakSrting;
__weak NSString *weakSrtingAutoRelease;

@implementation ViewController

- (void)createStringFunc {
        // 方式1
        NSString * string1  = [[NSString alloc] initWithFormat:@"hello world..."];
        weakSrting = string1;
        
        // 方式2
        NSString * string2  = [NSString stringWithFormat:@"hello world auto relase..."];
        weakSrtingAutoRelease = string2;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [self createStringFunc];
    NSLog(@"weakSrting: %@", weakSrting);
    NSLog(@"weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"view will appear weakSrting: %@", weakSrting);
    NSLog(@"view will appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}

- (void) viewDidAppear:(BOOL)animated
{
    NSLog(@"view did appear weakSrting: %@", weakSrting);
    NSLog(@"view did appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}

查看运行结果,如下:


运行结果

结果分析如下:

  • 使用方式1创建的字符串weakSrting,在createStringFunc方法执行完成后就会释放作用域结束),弱引用weakSrting也会释放掉。所以weakSrting打印结果都是空。
  • 使用方式2创建的对象weakSrtingAutoRelease,这个对象被系统自动添加到了当前的autoreleasepool中,起到了延迟释放的效果。这个对象是一个autoreleased对象,autoreleased对象是被添加到了当前最近的autoreleasepool中,只有当这个autoreleasepool自身drain的时候,autoreleasepool中的autoreleased对象才会被release

对象weakSrtingAutoRelease,在viewWillAppear中打印这个对象的时候,能够输出,说明此时对象还没有被释放。但是在viewDidAppear中打印这个对象的时候就成了null了,那么这个对象一定是在viewWillAppearviewDidAppear方法之间的某个时候被释放了,并且是由于它所在的autoreleasepoolrelease的时候释放的。我们可以在lldb调试中设置观察点(watchpoint set v weakSrtingAutoRelease),来查看对象的释放过程,如下:

对象的释放过程

在运行栈中可以发现,weakSrtingAutoRelease对象在自动释放池释放时完成了释放

  • 案例2
    案例1看起来不够直接,那么我们来一个直接点的案例。代码中手动添加了一个@autoreleasepool,在自动释放池内,weakSrtingAutoRelease一直不会释放,而出了自动释放池就会释放。如下:
    案例2

    明显看出使用方式2创建的对象weakSrtingAutoRelease在自动释放池内都能够正常使用,出了自动释放池就会被释放,起到延迟释放的效果。

但是使用方式1创建的字符串weakSrting,为什么在自动释放池内就释放了呢?他不会加入到自动释放池吗?带着疑问继续往下走!!

2. 自动释放池原理分析

2.1 原理初探

通过clang查看自动释放池的实现原理,如下:

main.cpp

@autoreleasepool在编译后变成了以下代码:

    __AtAutoreleasePool __autoreleasepool;

main.cpp文件中全局搜索__AtAutoreleasePool的定义,找到__AtAutoreleasePool结构体的定义,如下:

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

该结构体提供了一个构造函数objc_autoreleasePoolPush和一个析构函数objc_autoreleasePoolPop。所以自动释放池在底层其实是一个结构体,其通过objc_autoreleasePoolPush完成自动释放池的创建objc_autoreleasePoolPop释放自动释放池

设置objc_autoreleasePoolPush的符号断点,的确能够进入,汇编如下:

objc_autoreleasePoolPush符号断点

可以确定自动释放池其实现源码在我们最熟悉的libobjc.A.dylib库。那就非常的nice了!

2.2 结构分析

下面通过源码进行分析。跟踪objc_autoreleasePoolPush的方法实现,如下:

objc_autoreleasePoolPush

其调用了objc_autoreleasePoolPush()方法,继续跟踪代码:
objc_autoreleasePoolPush()

在该方法的实现中,其调用了AutoreleasePoolPagepush方法。那么AutoreleasePoolPage的结构是怎么的呢?如下:
AutoreleasePoolPage

通过AutoreleasePoolPage类的注释可以得到以下关键信息:

  • 一个线程的自动释放池是一堆指针
  • 每个指针要么是一个要释放的对象,要么是POOL_BOUNDARY(自动释放池边界-哨兵对象)
  • 堆栈被分成一个双向链接页面列表, 页面已添加对象并根据需要删除
  • 线程本地存储指向新自动释放的热点页面对象被存储

AutoreleasePoolPage继承于AutoreleasePoolPageData,那么查看AutoreleasePoolPageData的结构如下:

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

属性相关说明:

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

2.3 源码实现

objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

那么我们继续跟踪push()方法,其源码实现如下:

    static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

在非debug模式下首先调用autoreleaseFast方法,并传入边界对象哨兵对象)。查看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);
        }
    }
  • 首先获取当前hotPage,如果不为空且没有满,则会向该页中添加obj
  • 如果该页已满,则调用autoreleaseFullPage方法;
  • 如果当前hotPage不存在,也就是没有page,则调用autoreleaseNoPage方法。autoreleaseNoPage实现源码如下:
    autoreleaseNoPage

    在完成AutoreleasePoolPage创建后,首先添加哨兵对象,然后在加入obj。 首先查看AutoreleasePoolPage构造函数,如下:
    AutoreleasePoolPage

    通过调用AutoreleasePoolPageData的构造函数实现初始化,并确定页之间的链表关系。通过上面的结构我们可以确定AutoreleasePoolPageData属性占56个字节。见下图:
    AutoreleasePoolPageData属性

    因为页中next字段用于设置存储obj的位置,那么因为每个页自身有一些属性需要占用一部分空间,所以next的起始值是page首地址平移56个字节,也就是构造函数中begin()方法所确定下来的值。
 id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

断点调试如下:

begin()断点调试

如果页满时,就会调用上面的autoreleaseFullPage方法,见下面实现源码:

 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.
        ASSERT(page == hotPage());
        ASSERT(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

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

do..while循环找到最后一个page,如果page没有满,就将page设置为hotPage。如果page已满,则会新建一个page,也将page设置为hotPage。最后往page中添加obj

综合上面的数据结构和源码实现,我们可以得出以下结论:

  • Autoreleasepool是由多个AutoreleasePoolPage双向链表的形式连接起来的
  • Autoreleasepool的基本原理:在自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位边界),在此期间,当有对象调用autorelease时,会把对象添加到AutoreleasePoolPage
  • 如果当前页加满了,会初始化一个新页,然后用双向链表链接起来,并把初始化的一页设置为hotPage,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位

2.4 满页临界值

自动释放池一页能够存储多少个对象呢?如果能够打印输出自动释放池的数据,会更便于我们对自动释放池的了解。在源码中也提供了相关的打印数据结构的方法,如下:

void 
_objc_autoreleasePoolPrint(void)
{
    AutoreleasePoolPage::printAll();
}

printAll()方法如下:

    __attribute__((noinline, cold))
    static void printAll()
    {
        _objc_inform("##############");
        _objc_inform("AUTORELEASE POOLS for thread %p", objc_thread_self());

        AutoreleasePoolPage *page;
        ptrdiff_t objects = 0;
        for (page = coldPage(); page; page = page->child) {
            objects += page->next - page->begin();
        }
        _objc_inform("%llu releases pending.", (unsigned long long)objects);

        if (haveEmptyPoolPlaceholder()) {
            _objc_inform("[%p]  ................  PAGE (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
            _objc_inform("[%p]  ################  POOL (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
        }
        else {
            for (page = coldPage(); page; page = page->child) {
                page->print();
            }
        }

        _objc_inform("##############");
    }

创建一个案例查看其内部存储结构,如下:

案例分析

通过上面的输出可以发现,该自动释放池的起始页是0x10380a000,地址平移56个字节后放入的是哨兵对象,哨兵对象地址为0x10380a038,紧接着放入4个对象。那么一页能放多少呢?源码中也有定义,如下:

static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif

#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)

通过以上的源码定义发现其大小为1<<12,也即是4096,而每页自身属性的占用56个字节,同时第一页需要一个哨兵对象8个字节,所以首页最多可以放(4096 - 56 - 8) / 8 = 504个对象。验证一下:

案例分析

案例分析

通过输出自动释放池的数据结构可以发现,当放入505个对象时,会新开辟一页,并且第二页中只有一个对象。(哨兵对象只会放在第一页)所以第一页最多可以放504个对象,之后每页可以存储505个对象。

3. 自动释放池注意点

3.1 对象release而非销毁

引入案例,如下:

案例分析

当自动释放池结束的时候,仅仅是对存储在自动释放池中的对象发送1release消息,而不是销毁对象

3.2 自动释放池的嵌套

引入案例,如下:

案例分析

通过该案例可以发现,自动释放池嵌套并不会影响数据结构,只是多插入一个哨兵对象

3.3 哪些对象可以放入自动释放池

引入案例,如下:

  • MRC环境
    MRC环境
  • ARC环境
    ARC环境

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

推荐阅读更多精彩内容