美文网首页
iOS 内存管理底层分析(二)- AutoreleasePool

iOS 内存管理底层分析(二)- AutoreleasePool

作者: 顶级蜗牛 | 来源:发表于2023-02-10 15:17 被阅读0次

相关文献:
iOS 内存管理底层分析(一)- 内存相关
iOS 内存管理底层分析(二)- AutoreleasePool底层

本文掌握知识点:
1.AutoreleasePool是什么
2.AutoreleasePool与Runloop的关系
3.AutoreleasePool的底层原理
4.探究一个AutoreleasePoolPage能存多少个对象
5.什么对象能被添加到AutoreleasePool
6.__autoreleasing/autorelease的源码分析

一、AutoreleasePool 自动释放池

自动释放池是Objective-C/Swift中的一种内存自动回收机制,AutoreleasePool可以将其中的变量进行release的时机延迟简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即release。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到runloop休眠/超出@autoreleasepool作用域{}之后才会被释放。

1.AutoreleasePool与Runloop的关系
AutoreleasePool机制图
1.1 AutoreleasePool创建
  • App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用
    _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
1.2 AutoreleasePool释放
  • 第二个Observer监视了两个事件: BeforeWaiting(准备进入休眠)
    时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用 _objc_autoreleasePoolPop()来释放自动释放池。这个Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的AutoreleasePool环绕着,所以不会出现内存泄漏。

官方文档
每一个线程(包括主线程)维护自己的NSAutoreleasePool对象,在创建新池时,它们会被添加到堆栈的顶部。当池被释放时,它们将从堆栈中移除。自动释放的对象被放置到当前线程的顶部自动释放池中。当线程终止时,它会自动耗尽与自己相关的所有自动释放池。

2.AutoreleasePool的本质

通过分析main.m文件里面的@autoreleasepool{}来分析其本质是什么。
通过clang将main.m编译成main.cpp

// clang指令
$ clang -rewrite-objc main.m

程序编译后会将@autoreleasepool编译成__AtAutoreleasePool类型的变量。
在main.cpp里找到这个__AtAutoreleasePool的声明:

struct __AtAutoreleasePool {
    // 构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    // 析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

__AtAutoreleasePool是一个结构体,有构造函数 + 析构函数,结构体定义的对象在作用域结束后,会自动调用析构函数。

于是就可以把main函数转换成这样:

int main(int argc, char * argv[]) {
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush(); // 构造函数
        // 程序员所编写要执行的代码
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_48_22gnp4yj39sb798xltkcy_140000gn_T_main_1850b4_mi_0); 
        objc_autoreleasePoolPop(atautoreleasepoolobj); // 析构函数
    }
    return 0;
}

AutoreleasePool的本质就是一个结构体。

我们要搞清楚它的底层做了哪些事,就需要探索objc_autoreleasePoolPushobjc_autoreleasePoolPop做了哪些操作。

二、AutoreleasePool的底层原理

我们知道@autoreleasepool{}最终编译后的代码是objc_autoreleasePoolPushobjc_autoreleasePoolPop

打开objc4-838.1源码去搜索这两个函数的实现:

@autoreleasepool{}

源码中是通过AutoreleasePoolPage匿名空间来调用push/pop。
接下来只需要分析AutoreleasePoolPage即可。

来看看苹果对于AutoreleasePoolPage的介绍,它是自动释放池的实现:

1.AutoreleasePoolPage的数据结构
//************宏定义************
#define PAGE_MIN_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096            /* bytes per 80386 page */

//************类定义************
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

public:
    //页的大小
    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

