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

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.xdnf.cn/news/1539181.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

怎么把图片压缩变小?把图片压缩变小的八种压缩方法介绍

怎么把图片压缩变小&#xff1f;在当今这个信息高度共享的时代&#xff0c;图片不仅仅是简单的视觉元素&#xff0c;它们承载着我们的记忆、故事和创意。无论是旅行的风景、家庭的聚会&#xff0c;还是工作中的项目展示&#xff0c;图片都在我们的生活中扮演着不可或缺的角色。…

文达通从创业板、北交所转战港股:净利润下滑,资产负债率124%

《港湾商业观察》施子夫 8月13日&#xff0c;青岛文达通科技股份有限公司&#xff08;以下简称&#xff0c;文达通&#xff09;递表港交所获受理&#xff0c;联席保荐机构为山证国际、浤博资本。 在选择递表港交所前&#xff0c;文达通曾于新三板挂牌上市并且还曾尝试谋求创业…

智谱清影 -CogVideoX-2b-部署与使用,带你揭秘生成6s视频的极致体验!

文章目录 1 效果展示2 CogVideoX 前世今生3 CogVideoX 部署实践流程3.1 创建丹摩实例3.2 配置环境和依赖3.3 模型与配置文件3.4 运行4 遇到问题 1 效果展示 A street artist, clad in a worn-out denim jacket and a colorful bandana, stands before a vast concrete wall in …

产品:直播送礼延迟这么大,你就不能快点吗

先赞后看&#xff0c;南哥助你Java进阶一大半 其实抖音的实时音视频技术RTC&#xff0c;是来源于火山引擎RTC的支持&#xff0c;抖音、火山引擎、巨量引擎都属于字节旗下不同的业务板块。 我是南哥&#xff0c;一个Java学习与进阶的领路人。 相信对你通关面试、拿下Offer进入心…

俄罗斯电商Ozon实用运营工具推荐

想要在俄罗斯最大的跨境电商平台 Ozon 上大展拳脚&#xff0c;却对俄语感到无从下手&#xff1f;又或是担心难以把握俄罗斯市场的热点趋势&#xff1f;别担心&#xff01;在这篇文章中&#xff0c;我们将为你介绍一系列实用工具&#xff0c;涵盖翻译、运营和图片处理等方面&…

有源滤波器UAF42

有源滤波器模块&#xff0c;在电路板上同时实现了低通&#xff0c;高通&#xff0c;带通 滤波器&#xff0c;可选其一进行输出&#xff0c;并可通过改变滑变阻值&#xff0c;轻松调节其滤波器中心频率&#xff0c;Q值&#xff0c;通带增益等&#xff0c; 也可方便实现Butterwo…

深度学习基础案例5--VGG16人脸识别(体验学习的痛苦与乐趣)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 这次目标本来要达到60%&#xff0c;但是却非常稳定的达到了40%&#xff0c;​&#x1f622;​​&#x1f622;​​&#x1f622;​​&#x1f622;​&am…

实战17-NavBar+Vip布局

