iOS--AutoreleasePool的解析

NSAutoreleasePool(自动释放池):双链表。
本文主要从NSAutoreleasePool官方文档介绍、NSAutoreleasePool的应用场景、NSAutoreleasePool的源码阅读、NSAutoreleasePoolRunLoop分析这四个方面去总结和解析自动释放池。

一、NSAutoreleasePool官方文档介绍

  • NSAutoreleasePool官方文档已经介绍的很详尽,其中有这样一段话:
    If you use Automatic Reference Counting (ARC), you cannot use autorelease pools directly. Instead, you use @autoreleasepool blocks. For example, in place of:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];

you would write:

@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

@autoreleasepool blocks are more efficient than using an instance of NSAutoreleasePool directly; you can also use them even if you do not use ARC.
在我们的ARC模式下,我们不可以直接使用自动释放池,可以用@autoreleasepool的block块代替。@autoreleasepool的block块比直接使用NSAutoreleasePool实例使用更有效。即使不是ARC模式,也可以使用。更详尽的介绍,在官方文档里都有,可以网络翻译过来查看。

  • Application Kit 在事件循环开始的时候在主线程创建了一个自动释放池,并且在结束的时候去清空它,从而释放所有进程事件中生成的自动释放的对象。如果使用了 Application Kit ,就没必要再去创建自己的自动释放池。然而,如果你的应用在事件循环中创建了很多临时的自动释放的对象,创建临时的自动释放池会将有助于削减你内存峰值的占用。
  • 每一个线程(包括主线程)包含一个它自己的自动释放池对象的栈。作为一个新的被创建的池子,它们被添加到栈的顶部。当池子被释放的时候,它们从栈中被移除。自动释放的对象被放在当前线程的自动释放池的顶部。当一个线程终止的时候,它自动清空与它关联的所有的自动释放池。
  • 如果应用或线程是长久保存的并且潜在的生成了很多自动释放的对象,这时应该定期的清空并且创建自动释放池(就像 Application Kit 在主线程中做的那样);否则,对象的积累会增加内存的占用。如果,独立的线程并没有使用 Cocoa 的调用,你没有必要去创建一个自动释放池。
  • 一个 autorealese 对象的释放时机:手动指定 autoreleasepool 的 autorelease 对象,在当前作用域大括号结束时释放。不手动指定 autoreleasepool 的 autorelease 对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 Push 和 Pop。一个典型的例子是在一个类方法中创建一个对象并作为返回值,这时就需要将该对象放置到对应的 autoreleasepool 中。

二、NSAutoreleasePool的应用场景:

关于自动释放池使用场景,我们可以从一段经典for循环入手:

for (int i = 0; i < 1000000; i++) {
        @autoreleasepool {
             NSNumber *num = [NSNumber numberWithInt:i];
             NSString *str = [NSString stringWithFormat:@"%d ", I];
              //Use num and str...whatever...
             [NSString stringWithFormat:@"%@%@", num, str];
        }
    }

这段代码或者类似的代码都能在网上找到,但是它真的有问题吗?套用一个大神做的案例:作者是对NSNumber和NSString做了实验。

//Run loop without autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            NSNumber *num = [NSNumber numberWithInt:i];
            NSString *str = [NSString stringWithFormat:@"%d ", I];
            //Use num and str...whatever...
            [NSString stringWithFormat:@"%@%@", num, str];
            if (i % kStep == 0) {
                [_memoryUsageList2 addObject:@(getMemoryUsage())];
            }
        }
    });

//Run loop with autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            @autoreleasepool {
                NSNumber *num = [NSNumber numberWithInt:i];
                NSString *str = [NSString stringWithFormat:@"%d ", I];
                //Use num and str...whatever...
                [NSString stringWithFormat:@"%@%@", num, str];
                NSObject *object = [NSObject new];
                if (i % kStep == 0) {
                    [_memoryUsageList1 addObject:@(getMemoryUsage())];
                }
            }
        }
    });

然后运行两个for循环,查看内存的变化:


NSNumber和NSString

通过图中的对比,使用@autoreleasepool比不使用内存耗费的确实少,先不要下结论,我在后面有加了其他的实验。

  • 自定义Person类继承自NSObject,使用自定义对象类来进行第二次试验:
//Run loop with autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            @autoreleasepool {
                Person *p = [Person new];
                p.page = I+1;
                if (i % kStep == 0) {
                    [_memoryUsageList1 addObject:@(getMemoryUsage())];
                }
            }
        }
    });
    
    //Run loop without autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            Person *p = [Person new];
            p.page = I+1;
            if (i % kStep == 0) {
                [_memoryUsageList2 addObject:@(getMemoryUsage())];
            }
        }
    });
