【iOS】ARC 与 Autorelease

ARC 与 Autorelease

文章目录

前言

今天笔者来学习一下有关于ARC和我们这里的一个Auorelease的内容

何为ARC

首先ARC就是我们自动引用计数,自动引用计数主要在代码中插入执行下面步骤

  • 生成对象
  • 持有对象
  • 释放对象
  • 废弃对象

在OC中对应的方法是:

对象操作 OC方法
生成并且持有对象 alloc/new/copy/mutableCopy等方法
持有对象 retain方法
释放对象 release
废弃对象 dealloc

内存管理考虑方式

这里我们如果过度注意于引用计数这几个字上面的话,其实不算是一个正常客观的一个思考方式:

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也可以持有
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

在结合上面的对应的对象操作,我们下面对于这个几个部分进行一个讲解

自己生成的对象,自己持有

使用下面方法名称开头的方法名意味着自己生成的对象只有自己可以持有

  • alloc
  • new
  • copy
  • mutablecopy
objc 复制代码
+ (id)allocMyObject{
    return [[NSObject alloc] init];
}
+ (id)myObject{
    return [[NSObject alloc] init];
}

这里我们打一个断点,来看一下这里的内容的内容,这里我们可以看到objc_release设置了一个标记位.只有用alloc开头的地方做了一个标记,另一个方法就没有标记.

这里博文后面在详细ARC对应实现内容,这里

非自己生成的对象,自己也可以持有

类似于这种代码:

objc 复制代码
NSMutableArray* ary = [NSMutableArray array];
[ary reatin];

这里是采用reatin来持有的

不再需要自己持有的对象时释放
objc 复制代码
[obj release];
非自己持有的对象无法释放

ARC的具体实现

在现在的OC语言中,我们有这些对象的是符合条件的:

  • block
  • 对象
  • 由attribute((NSObject))标记的类型。
编译期和运行期ARC做的事情
  • 在编译期,ARC会把互相抵消的retain、release、autorelease操作约简。
  • ARC包含有运行期组件,可以在运行期检测到autorelease和retain这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。
ARC实现:

上面我们介绍了他的消息发送的标记,也就是ARC中生成并持有的操作

objc 复制代码
+ (id)allocMyObject{
    return [[NSObject alloc] init];
}
+ (id)myObject{
    return [[NSObject alloc] init];
}

这里说明我们的allocMyObject方法会在标识位调用一个objc_release,而另一个方法则是调用我们的objc_unsfaleClaimAutoreleaseReturnVlaue.

这里还要对比一个函数objc_autoreleaseReturnValue:这个函数的作用相当于代替我们手动调用 autorelease, 创建了一个autorelease对象。编译器会检测之后的代码, 根据返回的对象是否执行 retain操作, 来设置全局数据结构中的一个标志位, 来决定是否会执行 autorelease操作。该标记有两个状态, ReturnAtPlus0代表执行 autorelease, 以及ReturnAtPlus1代表不执行 autorelease。

objc 复制代码
id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

objc_unsafeClaimAutoreleasedReturnValue:这个函数的作用是:函数作用是对autorelease对象不做处理仅仅返回,对非autorelease对象调用objc_release函数并返回。所以本情景中它创建时执行了 autorelease操作了,就不会对其进行 release操作了。只是返回了对象,在合适的实际autoreleasepool会对其进行释放的。

objc 复制代码
id
objc_unsafeClaimAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus0) return obj;

    return objc_releaseAndReturn(obj);
}

这时候我们在主函数赋值:

objc 复制代码
 id tmp1 = [self allocMyObject];

这里我们可以看到下面有一个objc_storeStrong

objc 复制代码
void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

先获取首先获取旧对象,然后进行比较,如果新对象和旧对象相同,则返回。否则,保留新对象,并将新对象的引用+1。否则,保留新对象,并将新对象的引用+1。然后更新指针*location,指向新对象。最后释放旧对象

这里其实就是我们的代码__strong修饰符给它插入了这个函数objc_storeStrong,所以在ARC的规则下其实就是通过下面这几种所有权修饰符号来插入不同的内存管理函数进行一个自动内存管理的:

  • __strong
  • __weak
  • __ unsafe __ retain
  • __autoreleasing

__autoreleasing 与 AutoreleasePool

在ARC无效的时候:

objc 复制代码
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];

在ARC有效的时候:

objc 复制代码
@autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        id __autoreleasing obj = [[NSObject alloc] init];
        
    }

这里通过给对象赋值给附有__autoreleasing的变量等价于在MRC情况下,调用对象的autorelease方法,就是把对象注册到我们的autorelasePool