NavBar.ets import { PADDING } from ../../constants/size import rvp from ../../utils/resposive/rvIndexComponent export default struct NavBar {StorageProp(topHeight) topHeight: number 0;build() {Row() {Row({ space: rvp(6) }) {Text(请选择地址).fontSize(rvp(1…

Java-测试-Mockito 入门篇

之前很长一段时间我都认为测试就是使用SpringBootTest类似下面的写法&#xff1a; SpringBootTest class SysAuthServiceTest {AutowiredSysRoleAuthMapper sysRoleAuthMapper;Testpublic void test() {QueryWrapper<SysRoleAuth> queryWrapper new QueryWrapper<&g…

Web开发:Thymeleaf模板引擎

1. Thymeleaf 简介 Thymeleaf 是一个现代的服务器端模板引擎&#xff0c;用于生成 HTML、XML、JavaScript 和 CSS。它的设计理念是使模板能够自然地在 Web 浏览器中呈现&#xff0c;同时允许动态生成内容。 2. 最佳实践总结 2.1 项目结构和模板组织 保持清晰的目录结构&…

Electron-vue asar 局部打包优化处理方案——绕开每次npm run build 超级慢的打包问题

背景 因为组员对于 Electron 打包过程存在比较迷糊的状态&#xff0c;且自己也没主动探索 Electron-vue 打包细节&#xff0c;导致每次打包过程都消耗 5-6 分钟的时间&#xff0c;在需要测试生产打包时&#xff0c;极其浪费时间&#xff0c;为此针对 Electron-vue 打包的几个环…

C++ —— 关于vector

目录 链接 1. vector的定义 2. vector的构造 3. vector 的遍历 4. vector 的扩容机制 5. vector 的空间接口 5.1 resize 接口 5.2 push_back 5.3 insert 5.4 erase 5.5 流插入与流提取 vector 并不支持流插入与流提取&#xff0c;但是可以自己设计&#xff0c;更…

MSF的使用学习

一、更新MSF apt update # 更新安装包信息&#xff1b;只检查&#xff0c;不更新&#xff08;已安装的软件包是否有可用的更新&#xff0c;给出汇总报告&#xff09; apt upgrade # 更新已安装的软件包&#xff0c;不删除旧包&#xff1b; apt full-upgrade # 升级包&#x…

深度学习-18-深入理解BERT实战使用预训练的DistilBERT模型

文章目录 1 预训练的BERT模型2.1 单词级的嵌入表示2.2 句子级的嵌入表示2.3 从最顶层编码器层中抽取嵌入表示2.3.1 预处理输入2.3.2 获得嵌入表示2.4 从所有的编码器层中抽取嵌入表示2.4.1 预处理输入2.4.2 嵌入表示3 为下游任务微调BERT3.1 文本分类3.1.1 原理(微调BERT模型)3…

MTK芯片机型的“工程固件” 红米note9 5G版资源预览 写入以及改写参数相关步骤解析

小米机型:小米5 小米5x 米6 米6x 米8 米9 米10系列 米11系列 米12系列 mix mix2 mix2s mix3 max max2 max3 note3 8se 9se cc9系列 米play 平板系列等分享 红米机型:红米note4 红米note4x 红米note5 红米note6 红米note7 红米note8 红米note8pro 红米s2 红米note7pro 红米…

大数据概念与价值

文章目录 引言大数据的概念高德纳咨询公司的定义麦肯锡全球研究所的定义什么是大数据&#xff1f; 大数据的特征Volume&#xff08;体积&#xff09;Variety&#xff08;种类&#xff09;Velocity&#xff08;速度&#xff09;Value&#xff08;价值&#xff09;Veracity&#…

OpenCV 1

前言&#xff1a;开新坑辽&#xff0c;&#xff0c; 目录 计算机眼中的图像 视频的读取与处理 ROI区域 边界填充 数值计算 腐蚀操作 膨胀操作 开运算与闭运算 梯度计算 礼貌与黑帽 Sobel算子 梯度计算方法 scharr与laplacian 计算机眼中的图像 灰色图片&#xff0…

微服务——网关路由(Spring Cloud Gateway)

网关路由 1.什么是网关 网关又称网间连接器、协议转换器&#xff0c;是在网络层以上实现网络互连的复杂设备&#xff0c;主要用于两个高层协议不同的网络之间的互连。网关就是网络的关口。数据在网络间传输&#xff0c;从一个网络传输到另一网络时就需要经过网关来做数据的路由…

MYSQL登录失败,确保密码正确,常见问题

今天登录MYSQL时&#xff0c;发现登录不进去,我能确保密码没有错误&#xff0c;并且我昨天以这样的方式登录成功&#xff0c;我已经重启过mysql服务&#xff0c;但是依旧登录不进去。 C:\Users\user>mysql -u root -p Enter password: ****** ERROR 1045 (28000): Access …

(已解决)vscode如何选择python解释器

文章目录 前言解决方案 前言 有的时候可能有不同版本的编译器&#xff0c;以适用不同年份的项目。所以&#xff0c;怎么在vscode中换python解释器呢&#xff1f; 解决方案 对着要运行的python文件进行右键&#xff0c;比如我是要运行main文件&#xff0c;点击那个命令选项版…