假期学习笔记总结--iOS 自动释放池

devtools/2024/9/23 16:36:46/

iOS 自动释放池

https://juejin.cn/post/6844904094503567368#heading-23

ARC和MRC

苹果在 iOS 5 中引入了ARC(Automatic Reference Counting)自动引用计数内存管理技术,通过LLVM编译器和Runtime协作来进行自动管理内存。LLVM编译器会在编译时在合适的地方为 OC 对象插入retainreleaseautorelease代码,省去了在MRC(Manual Reference Counting)手动引用计数下手动插入这些代码的工作,减轻了开发者的工作量。

MRC下,当我们不需要一个对象的时候,要调用releaseautorelease方法来释放它。调用release会立即让对象的引用计数减 1 ,如果此时对象的引用计数为 0,对象就会被销毁。调用autorelease会将该对象添加进自动释放池中,它会在一个恰当的时刻自动给对象调用release,所以autorelease相当于延迟了对象的释放。

ARC下,autorelease方法已被禁用,我们可以使用__autoreleasing修饰符修饰对象将对象注册到自动释放池中。

自动释放池

AppKit 和 UIKit 框架在事件循环(RunLoop)的**每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease对象。**通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。

如何创建一个自动释放池

在MRC下,可以使用NSAutoreleasepool或@autoreleasePool,一般用autoreleasePool,比NSAutoreleasepool快 ;

在ARC下,禁止使用NSAutoreleasePool类创建线程池,只能使用@autoreleasepool

平时创建一个main函数代码时,会发现其中有一个这个东西@autoreleasepool{},使用clang编译之后:@autoreleasepool{...}被编译成了{__AtAutoreleasePool __autoreleasepool; ... }

这个__AtAutoreleasePool到底是什么?

它其实是一个结构体,在创建__AtAutoreleasePool结构体变量的时候调用了objc_autoreleasePoolPush(void),销毁的时候会调动objc_autoreleasePoolPop(void *),其实构造函数和析构函数,所以我们可以看出其其实是一个C++封装的自动释放池变量,会将@autoreleasepool{…}中{}中的内容添加到自动释放池中,方便内存管理。

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

从上边的__AtAutoreleasePool我们可以看到这两个方法objc_autoreleasePoolPushobjc_autoreleasePoolPop

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

我们可以看出这里又引入了新的类AutoreleasePoolPage

class AutoreleasePoolPage {magic_t const magic;//AutoreleasePoolPage 完整性校验id *next;//存放下一个autorelease对象的地址pthread_t const thread; //AutoreleasePoolPage 所在的线程AutoreleasePoolPage * const parent;//父节点AutoreleasePoolPage *child;//子节点uint32_t const depth;//深度,也可以理解为当前page在链表中的位置uint32_t hiwat;
}

自动释放池本质上是一个由AutoreleasePoolPage构成的双向链表,parent和child表示前趋和后继;

其中有 56 bit 用于存储AutoreleasePoolPage的成员变量,剩下的0x100816038 ~ 0x100817000都是用来存储加入到自动释放池中的对象。*(这里存储autorelease对象的是由 id next; 构成的栈存储的)

每一个AutoreleasePoolPage的大小都是4096字节(16 进制 0x1000)。

图片.png

  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY (感觉不太准确,建议看下面的)
  • id *next指向了下一个能存放autorelease对象地址的区域

Runloop和Autorelease

App启动时会创建两个观察者监听主线程runloop,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。用于创建和释放自动释放池 ;

AutoreleasePool创建

  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用
    _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

AutoreleasePool释放

  • 第二个Observer监视了两个事件: BeforeWaiting(准备进入休眠)
    时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用 _objc_autoreleasePoolPop()来释放自动释放池。这个Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

(这里解释一下order,在 Cocoa 中,RunLoop Observers 是一种机制,用于监听 RunLoop 的运行状态并在特定时机执行回调。每个 Observer 都有一个 order 属性,用于指定观察者的优先级,决定了观察者的回调执行顺序。order 属性是一个整数类型的值,它决定了观察者的执行顺序。数值越小的观察者,其优先级越高,会先于数值较大的观察者执行。)

  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的AutoreleasePool环绕着,所以不会出现内存泄漏。