自定义Person类继承自NSObject

通过上图我们发现,自定义Person类继承自NSObject。反倒在开始的时候,不使用@autoreleasepool占用内存更小,但是后来慢慢的也趋于一致。

  • 第三次试验,我们用UIView对象:
//Run loop with autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            @autoreleasepool {
                UIView *view = [UIView new];
                if (i % kStep == 0) {
                    [_memoryUsageList1 addObject:@(getMemoryUsage())];
                }
            }
        }
    });
    
    //Run loop without autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            UIView *view = [UIView new];
            if (i % kStep == 0) {
                [_memoryUsageList2 addObject:@(getMemoryUsage())];
            }
        }
    });
UIView对象

额,什么鬼,使用和不使用@autoreleasepool内存都会暴增,但是使用@autoreleasepool内存还是比不适用小,有图为证:


屏幕快照 2019-05-13 下午11.12.54.png

虽然内存都会暴增,但是我们明显可以看到使用@autoreleasepool内存要更小。

  • 我们进行第四次试验,使用NSDictionary对象:
//Run loop with autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            @autoreleasepool {
                NSDictionary *dictionary = [[NSDictionary alloc] init];
                if (i % kStep == 0) {
                    [_memoryUsageList1 addObject:@(getMemoryUsage())];
                }
            }
        }
    });
    
    //Run loop without autoReleasePool
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < kIterationCount; i++) {
            NSDictionary *dictionary = [[NSDictionary alloc] init];
            if (i % kStep == 0) {
                [_memoryUsageList2 addObject:@(getMemoryUsage())];
            }
        }
    });

通过试验我们发现,使用NSDictionary创建出来的对象,无论是否使用@autoreleasepool,内存均不会有影响:


NSDictionary

与之类似的还有NSArray,NSObject。

通过以上试验,再结合引用计数带来的一次讨论和iOS开发笔记(七):深入理解Autorelease的文章,我们可以总结一下结论(for循环的次数均为50W次):

1.已知的NSDictionary ,NSArray,NSObject创建的对象,会自动添加到自动释放池中。
2.我们用NSObect自定义的子类,不需要pool管理,系统会自动的帮他在合适位置释放掉。我们可以通过内存看到不使用@autoreleasepool反而内存管理的更好。数据对比我们发现用NSObect自定义的子类比NSObject创建的对象的时候更节省内存开销。
3.NSString、NSNumber、UIImage和UIView对象,当一个循环中有大量临时变量产生时,使用@autoreleasepool是最好的选择。当然正常开发中可能不会用到UIView循环这么多次。
4.使用容器的 block 版本的枚举器时,内部会自动添加一个 AutoreleasePool。当然使用for循环的话,@autoreleasepool需要自己手动添加。例如数组的enumerateObjectsUsingBlock方法:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 这里被一个局部@autoreleasepool包围着
}];
数组的enumerateObjectsUsingBlock方法

三、NSAutoreleasePool的源码阅读:

我们在解析之前,首先需要做一些准备工作。苹果官方runtime源码、笔者自己下载的一套750源码。另外为了分析底层原理,我们需要将Objective-C对象转化为C/C++,以及两个概念:析构函数与构造函数。
构造函数:用于新建对象的初始化工作。
析构函数:用于在撤销对象之前,完成一些清理工作。例如释放内存。
每当创建对象时,初始化需要定义自己的构造函数,对象销毁需要定义自己的析构函数。

  • 1>将Objective-C对象转化为C/C++,我们可以执行:
cd 某文件的路径下
xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m

该命令行将Objective-C对象转化为C/C++,注意ViewController.m是你要转换的.m文件。需要注意的是在你要转换的.m文件中,头文件、宏定义、自定义变量必须是在该.m文件中存在的,不存在就会转换不了。(我试过)
可能你执行完上面的命令,会报错:

xcrun: error: SDK "iphonesimulator" cannot be located

这个时候需要你给Xcode命令行工具指定路径:

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/

执行完该命令后,重复执行以上操作即可。
简单的自动释放池应用:

    for (int i = 0; i < 1000000; i++) {
        @autoreleasepool {
            NSString *str = @"abc";
            str = [str lowercaseString];
            str = [str stringByAppendingString:@"xyz"];
        }
    }

经过我们的转化,我们可以从转化的文件夹里看到一个.cpp文件(感觉C和C++不是人能写的,哈哈,开个玩笑):