如上图所示的,其实可以按照下面这个表来理解__autoreleasing的和自动释放池的一个关系:

机制 角色
__autoreleasing 标记对象:声明对象应交给自动释放池管理。
Autorelease Pool 托管对象:存储被标记的对象,并在自身销毁时统一释放它们。

但是我们在实际开发中很少使用过有关于__autoreleasing这个来显式声明,这里有下面几种情况对象会被自动注册到AutoreleasePool

  • 编译器会进行优化,检查方法名是否以 alloc/new/copy/mutableCopy开始,如果不是则自动将返回对象注册到 Autoreleasepool;
objc 复制代码
+ (id)myObject{
    return [[NSObject alloc] init];
}
  • 在访问__weak变量的时候,实际上必定要访问注册到 Autoreleasepool的对象,即会自动加入 Autoreleasepool;

  • id的指针或对象的指针(id*,NSError **),在没有显式地指定修饰符时候,会被默认附加上 __autoreleasing修饰符,加入 Autoreleasepool。这里是为了实现一个传递指针值的安全,把它注册到autoreleasepool可以保证这个对象不会被以外释放

AutoreleasePool的结构
objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
    }
    return 0;
}

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

这里可以看到它对应的是这样一个__AtAutoreleasePool这个结构体:

objc 复制代码
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;
};

这里可以看到它的一个结构体样式,从本质上来讲这个释放池也是一个对象.

认识他的一个底层结构:

objc 复制代码
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.
每个指针都是要释放的对象,或者是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.
池令牌是指向该池的POOL_BOUNDARY的指针。弹出池后,将释放比哨点更热的每个对象。

- 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. 
线程本地存储指向热页面,该页面存储新自动释放的对象。
 

可以总结成下面四点:

  • 1、自动释放池一个关于指针的栈
  • 2、其中的指针是指要释放的对象或者 pool_boundary 哨兵(现在经常被称为 边界
  • 3、自动释放池是一个的结构(虚拟内存中提及过) ,而且这个页是一个双向链表(表示有父节点 和 子节点,在类中提及过,即类的继承链)
  • 4、自动释放池和线程有关系

我们主要关心三个问题:

  • 什么时候创建
  • 对象是怎么加入自动释放池的
  • 那些对象会被加入
AutoreleasePoolPage

这两个函数是我们之前在上面看到的两个方法:

objc 复制代码
void *
_objc_autoreleasePoolPush(void)
{
    return objc_autoreleasePoolPush();
}

void
_objc_autoreleasePoolPop(void *ctxt)
{
    objc_autoreleasePoolPop(ctxt);
}
  • 下面这个源码展示对应的一个结构:
objc 复制代码
#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)
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 4096字节大小
#endif
    
private:
	static pthread_key_t const key = AUTORELEASE_POOL_KEY;
	static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
	static size_t const COUNT = SIZE / sizeof(id);
    static size_t const MAX_FAULTS = 2;

    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil

    // SIZE-sizeof(*this) bytes of contents follow

从上面的page的信息可以看出,其实每一个自动释放池是一个页,页的大小是4096字节.

然后发现它继承于AutoreleasePoolPageData,下面展示出这个结构体的样式

objc 复制代码
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    //用来校验AutoreleasePoolPage的结构是否完整
	magic_t const magic;//16个字节
  
    //指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
	__unsafe_unretained id *next;//8字节 存储指针的一个指针
  
    //指向当前线程
	pthread_t const thread;//8字节
  
    //指向父节点,第一个结点的parent值为nil
	AutoreleasePoolPage * const parent;//8字节
  
    //指向子节点,最后一个结点的child值为nil
	AutoreleasePoolPage *child;//8字节
  
    //表示深度,从0开始,往后递增1
	uint32_t const depth;//4字节
  
    //表示high water mark 最大入栈数量标记
	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)
	{
	}
};

这里画出了他的一个样式,它是一个双方向链表,它其实是一个这样的结构AutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage中间存储他的一个信息.

objc_autoreleasePoolPush

现在我们学习它的压栈这个函数:

objc 复制代码
static inline void *push() 
    {
        id *dest;
  			
        if (slowpath(DebugPoolAllocation)) { //判断是否有pool
            // 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;
    }
autoreleaseNewPage

创建一个NewPage

objc 复制代码
id *autoreleaseNewPage(id obj)
    {
  			//获得当前操作页
        AutoreleasePoolPage *page = hotPage();
  			//如果存在,则需要压栈
        if (page) return autoreleaseFullPage(obj, page);
  			//不存在就创建页
        else return autoreleaseNoPage(obj);
    }
//获得当前操作页
static inline AutoreleasePoolPage *hotPage() 
    {
  
  //获取当前页
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
  //如果是一个空池,则返回nil,否则,返回当前线程的自动释放池
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
//创建类的函数
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.
        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);
    }


    static __attribute__((noinline))
    id *autoreleaseNewPage(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }

下面笔者直接给出结论:

  • autoreleasePool这里的链表的第一个页面可以存储了504个NSobject对象的指针,他的大小是4040 + 一个AutoreleasePoolPageData的大小(56字节) = 4096, 因为这里有一个哨兵节点,
  • 从第二页开始可以存储505个对象,因为少了一个哨兵节点 所以正好可以存储505个对象4040 / 8 = 505(这里为什么存储的是8个字节大小,是因为这里保存的是一个对象的指针,可以更好的利用内存).

下面看一下这个具体的结构

AutoreleasePoolPage中拥有 parent和 child指针,分别指向上一个和下一个 page;当前一个 page的空间被占满(每个 AutorelePoolPage的大小为4096字节)时,就会新建一个 AutorelePoolPage对象并连接到链表中,后来的 Autorelease对象也会添加到新的 page中;

每一个页内类似与一个栈的结构,通过数组实现的一个栈

另外,当 next==begin()时,表示 AutoreleasePoolPage为空;

当 next ==end(),表示 AutoreleasePoolPage已满。

上面这个图展示出了这个双向链表的一个具体结构

压栈对象 autoreleaseFast

上面介绍了有关于创建一个autoreleasePool所做的事情,下面介绍一下有关于对象是怎么被压入栈中的.

objc 复制代码
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); //第一次创建页
        }
    }

这里也就分成这几个步骤:

  • 获取当前操作页,并判断页是否存在以及是否满了
  • 如果页存在,且未满,则通过add方法压栈对象 (给next加加,添加数据类似与一个数组的样式);
  • 如果页存在,且满了,则通过autoreleaseFullPage方法安排新的页面 (就是通过双向链表产生一个新页来实现)
  • 如果页不存在,则通过autoreleaseNoPage方法创建新页
objc_autoreleasePoolPop

这里的出出栈的思路其实大致和压入栈中一样

objc 复制代码
pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
  //判断入参
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) { 
            // Popping the top-level placeholder pool.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }
  • 这里先处理入参
  • 容错处理
  • 通过popPage出栈页 (这里其实就是类似于给这个页中的对象发送release消息)下面看一下这里的源码
objc 复制代码
popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top)
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }
//释放到stop位置之前的所有对象
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();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
            AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

            // create an obj with the zeroed out top byte and release that
            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
                for (int i = 0; i < count + 1; i++) {
                    objc_release(obj);
                }
#else
                objc_release(obj);
#endif
            }
        }

        setHotPage(this);

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

上面其实就展示了这里的一个移除数据的操作:通过id obj = *--page->next; //类似于处栈的方式处理里面的数据然后发送release消息来释放内存

小结

这里其实就分成两个部分分析了AutoreleasePool的内容

  • 在自动释放池的压栈(即push)操作中
    • 当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象
    • 在页中压栈普通对象主要是通过next指针递增进行的,
    • 页满了时,需要设置页的child对象为新建页
  • 在出栈操作中
    • 通过next指针递减来实现一个释放
    • 页空了时,需要赋值页的parent对象为当前页
相关推荐
二流小码农6 小时前
鸿蒙开发:实现一个标题栏吸顶
android·ios·harmonyos
season_zhu6 小时前
iOS开发:关于日志框架
ios·架构·swift
Hello.Reader6 小时前
Git 安装全攻略Linux、macOS、Windows 与源码编译
linux·git·macos
Hope Fancy6 小时前
macOS 连接 Docker 运行 postgres,使用navicat添加并关联数据库
macos·docker·postgresql
John Song6 小时前
macOS 上使用 Homebrew 安装redis-cli
数据库·redis·macos
yanjiee6 小时前
编译一个Mac M系列可以用的yuview
macos
数据知道7 小时前
Mac电脑上本地安装 redis并配置开启自启完整流程
数据库·redis·macos
Digitally10 小时前
如何在电脑上轻松访问 iPhone 文件
ios·电脑·iphone
安和昂10 小时前
【iOS】YYModel源码解析
ios
pop_xiaoli10 小时前
UI学习—cell的复用和自定义cell
学习·ui·ios