一些面试的点

  • 自动释放池的本质是AutoreleasePoolPage创建的双向链表,AutoreleasePoolPage是一个结构体,是一个栈存储的页 ;

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

  • AutoreleasePoolPage 对象并不是每次调用 push 操作都会创建一个新的。AutoreleasePoolPage 对象是用来管理自动释放对象的,而 push 操作通常会在当前 AutoreleasePoolPage 的栈顶添加一个标记对象,比如 POOL_BOUNDARY,而不是每次都创建新的 AutoreleasePoolPage 对象。

  • 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指针到正确位置。

原理

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

通过 Clang clang -rewrite-objc main.m 将以上代码转换为 C++ 代码。

struct __AtAutoreleasePool {__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}void * atautoreleasepoolobj;
};int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;  }return 0;
}
  • @autoreleasepool底层是创建了一个__AtAutoreleasePool结构体对象;
  • 在创建__AtAutoreleasePool结构体时会在构造函数中调用objc_autoreleasePoolPush()函数,并返回一个atautoreleasepoolobj(POOL_BOUNDARY存放的内存地址,下面会讲到);
  • 在释放__AtAutoreleasePool结构体时会在析构函数中调用objc_autoreleasePoolPop()函数,并将atautoreleasepoolobj传入。

两个函数的实现:

// NSObject.mm
void * objc_autoreleasePoolPush(void)
{return AutoreleasePoolPage::push();
}void objc_autoreleasePoolPop(void *ctxt)
{AutoreleasePoolPage::pop(ctxt);
}

@autoreleasepool底层就是使用AutoreleasePoolPage类来实现的。

AutoreleasePoolPage类