private:
    
    ...
    
    //构造函数
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//开始存储的位置
                                objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
                                newParent ? newParent->hiwat : 0)
    {...}
    
    //析构函数
    ~AutoreleasePoolPage() {...}
    
    ...
    
    //页的开始位置
    id * begin() {...}
    
    //页的结束位置
    id * end() {...}
   
    //页是否为空
    bool empty() {...}
    
    //页是否满了
    bool full() {...}
   
    //页的存储是否少于一半
    bool lessThanHalfFull() {...}
     
     //添加释放对象
    id *add(id obj){...}
    
    //释放所有对象
    void releaseAll() {...}
    
    //释放到stop位置之前的所有对象
    void releaseUntil(id *stop) {...}
    
    //杀掉
    void kill() {...}
    
    //释放本地线程存储空间
    static void tls_dealloc(void *p) {...}
    
    //获取AutoreleasePoolPage
    static AutoreleasePoolPage *pageForPointer(const void *p) {...}
    static AutoreleasePoolPage *pageForPointer(uintptr_t p)  {...}
    
    //是否有空池占位符
    static inline bool haveEmptyPoolPlaceholder() {...}
    
    //设置空池占位符
    static inline id* setEmptyPoolPlaceholder(){...}
    
    //获取当前操作页
    static inline AutoreleasePoolPage *hotPage(){...}
    
    //设置当前操作页
    static inline void setHotPage(AutoreleasePoolPage *page) {...}
    
    //获取coldPage
    static inline AutoreleasePoolPage *coldPage() {...}
    
    //快速释放
    static inline id *autoreleaseFast(id obj){...}
   
   //添加自动释放对象,当页满的时候调用这个方法
    static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {...}
    
    //添加自动释放对象,当没页的时候使用这个方法
    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj){...}
   
   //创建新页
    static __attribute__((noinline))
    id *autoreleaseNewPage(id obj) {...}
    
public:
    //自动释放
    static inline id autorelease(id obj){...}
   
    //入栈
    static inline void *push() {...}
    
    //兼容老的 SDK 出栈方法
    __attribute__((noinline, cold))
    static void badPop(void *token){...}
    
    //出栈页面
    template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop){...}
    __attribute__((noinline, cold))
    static void
    popPageDebug(void *token, AutoreleasePoolPage *page, id *stop){...}
    
    //出栈
    static inline void
    pop(void *token){...}
    
    static void init(){...}
    
    //打印
    __attribute__((noinline, cold))
    void print(){...}
    
    //打印所有
    __attribute__((noinline, cold))
    static void printAll(){...}
    
    //打印Hiwat
    __attribute__((noinline, cold))
    static void printHiwat(){...}

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; // 校验AutoreleasePoolPage结构是否完整
    __unsafe_unretained id *next; // 指向最新添加的进池的对象的下一个位置,初始时指向begin()
    pthread_t const thread; // 指向当前线程
    AutoreleasePoolPage * const parent; // 指向父节点,第一个节点的parent值为nil
    AutoreleasePoolPage *child; // 指向子节点,最后一个节点的child值为nil
    uint32_t const depth; // 代表深度,从0开始,往后递增1
    uint32_t hiwat; // 最大入栈数量标记
    
    // 初始化
    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)
    {
    }
};

AutoreleasePoolPage就有这些属性:

magic_t const magic; // 校验AutoreleasePoolPage结构是否完整
__unsafe_unretained id *next; // 指向最新添加的进池的对象的下一个位置,初始时指向begin()
pthread_t const thread; // 指向当前线程
AutoreleasePoolPage * const parent; // 指向父节点,第一个节点的parent值为nil
AutoreleasePoolPage *child; // 指向子节点,最后一个节点的child值为nil
uint32_t const depth; // 代表深度,从0开始,往后递增1
uint32_t hiwat; // 最大入栈数量标记

其中AutoreleasePoolPageData结构体的内存大小为56字节
magic的类型是magic_t结构体,所占内存大小为m[4];所占内存(4*4=16字节);
next(指针)thread(对象)parent(对象)child(对象)均占8字节(4*8=32字节);
depthhiwat类型为uint32_t,实际类型是unsigned int类型,均占4字节(2*4=8字节)。

自动释放池是以栈为节点,通过双向链表的形式连接的结构。

2.objc_autoreleasePoolPush 源码分析
AutoreleasePoolPage::push()

push是判断 是否有自动释放池(第一次来肯定是没有的):

  • 如果没有,则通过autoreleaseNewPage创建;
  • 如果有,则通过autoreleaseFast压栈哨兵对象
2.1 autoreleaseNewPage 第一次创建Page
//创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
    //获取当前操作页
    AutoreleasePoolPage *page = hotPage();
    //如果存在,则压栈对象
    if (page) return autoreleaseFullPage(obj, page);
    //如果不存在,则创建页
    else return autoreleaseNoPage(obj);
}

//******** hotPage方法 ********
//获取当前操作页
static inline AutoreleasePoolPage *hotPage() 
{
    //获取当前页
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    //如果是一个空池,则返回nil,否则,返回当前线程的自动释放池
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}