屏幕快照 2019-05-15 上午11.07.12.png

双击红色箭头指向的.cpp文件,查阅我们转化后的代码:

for (int i = 0; i < 1000000; i++) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            NSString *str = (NSString *)&__NSConstantStringImpl__var_folders_22_cvfxq3v91pz2fs_z37pckgqh0000gn_T_AutoreleasePoolVC_bbec29_mi_1;
            str = ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)str, sel_registerName("lowercaseString"));
            str = ((NSString *(*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)str, sel_registerName("stringByAppendingString:"), (NSString *)&__NSConstantStringImpl__var_folders_22_cvfxq3v91pz2fs_z37pckgqh0000gn_T_AutoreleasePoolVC_bbec29_mi_2);
        }
    }

我们在转化后的程序中发现了 __AtAutoreleasePool,也就是说:

@autoreleasepool {

}

实际上是转化为这样子的一个结构体实现的:

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

}

那么 __AtAutoreleasePool到底是何许人也呢,不要急我们继续在.cpp文件中全局搜索,可以找到__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;
};

根据我们上文提到的析构函数与构造函数的定义,可以知道__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}__AtAutoreleasePool的构造函数,用于自动释放池的初始化。而~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}__AtAutoreleasePool的析构函数,用于__AtAutoreleasePool的释放内存。所以上面的程序执行可以写成如下形式:

for (int i = 0; i < 1000000; i++) {
        /* @autoreleasepool */ {
            
            void *atautoreleasepoolobj = objc_autoreleasePoolPush();//自动释放池的创建
            //中间产生的对象放在自动释放池中
            NSString *str = (NSString *)&__NSConstantStringImpl__var_folders_22_cvfxq3v91pz2fs_z37pckgqh0000gn_T_AutoreleasePoolVC_bbec29_mi_1;
            str = ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)str, sel_registerName("lowercaseString"));
            str = ((NSString *(*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)str, sel_registerName("stringByAppendingString:"), (NSString *)&__NSConstantStringImpl__var_folders_22_cvfxq3v91pz2fs_z37pckgqh0000gn_T_AutoreleasePoolVC_bbec29_mi_2);

            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
    }
  • 2>源码解读:在我们下载好的源代码中,全局搜索objc_autoreleasePoolPush方法的实现,发现最终都是由AutoreleasePoolPage调用push方法实现的:
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

所以AutoreleasePool的实现和AutoreleasePoolPage有着密不可分的关系,我们可以在源码NSObject.mm文件中找到这样一段话:

/***********************************************************************
   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. 
**********************************************************************/

它简单的介绍了AutoreleasePool的实现流程:线程的自动释放池是指针的栈。每个指针要么是要释放的对象,要么是POOL_BOUNDARY(AutoreleasePoolPage的边界),池子的token是指向该池的POOL_BOUNDARY的指针。当池被弹出,优先释放对象,其次是哨兵。该栈被分成许多page(AutoreleasePoolPage)组成的双链表,必要的时候page会被添加和删除。 Thread-local storage points to the hot page, where newly autoreleased objects are stored。(这句话翻译不好...)
下面笔者将围绕着AutoreleasePoolPage、objc_autoreleasePoolPush、objc_autoreleasePoolPop的相关实现展开源码分析。

AutoreleasePoolPage的介绍:

AutoreleasePoolPage是C++中的一个类,我们可以在源码中的NSObject.mm文件中找到它的定义(此处只截取了AutoreleasePoolPage的变量部分):

class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}
  • EMPTY_POOL_PLACEHOLDER(占位的空池子):当一个pool恰好被push并且它绝不包含任何对象的时候,EMPTY_POOL_PLACEHOLDER存储在TLS(Thread-local storage 存储本地的线程)中。当顶层(即libdispatch)推送和弹出pools,却没有使用这些pools的时候,可以节省内存开销。
  • POOL_BOUNDARY:在宏定义中我们看到,它是一个nil对象。AutoreleasePoolPage的边界。
  • SIZE:AutoreleasePoolPage的大小,我们可以在源码中找到它大小为4096个字节。虚拟内存页面的大小,提高效率。
  • COUNT:一个page里对象数。
  • magic:检查校验完整性的变量。
  • next:指向新加入的autorelease对象。
  • thread:当前page所在的线程。
  • parent:指向前一个page的指针。
  • child:指向后一个page的指针。
  • depth:双向链表的深度,即该链表中结点的个数。
  • hiwat:数据容纳的一个上限。

自此我们看到AutoreleasePoolPage类中有parent和child两个指向AutoreleasePoolPage的指针,所以我们判定AutoreleasePool是由AutoreleasePoolPage组成的一个双链表。*next

