likes
comments
collection
share

iOS内存管理——内存管理(自动释放池AutoreleasePool)

作者站长头像
站长
· 阅读数 26

1.自动释放池

1.相关概念

  1. 如果在函数、方法的开始处将对象的引用计数加1,在函数、方法不需要该对象的时候将其引用计数减1,这思想基本OK

  2. 问题:有些函数、方法需要返回一个对象,而系统可能在该对象被返回之前,就已经销毁了对象。那么为了保证函数、方法返回的对象在被返回之前不被销毁,我们就要使用自动释放池进行延迟销毁(NSAutoreleasePool)。

  3. 所谓自动释放池,是指它是一个存放对象的容器(集合),而自动释放池会保证延迟销毁该池中所有的对象。出于自动释放池的考虑,所有的对象都应该添加到自动释放池中,这样可以让自动释放池在销毁之前,先销毁池中的所有对象。

  4. autorelease方法。该方法不会改变对象的引用计数,只是将该对象添加到自动释放池中。该方法会返回调用该方法的对象本身。

  5. 当程序在自动释放池上下文中调用某个对象的autorelease方法时,该方法只是将对象添加到自动释放池中,当该自动释放池释放时,自动释放池会让池中所有的对象执行release方法。

  6. 自动释放池的销毁和其他普通对象相同,只要其引用计数为0,系统就会自动销毁自动释放池对象。系统会在调用NSAotoreleasePooldealloc方法时回收该池中的所有对象。

  7. NSAutoreleasePool还提供了一个drain方法来销毁自动释放池中的对象。与release不同,release会使自动释放池自身的引用计数变为0,从而让系统回收NSAutoreleasePool对象,在回收NSAutoreleasePool对象之前,系统会回收该池中的所有对象。而drain方法则只是回收池中的所有对象,并不会销毁自动释放池。

2.运行逻辑

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

自动释放池的运行机制:

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

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

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..."];
    

    iOS内存管理——内存管理(自动释放池AutoreleasePool) 使用alloc出来的方式,字符串在调用release的时候被回收(假设该字符串没有被其他东西引用,变量会在超出其作用域的时候release)。

  • 方式2

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

    iOS内存管理——内存管理(自动释放池AutoreleasePool) 使用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);
    }
    

    查看运行结果: iOS内存管理——内存管理(自动释放池AutoreleasePool)

    使用方式1创建的字符串weakSrting,在createStringFunc方法执行完成后就会释放(作用域结束),弱引用weakSrting也会释放掉。所以weakSrting打印结果都是空。

    使用方式2创建的对象weakSrtingAutoRelease,这个对象被系统自动添加到了当前的autoreleasepool中,起到了延迟释放的效果。这个对象是一个autoreleased对象,autoreleased对象是被添加到了当前最近的autoreleasepool中的,只有当这个autoreleasepool自身drain的时候,autoreleasepool中的autoreleased对象才会被release

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

    iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

  • 场景2(ARC)

    此种方案更直观一些,代码中手动添加了一个@autoreleasepool,在自动释放池内,weakSrtingAutoRelease一直不会释放,而出了自动释放池就会释放。见下图:

    iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

    但是使用方式1创建的字符串weakSrting,为什么在自动释放池内就释放了呢?他不会加入到自动释放池吗?这个问题下面会说明!!!

2.自动释放池原理分析

1.原理初探

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

    __AtAutoreleasePool __autoreleasepool;

全局搜索__AtAutoreleasePool的定义,找到__AtAutoreleasePool结构体的定义:

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

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

通过设置符号断点,查看汇编,可以确定自动释放池其实现源码在我们最熟悉的libobjc.A.dylib库。

iOS内存管理——内存管理(自动释放池AutoreleasePool)

2.结构分析

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

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

上面的注释信息如何理解呢?查看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最⼤⼊栈数量标记

3.源码实现

跟踪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实现源码如下:

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

跟踪实现代码,会发现其返回的是AutoreleasePoolPage,综合上面的数据结构和源码实现,我们可以得出以下结论:

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

iOS内存管理——内存管理(自动释放池AutoreleasePool)

4.满页临界值

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

    void 
    _objc_autoreleasePoolPrint(void)
    {
        AutoreleasePoolPage::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("##############");
    }

引入下面就的案例查看其内部存储结构:

iOS内存管理——内存管理(自动释放池AutoreleasePool)

通过上面的输出可以发现,该自动释放池的起始页是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个对象。验证一下:

iOS内存管理——内存管理(自动释放池AutoreleasePool) iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

3.自动释放池注意点

1.对象release而非销毁

先看下面的案例:

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

2.自动释放池的嵌套

自动释放池可以嵌套!

iOS内存管理——内存管理(自动释放池AutoreleasePool)

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

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

依然通过案例进行分析。

  • MRC环境

    iOS内存管理——内存管理(自动释放池AutoreleasePool)

  • ARC环境

    iOS内存管理——内存管理(自动释放池AutoreleasePool)

1.主动调用autorelase方法的,用alloc, init,copy等方法创建的对象,这些我们自己持有的,我们想让他延迟释放,就调用autorelase方法,这样在自动释放池出栈的时候,对象就会释放掉。

2.对于那种stringWithFormt这种从名字来看,没有被调用者持有的情况,要么是自动加到自动释放池里的,要么是常量字符串,不用引用计数来管理。

转载自:https://juejin.cn/post/7010726670181253127
评论
请登录