//******** autoreleaseNoPage方法 ********
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // "No page" could mean no pool has been pushed
    // or an empty placeholder pool has been pushed and has no contents yet
    ASSERT(!hotPage());

    bool pushExtraBoundary = false;
    //判断是否是空占位符,如果是,则压栈哨兵标识符置为YES
    if (haveEmptyPoolPlaceholder()) {
        // We are pushing a second pool over the empty placeholder pool
        // or pushing the first object into the empty placeholder pool.
        // Before doing that, push a pool boundary on behalf of the pool 
        // that is currently represented by the empty placeholder.
        pushExtraBoundary = true;
    }
    //如果对象不是哨兵对象,且没有Pool,则报错
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     objc_thread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    //如果对象是哨兵对象,且没有申请自动释放池内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {//如果传入参数为哨兵
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // Install and return the empty pool placeholder.
        return setEmptyPoolPlaceholder();//设置空的占位符
    }

    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    //初始化第一页。这里传递nil,意思是第一页的parent指向为nil。
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    //设置page为当前聚焦页
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    //压栈哨兵的标识符为YES,则压栈哨兵对象
    if (pushExtraBoundary) {
        //压栈哨兵
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    //压栈对象
    return page->add(obj);
}

a.autoreleaseNewPage主要是通过hotPagetls线程缓存里获取当前页,判断当前页是否存在:

  • 如果存在,则通过autoreleaseFullPage方法压栈对象;
  • 如果不存在,则通过autoreleaseNoPage方法创建页。

b.autoreleaseNoPage中发现当前线程的自动释放池是通过AutoreleasePoolPage创建的,它的定义中有构造方法,而构造方法的实现是通过父类AutoreleasePoolPageData的初始化方法(上面代码实例就有);被创建出来的第一页page中的第一个被添加进去一定是哨兵对象POOL_BOUNDARY

AutoreleasePoolPage的构造方法:

//**********AutoreleasePoolPage构造方法**********
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//开始存储的位置
                                objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
                                newParent ? newParent->hiwat : 0)
{ 
    if (parent) {
        parent->check();
        ASSERT(!parent->child);
        parent->unprotect();
        //this 表示 新建页面,将当前页面的子节点 赋值为新建页面
        parent->child = this;
        parent->protect();
    }
    protect();
}

//**********AutoreleasePoolPageData初始化方法**********
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)
    {
    }
  • begin()表示压栈的位置(即下一个要释放对象的压栈地址),其中的56就是结构体AutoreleasePoolPageData的内存大小;
//********begin()********
//页的开始位置
id * begin() {
    //等于 首地址+56(AutoreleasePoolPage类所占内存大小)
    return (id *) ((uint8_t *)this+sizeof(*this));
}
  • objc_thread_self()表示的是当前线程,而当前线程时通过tls获取的;
__attribute__((const))
static inline pthread_t objc_thread_self()
{
    //通过tls获取当前线程
    return (pthread_t)tls_get_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
}
  • newParent表示父节点;
  • 后续两个参数是通过父节点的深度、最大入栈个数计算depth以及hiwat

所以 AutoreleasePoolPage本身的属性占用56个字节,而哨兵对象POOL_BOUNDARY (8字节)是第一个被添加的对象,所以之后被添加进池的对象就要从64字节后开始,并且第一页的parent指向nil,每一次添加depth深度+1。

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

autoreleaseFast主要是通过hotPagetls线程缓存里获取当前页,判断当前页是否存在:

  • 如果当前页存在且未满,则通过add函数压栈对象;
  • 如果当前页存在且满了,则通过autoreleaseFullPage函数安排新的页面;
  • 如果当前页不存在,则通过autoreleaseNoPage方法创建新页(上文分析过)。

a.autoreleaseFullPage

//添加自动释放对象,当页满的时候调用这个方法
static __attribute__((noinline))
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-while遍历循环查找界面是否满了
    do {
        //如果子页面存在,则将页面替换为子页面
        if (page->child) page = page->child;
        //如果子页面不存在,则新建页面
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    //设置为当前操作页面
    setHotPage(page);
    //对象压栈
    return page->add(obj);
}

通过构造方法将新建页面的parent指向当前页,通过当前页操作child对象,将当前页child指向新建页面,将对象压栈。

3.objc_autoreleasePoolPop 源码分
_objc_autoreleasePoolPop