屏幕快照 2019-05-15 下午11.18.24.png
objc_autoreleasePoolPush:

在源码中,我们可以找到objc_autoreleasePoolPush的实现:

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

本质上还是AutoreleasePoolPage调用自身的push方法:

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

在push方法中,有一个if条件判断句。DebugPoolAllocation判断是否是调试模式(当自动释放池出现顺序错误时停止,并允许堆调试器跟踪自动释放池),序号1代码执行dest = autoreleaseNewPage(POOL_BOUNDARY);,序号2代码执行dest = autoreleaseFast(POOL_BOUNDARY);我们将根据次序号1和2来分析push方法其中的实现逻辑。

  • 序号1(autoreleaseNewPage方法,每个自动释放池都从一个新的AutoreleasePoolPage开始):
id *autoreleaseNewPage(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page) 
        return autoreleaseFullPage(obj, page);
        else 
        return autoreleaseNoPage(obj);
    }

autoreleaseNewPage方法中,id obj就是我们的POOL_BOUNDARY(page的边界)。hotPage()个人认为是使用key-value的形式获取到当前的AutoreleasePoolPage。hotPage()源码如下:

static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);//`hotPage()`个人认为是使用key-value的形式获取到当前的AutoreleasePoolPage。
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }

另外我们也看到了,在if条件中,通过判断page是否存在,程序执行了autoreleaseFullPageautoreleaseNoPage其中的一个方法。

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

我们将依次对autoreleaseFullPage方法中的执行语句进行分析。注意id obj是我们page的POOL_BOUNDARY,AutoreleasePoolPage *page为hotPage():

  • assert(page == hotPage());assert是一个断言函数,括号中的page == hotPage()条件如果成立的话,再执行下面的程序。一般assert函数在测试条件下使用,频繁使用会影响程序性能。
  • assert(page->full() || DebugPoolAllocation);同样也是一个断言函数。DebugPoolAllocation就是判断是否是调试模式。page->full()是检测AutoreleasePoolPage空间是否使用完了。full()函数源码:
bool full() { 
        return next == end();
    }

end()源码,AutoreleasePoolPage的大小为4096,即其SIZE(AutoreleasePoolPage的一个变量)的大小,* end()指针指向AutoreleasePoolPage的最高地址:

id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

AutoreleasePoolPage类中有一个next指针,在full()函数中,如果next指针和end()指针指向的地址是一样的,那就证明该page已满了。可是next指针到底是做什么的呢?(指向新加入的autorelease对象。)下面会有介绍,稍安勿躁。

  • 源代码向下执行,又遇到一个do-while循环结构。循环成立的先决条件是page已满,在do循环体中,如果page->child(当前page后面的AutoreleasePoolPage)存在,进行page的赋值语句。如果双链表中的page都已占满,那么会创建一个新的page:page = new AutoreleasePoolPage(page);
  • setHotPage(page);将page设置为hotpage。我们可以从源码中看到page和key成对存在的,源码如下:
static inline void setHotPage(AutoreleasePoolPage *page) 
    {
        if (page) page->fastcheck();
        tls_set_direct(key, (void *)page);//page和key成对存在的
    }
  • return page->add(obj);我们一起来看下add()函数到底做了什么操作:
id *add(id obj)
    {
        assert(!full());//@1
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing //@2
        *next++ = obj;////@3
        protect();
        return ret;
    }

@1步骤assert(!full());依然是一个断言函数,确保page还有存储空间。
@2和@3步骤id *ret = next; *next++ = obj;page的next指针指向新加入的元素,也就是将释放对象压入栈顶。疑问:此处有一个疑问就是obj由上下文可知是一个哨兵对象(nil),其他的释放对象应该也是调用此方法。所以猜测add方法其实是先向page中添加POOL_BOUNDARY,再加入释放的对象。

2> autoreleaseNoPage源码:
id *autoreleaseNoPage(id obj)
    {
        assert(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            return setEmptyPoolPlaceholder();
        }
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        return page->add(obj);
    }

autoreleaseNoPage意味着没有AutoreleasePoolPage或者说一个新的AutoreleasePoolPage已经创建了,但是还没有内容。此方法主要创建一个新的AutoreleasePoolPage将我们需要释放的对象加入到AutoreleasePoolPage中。

  • 序号2(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);
        }
    }

此方法是在page存在的情况下,autoreleaseFast源码中取出当前的hotPage()又加了一层判断,page存在并且page没有占满的,向page中添加释放对象。page存在并且page已占满,调用autoreleaseFullPage函数,page为空执行autoreleaseNoPage函数。两个函数都已解析过,不再重复。