class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  // EMPTY_POOL_PLACEHOLDER:表示一个空自动释放池的占位符
#   define POOL_BOUNDARY nil                // POOL_BOUNDARY:哨兵对象static pthread_key_t const key = AUTORELEASE_POOL_KEY;static uint8_t const SCRIBBLE = 0xA3;   // 用来标记已释放的对象static size_t const SIZE =              // 每个 Page 对象占用 4096 个字节内存
#if PROTECT_AUTORELEASEPOOL                 // PAGE_MAX_SIZE = 4096PAGE_MAX_SIZE;  // must be muliple of vm page size
#elsePAGE_MAX_SIZE;  // size and alignment, power of 2
#endifstatic size_t const COUNT = SIZE / sizeof(id);  // Page 的个数magic_t const magic;                // 用来校验 Page 的结构是否完整id *next;                           // 指向下一个可存放 autorelease 对象地址的位置,初始化指向 begin()pthread_t const thread;             // 指向当前线程AutoreleasePoolPage * const parent; // 指向父结点,首结点的 parent 为 nilAutoreleasePoolPage *child;         // 指向子结点,尾结点的 child  为 niluint32_t const depth;               // Page 的深度,从 0 开始递增uint32_t hiwat;......
}
  • 自动释放池与线程一一对应;自动释放池(即所有的AutoreleasePoolPage对象)是以``AutoreleasePoolPage为结点通过双向链表`的形式组合而成;
  • 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。

push (这里是自动释放池的push哦,别理解成autorelease对象的push)

    static inline void *push() {id *dest;if (DebugPoolAllocation) { // 出错时进入调试状态// Each autorelease pool starts on a new pool page.dest = autoreleaseNewPage(POOL_BOUNDARY);} else {dest = autoreleaseFast(POOL_BOUNDARY);  // 传入 POOL_BOUNDARY 哨兵对象}assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);return dest;}

当创建一个自动释放池时,会调用push()方法。push()方法中调用了autoreleaseFast()方法并传入了POOL_BOUNDARY哨兵对象。

先解释一下这个POOL_BOUNDARY哨兵对象:

  • POOL_BOUNDARY的前世叫做POOL_SENTINEL,称为哨兵对象或者边界对象;

  • POOL_BOUNDARY用来区分不同的自动释放池,以解决自动释放池嵌套的问题;

  • 每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈,并返回其存放的内存地址;(注意,将一个POOL_BOUNDARY入栈是作为自动释放池的边界,也就是说一个POOL_BOUNDARY对应一个自动释放池)

  • 当往自动释放池中添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY

  • 当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY

总的来说:POOL_BOUNDARY称为哨兵对象或者边界对象,作为每一个自动释放池的边界,方便了自动释放池的销毁 ;

而且关于自动释放池的内存比较模糊,没找到比较好的说法,我个人总结一下:同一个线程中的自动释放池会存在于线程栈顶的同一个双向链表中,它们通过哨兵对象区分开来,不同线程间这个双向链表也是不一样的(不一定准确)

下面我们来看一下autoreleaseFast()方法的实现:(要注意,这个方法的实现是将一个对象加入AutoreleasePoolPage为节点的双向链表中的某个节点的next中去,而这里的参数是POOL_BOUNDARY,他可以区分不同的自动释放池;这也佐证了我上面说的)

    static inline id *autoreleaseFast(id obj){AutoreleasePoolPage *page = hotPage();     // 双向链表中的最后一个 Pageif (page && !page->full()) {        // 如果当前 Page 存在且未满return page->add(obj);                 // 将 autorelease 对象入栈,即添加到当前 Page 中;} else if (page) {                  // 如果当前 Page 存在但已满return autoreleaseFullPage(obj, page); // 创建一个新的 Page,并将 autorelease 对象添加进去} else {                            // 如果当前 Page 不存在,即还没创建过 Pagereturn autoreleaseNoPage(obj);         // 创建第一个 Page,并将 autorelease 对象添加进去}}

autoreleaseFast()中先是调用了hotPage()方法获得未满的Page,从AutoreleasePoolPage类的定义可知,每个Page的内存大小为4096个字节,每当Page满了的时候,就会创建一个新的PagehotPage()方法就是用来获得这个新创建的未满的PageautoreleaseFast()在执行过程中有三种情况:

① 当前Page存在且未满时,通过page->add(obj)autorelease对象入栈,即添加到当前Page中;

    id *add(id obj){assert(!full());unprotect();id *ret = next;  // faster than `return next-1` because of aliasing*next++ = obj;protect();return ret;}

page->add(obj)其实就是将autorelease对象添加到Page中的next指针所指向的位置,并将next指针指向这个对象的下一个位置,然后将该对象的位置返回。

② 当前Page存在但已满时,通过autoreleaseFullPage(obj, page)创建一个新的Page,并将autorelease对象添加进去;

    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 {if (page->child) page = page->child;else page = new AutoreleasePoolPage(page);} while (page->full());setHotPage(page);return page->add(obj);}

autoreleaseFullPage()方法中通过while循环,通过Pagechild指针找到最后一个Page

  • 如果最后一个Page未满,就通过page->add(obj)autorelease对象添加到最后一个Page中;
  • 如果最后一个Page已满,就创建一个新的Page并将该Page设置为hotPage,通过page->add(obj)autorelease对象添加进去。

③ 当前Page不存在,即还没创建过Page,通过autoreleaseNoPage(obj)创建第一个Page,并将autorelease对象添加进去。

    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 yetassert(!hotPage());bool pushExtraBoundary = false;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;}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", pthread_self(), (void*)obj, object_getClassName(obj));objc_autoreleaseNoPool(obj);return nil;}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.AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);setHotPage(page);// Push a boundary on behalf of the previously-placeholder'd pool.if (pushExtraBoundary) {page->add(POOL_BOUNDARY);}// Push the requested object or pool.return page->add(obj);}

autoreleaseNoPage()方法中会创建第一个Page。该方法会判断是否有空的自动释放池存在,如果没有会通过setEmptyPoolPlaceholder()生成一个占位符,表示一个空的自动释放池。接着创建第一个Page,设置它为hotPage。最后将一个POOL_BOUNDARY添加进Page中,并返回POOL_BOUNDARY的下一个位置。

以上就是push操作的实现,往自动释放池中添加一个POOL_BOUNDARY,并返回它存放的内存地址。接着每有一个对象调用autorelease方法,会将它的内存地址添加进自动释放池中。

autorelease (这里就是autorelease对象的push了)

autorelease方法的函数调用栈如下:

// NSObject.mm
① objc_autorelease
// objc-object.h 
② objc_object::autorelease
// NSObject.mm
③ autorelease
④ _objc_rootAutorelease
// objc-object.h
⑤ objc_object::rootAutorelease
// NSObject.mm
⑥ objc_object::rootAutorelease2
⑦ AutoreleasePoolPage::autorelease

AutoreleasePoolPage类的autorelease方法实现如下:

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

可以看到,调用了autorelease方法的对象,也是通过以上解析的autoreleaseFast()方法添加进Page中。

pop (销毁一个自动释放池)

    static inline void pop(void *token) {AutoreleasePoolPage *page;id *stop;if (token == (void*)EMPTY_POOL_PLACEHOLDER) {// Popping the top-level placeholder pool.if (hotPage()) {// Pool was used. Pop its contents normally.// Pool pages remain allocated for re-use as usual.pop(coldPage()->begin());} else {// Pool was never used. Clear the placeholder.setHotPage(nil);}return;}page = pageForPointer(token);stop = (id *)token;if (*stop != POOL_BOUNDARY) {if (stop == page->begin()  &&  !page->parent) {// Start of coldest page may correctly not be POOL_BOUNDARY:// 1. top-level pool is popped, leaving the cold page in place// 2. an object is autoreleased with no pool} else {// Error. For bincompat purposes this is not // fatal in executables built with old SDKs.return badPop(token);}}if (PrintPoolHiwat) printHiwat();page->releaseUntil(stop);// memory: delete empty childrenif (DebugPoolAllocation  &&  page->empty()) {// special case: delete everything during page-per-pool debuggingAutoreleasePoolPage *parent = page->parent;page->kill();setHotPage(parent);} else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {// special case: delete everything for pop(top) // when debugging missing autorelease poolspage->kill();setHotPage(nil);} else if (page->child) {// hysteresis: keep one empty child if page is more than half fullif (page->lessThanHalfFull()) {page->child->kill();}else if (page->child->child) {page->child->child->kill();}}}

pop ()方法会传入一个POOL_BOUNDARY对应在Page中的地址,当销毁自动释放池时,会调用pop()方法将自动释放池中的release对象全部释放**(实际上是从自动释放池的中的最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY)**

pop()方法的执行过程如下:

① 判断token是不是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池;

② 如果不是的话,就通过pageForPointer(token)拿到token所在的Page(自动释放池的首个Page);

③ 通过page->releaseUntil(stop)将自动释放池中的autorelease对象全部释放,传参stop即为POOL_BOUNDARY的地址;

④ 判断当前Page是否有子Page,有的话就销毁。

pop()方法中释放autorelease对象的过程在releaseUntil()方法中,下面来看一下这个方法的实现:

    void releaseUntil(id *stop) {// Not recursive: we don't want to blow out the stack // if a thread accumulates a stupendous amount of garbagewhile (this->next != stop) {// Restart from hotPage() every time, in case -release // autoreleased more objectsAutoreleasePoolPage *page = hotPage();// fixme I think this `while` can be `if`, but I can't prove itwhile (page->empty()) {page = page->parent;setHotPage(page);}page->unprotect();id obj = *--page->next;  // next指针是指向最后一个对象的后一个位置,所以需要先减1memset((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 emptyfor (AutoreleasePoolPage *page = child; page; page = page->child) {assert(page->empty());}
#endif}

releaseUntil()方法其实就是通过一个while循环,从最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY

有关这里push和pop,以及release方法的小结

小结:

  • push操作是往自动释放池中添加一个POOL_BOUNDARY,并返回它存放的内存地址;
  • 接着每有一个对象调用autorelease方法,会将它的内存地址添加进自动释放池中。
  • pop操作是传入一个POOL_BOUNDARY的内存地址,从最后一个入栈的autorelease对象开始,将自动释放池中的autorelease对象全部释放(实际上是给它们发送一条release消息),直到遇到这个POOL_BOUNDARY

首先要明确,这里研究的push和pop指的是自动释放池的创建与销毁 ;但我们在理解它们的底层实现发现,它们都在对next指针处进行操作(也就是release对象所在的地方) ;

push会在next加入哨兵对象POOL_BOUNDARY,标志着一个自动释放池的创建;

而pop会从next往前release那些autorelease对象,知道遇到哨兵对象POOL_BOUNDARY,这也标志着一个自动释放池的销毁 ;

这时再去思考自动释放池的本质和自动释放池的内存模式,我觉得会有新的理解 ;

AutoreleasePoolPage()

我们来看一下创建一个Page的过程。AutoreleasePoolPage()方法的参数为parentPage,新创建的Pagedepth加一,next指针的初始位置指向begin,将新创建的Pageparent指针指向parentPage。将parentPagechild指针指向自己,这就形成了双向链表的结构。

    AutoreleasePoolPage(AutoreleasePoolPage *newParent) : magic(), next(begin()), thread(pthread_self()),parent(newParent), child(nil), depth(parent ? 1+parent->depth : 0), hiwat(parent ? parent->hiwat : 0){ if (parent) {parent->check();assert(!parent->child);parent->unprotect();parent->child = this;parent->protect();}protect();}

begin、end、empty、full

下面再来看一下beginendemptyfull这些方法的实现。

  • begin的地址为:Page自己的地址+Page对象的大小56个字节;
  • end的地址为:Page自己的地址+4096个字节;
  • empty判断Page是否为空的条件是next地址是不是等于begin
  • full判断Page是否已满的条件是next地址是不是等于end(栈顶)。
    id * begin() {return (id *) ((uint8_t *)this+sizeof(*this));}id * end() {return (id *) ((uint8_t *)this+SIZE);}bool empty() {return next == begin();}bool full() { return next == end();}

复杂情况下@autoreleasepool的探讨

可以通过以下私有函数来查看自动释放池的情况:

extern void _objc_autoreleasePoolPrint(void);

由于iOS工程中,系统在自动释放池中注册了一些对象。为了排除这些干扰,接下来我们通过macOS工程代码示例:

单个 @autoreleasepool
int main(int argc, const char * argv[]) {_objc_autoreleasePoolPrint();     // print1@autoreleasepool {_objc_autoreleasePoolPrint(); // print2HTPerson *p1 = [[[HTPerson alloc] init] autorelease];HTPerson *p2 = [[[HTPerson alloc] init] autorelease];_objc_autoreleasePoolPrint(); // print3}_objc_autoreleasePoolPrint();     // print4return 0;
}

内存分布图

嵌套 @autoreleasepool
int main(int argc, const char * argv[]) {_objc_autoreleasePoolPrint();             // print1@autoreleasepool { //r1 = push()_objc_autoreleasePoolPrint();         // print2HTPerson *p1 = [[[HTPerson alloc] init] autorelease];HTPerson *p2 = [[[HTPerson alloc] init] autorelease];_objc_autoreleasePoolPrint();         // print3@autoreleasepool { //r2 = push()HTPerson *p3 = [[[HTPerson alloc] init] autorelease];_objc_autoreleasePoolPrint();     // print4@autoreleasepool { //r3 = push()HTPerson *p4 = [[[HTPerson alloc] init] autorelease];_objc_autoreleasePoolPrint(); // print5} //pop(r3)_objc_autoreleasePoolPrint();     // print6} //pop(r2)_objc_autoreleasePoolPrint();         // print7} //pop(r1)_objc_autoreleasePoolPrint();             // print8return 0;
}

内存分布图

复杂情况@autoreleasepool

AutoreleasePoolPage类的定义可知,自动释放池(即所有的AutoreleasePoolPage对象)是以为结点通过双向链表的形式组合而成。每当Page满了的时候,就会创建一个新的Page,并设置它为hotPage,而首个PagecoldPage。接下来我们来看一下多个Page和多个@autoreleasepool嵌套的情况。

int main(int argc, const char * argv[]) {@autoreleasepool { //r1 = push()for (int i = 0; i < 600; i++) {HTPerson *p = [[[HTPerson alloc] init] autorelease];}@autoreleasepool { //r2 = push()for (int i = 0; i < 500; i++) {HTPerson *p = [[[HTPerson alloc] init] autorelease];}@autoreleasepool { //r3 = push()for (int i = 0; i < 200; i++) {HTPerson *p = [[[HTPerson alloc] init] autorelease];}_objc_autoreleasePoolPrint();} //pop(r3)} //pop(r2)} //pop(r1)return 0;
}

一个AutoreleasePoolPage对象的内存大小为4096个字节,它自身成员变量占用内存56个字节,所以剩下的4040个字节用来存储autorelease对象的内存地址。又因为64bit下一个OC对象的指针所占内存为8个字节,所以一个Page可以存放505个对象的地址。POOL_BOUNDARY也是一个对象,因为它的值为nil。所以以上代码的自动释放池内存分布图如下所示。

内存分布图

使用iOS工程示例分析

从以上macOS工程示例可以得知,在@autoreleasepool大括号结束的时候,就会调用Pagepop()方法,给@autoreleasepool中的autorelease对象发送release消息。

那么在iOS工程中,方法里的autorelease对象是什么时候释放的呢?有系统干预释放和手动干预释放两种情况。

  • 系统干预释放是不指定@autoreleasepool,所有autorelease对象都由主线程的RunLoop创建的@autoreleasepool来管理。
  • 手动干预释放就是将autorelease对象添加进我们手动创建的@autoreleasepool中。

下面还是在MRC环境下进行分析。

系统干预释放

我们先来看以下 Xcode 11 版本的iOS程序中的main()函数,和旧版本的差异。

// Xcode 11
int main(int argc, char * argv[]) {NSString * appDelegateClassName;@autoreleasepool {// Setup code that might create autoreleased objects goes here.appDelegateClassName = NSStringFromClass([AppDelegate class]);}return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
// Xcode 旧版本
int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}
}

网上对于iOS工程的main()函数中的@autoreleasepool有一种解释:
iOS工程的main()函数中有一个@autoreleasepool,这个@autoreleasepool负责了应用程序所有autorelease对象的释放。

其实这个解释是错误的。

应用程序所有autorelease对象的都是由RunLoop创建的@autoreleasepool来管理。而main()函数中的@autoreleasepool只是负责管理它的作用域中的autorelease对象。

新版本 Xcode 11 中的 main 函数发生了哪些变化?
旧版本是将整个应用程序运行放在@autoreleasepool内,由于RunLoop的存在,要return即程序结束后@autoreleasepool作用域才会结束,这意味着程序结束后main函数中的@autoreleasepool中的autorelease对象才会释放。
而在 Xcode 11中,触发主线程RunLoopUIApplicationMain函数放在了@autoreleasepool外面,这可以保证@autoreleasepool中的autorelease对象在程序启动后立即释放。正如新版本的@autoreleasepool中的注释所写 “Setup code that might create autoreleased objects goes here.”(如上代码),可以将autorelease对象放在此处。

这里非常推荐去看参考博客关于这里的分析,很精彩也很好懂

手动干预释放

即手动使用添加的@autoreleasepool,会在大括号结束时就会释放 ;

Q:ARC 环境下,autorelease 对象在什么时候释放?

回到我们最初的面试题,在ARC环境下,autorelease对象在什么时候释放?我们就分系统干预释放手动干预释放两种情况回答。

Q:ARC 环境下,需不需要手动添加 @autoreleasepool?

AppKit 和 UIKit 框架会在RunLoop每次事件循环迭代中创建并处理@autoreleasepool,因此,你通常不必自己创建@autoreleasepool,甚至不需要知道创建@autoreleasepool的代码怎么写。但是,有些情况需要自己创建@autoreleasepool

苹果给出了三种需要手动添加@autoreleasepool的情况:

  • ① 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  • ② 如果你编写的循环中创建了大量的临时对象;
    你可以在循环内使用@autoreleasepool在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool有助于减少应用程序的最大内存占用。
  • ③ 如果你创建了辅助线程。(不开启runloop)
    一旦线程开始执行,就必须创建自己的@autoreleasepool;否则,你的应用程序将存在内存泄漏。

Q:如果对 NSAutoreleasePool 对象调用 autorelease 方法会发生什么情况?

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];[pool autorelease];

答:抛出异常NSInvalidArgumentException并导致程序Crash,异常原因:不能对NSAutoreleasePool对象调用autorelease


http://www.ppmy.cn/devtools/116085.html

相关文章

面试速通宝典——1

1. 内存有哪几种类型&#xff1f; ‌‌‌‌  内存分为五个区&#xff0c;堆&#xff08;malloc&#xff09;、栈&#xff08;如局部变量、函数参数&#xff09;、程序代码区&#xff08;存放二进制代码&#xff09;、全局/静态存储区&#xff08;全局变量、static变量&#…

MQ入门(一):同步调用和异步调用--RabbitMQ基础入门

目录 1.初识MQ 1.1.同步调用 1.2.异步调用 1.3.技术选型 2.RabbitMQ 2.1.安装部署 2.2.RabbitMQ基本架构 2.3.收发消息 2.3.1.交换机 2.3.2.队列 2.3.3.绑定关系 2.3.4.发送消息 2.4.数据隔离 2.4.1.用户管理 2.4.2.virtual host 1.初识MQ 微服务一旦拆分&…

基于等保2.0标准——区块链安全扩展要求探讨

在数字经济时代&#xff0c;区块链作为新技术&#xff0c;能够推进经济社会规则体系重构&#xff0c;在经济金融、司法审判、食品追溯、商业贸易、公共信用等领域已有广泛应用。但在规划、建设、运维区块链的同时&#xff0c;也需要全面评估与防范区块链应用带来的安全隐患。 …

【C语言】fork函数使用

在 UNIX 和类 UNIX 系统中,`fork()` 是用于创建新进程的系统调用。它将当前进程(父进程)复制一份,创建一个新的子进程。`fork()` 调用返回两次:一次在父进程中,并返回子进程的进程ID;一次在子进程中,并返回值 0。可以通过对这些返回值进行判断,决定接下来在父进程和子…

AWS 实时数据流服务 Kinesis

AWS 实时数据流服务 Kinesis 什么是 KinesisKinesis 数据来源示例 AWS Lambda 发送数据到 Kinesis步骤 1&#xff1a;创建 Kinesis 数据流步骤 2&#xff1a;编写 Lambda 函数步骤 3&#xff1a;配置 Lambda 函数权限部署和测试 Lambda 函数 消费和处理 Kinesis 数据流示例 Fli…

Spring Boot 从 2.7.x 升级到 3.3注意事项

将 Spring Boot 从 2.7.x 升级到 3.3 是一个重要的迁移过程&#xff0c;特别是因为 Spring Boot 3.x 系列基于 Jakarta EE 9&#xff0c;而不再使用 Java EE。此版本升级伴随着许多重大变化&#xff0c;以下是你在升级过程中需要注意的关键事项&#xff1a; 1. JDK 版本升级 …

【Kubernetes】常见面试题汇总(二十八)

目录 79.您如何看待公司从单一服务转向微服务并部署其服务容器&#xff1f; 80.什么是 Headless Service&#xff1f; 特别说明&#xff1a; 题目 1-68 属于【Kubernetes】的常规概念题。 题目 69-113 属于【Kubernetes】的生产应用题。 79.您如何看待公司从单一服务转…

计算机前沿技术-人工智能算法-大语言模型-最新论文阅读-2024-09-19

计算机前沿技术-人工智能算法-大语言模型-最新论文阅读-2024-09-19 1. SAM4MLLM: Enhance Multi-Modal Large Language Model for Referring Expression Segmentation Authors: Yi-Chia Chen, Wei-Hua Li, Cheng Sun, Yu-Chiang Frank Wang, Chu-Song Chen SAM4MLLM: 增强多模…