注意这个参数void *ctxt是通过_objc_autoreleasePoolPush返回的,参数其实就是哨兵对象POOL_BOUNDARY的内存地址

objc_autoreleasePoolPop AutoreleasePoolPage:pop()

拿到哨兵所在的page,验证哨兵是否正确,为出栈做准备。

AutoreleasePoolPage:popPage()

调用pop操作时,会传入一个值,这个值就是push操作的返回值,即哨兵POOL_BOUNDARY的内存地址token。所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release 释放 哨兵之前的对象,并把next指针到正确位置。

    // 释放对象
    void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        // 不是递归的:如果线程积累了大量的垃圾,我们不希望耗尽堆栈
        
        // 1.从最后一页开始往第一页 出栈释放(为每一个对象调用objc_release),一直释放到哨兵对象
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            // 每次从hotPage()重新启动,以防-release自动释放更多对象
            AutoreleasePoolPage *page = hotPage(); // 获取当前的热page

            // fixme I think this `while` can be `if`, but I can't prove it
            // 如果当前的热page的对象空了,则往父节点去找哨兵
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
            AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

            // create an obj with the zeroed out top byte and release that
            // 创建一个顶字节归零的obj并释放它
            id obj = (id)entry->ptr;
            int count = (int)entry->count;  // grab these before memset
#else
            id obj = *--page->next;
#endif
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                // release count+1 times since it is count of the additional
                // autoreleases beyond the first one
                // 发行次数+1次,因为它是第一次自动发行之后的额外自动发行次数
                for (int i = 0; i < count + 1; i++) {
                    objc_release(obj);
                }
#else
                objc_release(obj);
#endif
            }
        }
        
        // 2.把第一页设置为热page
        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }
  
  // 删除子节点
  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);
    }

总结AutoreleasePool底层原理:在文章最底部的面试题1。

三、探究一个AutoreleasePoolPage能存多少个对象

1.AutoreleasePoolPage有多大?
#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)
1 << 12 = 4096B = 4KB

一页Page大小是4KB

2.一页Page能存多少个对象

上文说过AutoreleasePoolPage得属性是来源于继承自AutoreleasePoolPageData,其结构体的内存大小为56字节

4096 - 56 = 4040

而第一页的Page入栈了第一个对象是哨兵对象,其大小为8字节

4040 - 8 = 4032

结论:

  • 第一页可以存放504个对象,且只有第一页有哨兵,当一页压栈满了,就会开辟新的一页;
  • 第二页开始,最多可以存放505个对象。

验证代码:

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for(int i = 0; i < 505; i++) {  // 第一页只有504个对象的空间可以存
            __autoreleasing LGPerson *p = [LGPerson new];
        }
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

超过了第一页的数据,则会新建一页

四、什么对象能被添加到AutoreleasePool

1.手动添加到自动释放池
@autoreleasepool {
    // insert code here...
    Person *p = [Person new];
}

这样写会p对象会被添加到自动释放池里吗?
不会!因为它是被ARC管理内存的。

注意:当我们通过alloc、new、copy、mutableCopy、allocWith...去创建的对象是不会被添加到autoreleasepool的。

在ARC环境下,如果想把p对象添加到autoreleasepool,应该添加上__autoreleasing修饰,此时就不归ARC管了:

@autoreleasepool {
    // insert code here...
    __autoreleasing Person *p = [Person new];
}

验证:

extern void _objc_autoreleasePoolPrint(void); // 系统提供,用来打印当前释放池中的对象
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __autoreleasing Person *p = [Person new];
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

在MRC环境下,可以通过调用autorelease方法,把p对象添加到autoreleasepool:

Person *p = [[Person new] autorelease];

通过allocnewcopymutableCopyallocWith...去创建的对象去用__autoreleasing修饰/调用autorelease的,会被添加到autoreleasePool

2.自动添加到自动释放池

上一篇文章分析过TaggedPoint类型相关的:
字符串通过[NSString stringWithFormat:]去创建的,参数长度小于等于9且非中文的是TaggedPoint类型(相当于是一个值类型的对象);
否则是__NSCFString,此时它是自动被添加到自动释放池的。

NSString *str = [NSString stringWithFormat: @"1234567890"]; // 长度>9 或 中文

[NSString stringWithFormat:]创建出来的__NSCFString类型的对象,是自动被添加到自动释放池的。