push方法主要做了以下操作:
1、没有page,创建一个新的page,将释放对象加入到page中。
2、page存在但是page已占满,遍历整个链表中的page,直到找到一个没有占满的,将释放对象加入到page中。
3、page存在且有足够的存储空间,将释放对象加入到page中。

objc_autoreleasePoolPop:
static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            
            checkToken();
            
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        
        checkStop(stop, page, token);

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        killPage(page);
    }

为了方便解析每个方法的作用,我把pop方法中的某些处理分开写了(方法名自己命名的,代码没改)。像上面中提及的checkToken();以及checkStop(stop, page, token);做的是些校验处理,不再做深入分析。pop方法主要做了三件事:

1、调用pageForPointer函数查找到token所在的page。查找原理,翻看了一下源码,个人认为主要用到地址的偏移量来获取page。(intptr_t在不同的平台是不一样的,始终与地址位数相同,因此用来存放地址,即地址。)有兴趣的小伙伴可以自行查阅。
2、stop作为releaseUntil方法的参数,即释放加入到page中的对象,直到stop为止。
3、最终调用page->kill方法,本质是page->child = nil;将page->child置空。

  • 1> pageForPointer()源码:
static AutoreleasePoolPage *pageForPointer(const void *p) 
    {
        return pageForPointer((uintptr_t)p);
    }
static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
    {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;

        assert(offset >= sizeof(AutoreleasePoolPage));

        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }
  • (1).uintptr_t offset = p % SIZE;p是哨兵对象的地址位数,对SIZE做取余操作,笔者理解为获取偏移量。
  • (2).assert(offset >= sizeof(AutoreleasePoolPage)); 断言函数,偏移量一定不能比page的SIZE大。
  • (3).result = (AutoreleasePoolPage *)(p - offset);根据p地址减去偏移量或者page的地址。
  • 2> releaseUntil()源码:
void releaseUntil(id *stop)
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

该函数中主要是一个while循环,从栈顶一直释放对象,直到stop(哨兵对象的地址)。不过这里有一个很有意思的注释:fixme I think this `while` can be `if`, but I can't prove it.

  • 3> kill()源码:
void kill() 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;
        } while (deathptr != this);
    }
  • while (page->child) page = page->child;是为了找到双链表中的最后一个page。
  • 第二个do-while循环是从最后一个page开始置空,直到当前的page(this)。

源码阅读心得:
1、AutoreleasePool的数据结构:AutoreleasePool为什么不使用单链表呢。单链表虽然存储空间小于双链表,但是它无法进行逆向遍历。在kill()源码中我们可以看到在do-while循环中,有这样两段程序page = page->parent; page->child = nil;如果使用单链表的话,必然要产生一个临时变量,所以使用双链表更好。(个人观点)
2、苹果就是爸爸,源码很严谨,封装和命名堪称完美。方法和变量的注释适量。

四、NSAutoreleasePoolRunLoop:

关于这个问题,由于笔者能力有限,之前一些大神做了一些很好的总结:
带着问题看源码----子线程AutoRelease对象何时释放
深入理解RunLoop
iOS多线程——RunLoop与GCD、AutoreleasePool
NSRunloop,runloop,autoReleasePool和thread的关系理解及案例解决

写在最后:非常感谢大家的阅读,与此同时也非常感谢之前的一些作者的总结。鉴于笔者能力有限,理解错误、笔误之处还请大家多多指出,AutoreleasePool方面还有一些问题没有理解到位,路漫漫其修远兮,因为热爱所以永远在路上,路永远在脚下,后续还需继续更新,最后再次感谢大家的阅读。

借鉴的文章出处:
NSTimer,NSRunLoop,autoreleasepool,多线程的爱恨情仇
iOS基础 - autorelease 和 autoreleasepool
探秘Runtime - Runtime源码分析
Xcode 10 下如何创建可调试的objc4-723、objc4-750.1工程
Objective-C runtime机制(5)——iOS 内存管理
iOS - 自动释放池与@autoreleasepool
AutoreleasePool底层实现原理
关于自动释放池@autoreleasepool你需要知道知识
iOS开发笔记(七):深入理解Autorelease
autorelease 自动释放池的释放时机
自动释放池的前世今生 ---- 深入解析 Autoreleasepool
Using Autorelease Pool Blocks
黑幕背后的Autorelease
Autorelease

你可能感兴趣的:(iOS--AutoreleasePool的解析)