五、__autoreleasing/autorelease的源码分析

__autoreleasing其实就是调用autorelease方法的,在汇编环境下断点能看到。
打开objc4-838.1源码,去搜索autorelease

-(id) autorelease
{
    return _objc_rootAutorelease(self);
}

NEVER_INLINE id
_objc_rootAutorelease(id obj)
{
    ASSERT(obj);
    return obj->rootAutorelease();
}
objc_object::autorelease() objc_object::rootAutorelease() objc_object::rootAutorelease2() autorelease

__autoreleasing/autorelease最终又会回到 autoreleaseFast压栈对象 的分析。所以通过这种方式,就会把对象入栈到自动释放池中。

六、面试题

面试题1:AutoreleasePool的原理

  • 自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接

  • 自动释放池的压栈出栈主要是通过结构体的构造函数和析构函数调用底层的objc_autoreleasePoolPushobjc_autoreleasePoolPop,实际上是调用AutoreleasePoolPagepushpop两个方法。

  • 调用push操作其实就是创建一个新的AutoreleasePoolPage,而AutoreleasePoolPage的具体操作就是插入一个哨兵POOL_BOUNDARY,并返回插入哨兵POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况:

    • a.当page存在,且不满时,调用add方法将对象添加至pagenext指针处,并将next指向下一位;
    • b.当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中;
    • c.当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中。
  • 调用pop操作时,会传入一个值,这个值就是push操作的返回值,即哨兵POOL_BOUNDARY内存地址token。所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release 释放 token之前的对象,并把next 指针到正确位置。

面试题2:哨兵在一个自动释放池有几个?

  • 哨兵只有一个,且在第一页。(第一页最多可以存504个对象,第二页开始最多存 505个。)

面试题3:临时变量什么时候释放?

  • 如果在正常情况下,一般是超出其作用域就会立即释放;
  • 如果将临时变量加入了自动释放池,会延迟释放,即在runloop休眠或者autoreleasepool作用域之后释放。

面试题4:在ARC环境下,AutoreleasePool的释放时机是什么时候?

  • a.手动添加到自动释放池的对象(__autoreleasing),在出@autoreleasePool{}作用域的时候;
  • b.自动添加到自动释放池
    • NSString *str = [NSString stringWithFormat: @"1234567890"] ; 在出@autoreleasePool{}作用域的时候;
    • 主线程的runloopBeforeWaitingExit时刻需要释放AutoreleasePool里的对象;
    • 子线程没有开启runloop,在子线程被销毁的时候AutoreleasePool被释放;
    • 子线程开启runloop,在BeforeWaitingExit时刻需要释放AutoreleasePool里的对象。

面试题5:AutoreleasePool能否嵌套使用?

  • 可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高;
  • 可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的;
  • 自动释放池的多层嵌套其实就是不停的push哨兵对象,在pop时,会先释放里面的,在释放外面的

面试题6:哪些对象可以加入AutoreleasePool?alloc创建的对象可以吗?

  • 使用allocnewcopymutableCopyallocWith...生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中;
    但是被__autoreleasing修饰/autorelease调用则会被添加到自动释放池中;
  • 所有 autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。

面试题7:thread 和 AutoreleasePool的关系
官方文档中,找到如下说明:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池。

面试题8:RunLoop 和 AutoreleasePool的关系
官方文档中,找到如下说明:

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool;并且会在事件循环结束时,执行drain操作,释放其中的对象。

面试题9:优化下面代码

for(int i = 0; i<100000000; i++) {
  NSLog(@"%d", i);
  LGPerson *p = [LGPerson new];
}

缺陷:会出现内存峰值。猜测是因为释放内存的时间要大于创建的时间,所以造成内存峰值。

优化后:

for(int i = 0; i<100000000; i++) {
  @autoreleasepool {
      NSLog(@"%d", i);
      __autoreleasing LGPerson *p = [LGPerson new];
    }
}

比如一次滑动的时间内,runloop已经转了成百上千次了。说明runloop转得非常迅速的。
所以每一次在rooloop即将休眠的时候,都会把自动释放池里的对象释放了。

相关文章

网友评论

      本文标题:iOS 内存管理底层分析(二)- AutoreleasePool

      本文链接:https://www.haomeiwen.com/subject/shhfkdtx.html