【iOS】ARC 与 Autorelease
ARC 与 Autorelease
文章目录
- ARC 与 Autorelease
- 前言
- 何为ARC
- 内存管理考虑方式
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也可以持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放
- ARC的具体实现
- 编译期和运行期ARC做的事情
- ARC实现:
- __autoreleasing 与 AutoreleasePool
- AutoreleasePool的结构
- AutoreleasePoolPage
- objc_autoreleasePoolPush
- autoreleaseNewPage
- 压栈对象 autoreleaseFast
- objc_autoreleasePoolPop
- 小结
前言
今天笔者来学习一下有关于ARC和我们这里的一个Auorelease的内容
何为ARC
首先ARC就是我们自动引用计数,自动引用计数主要在代码中插入执行下面步骤
- 生成对象
- 持有对象
- 释放对象
- 废弃对象
在OC中对应的方法是:
对象操作 | OC方法 |
---|---|
生成并且持有对象 | alloc/new/copy/mutableCopy等方法 |
持有对象 | retain方法 |
释放对象 | release |
废弃对象 | dealloc |
内存管理考虑方式
这里我们如果过度注意于引用计数这几个字上面的话,其实不算是一个正常客观的一个思考方式:
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也可以持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放
在结合上面的对应的对象操作,我们下面对于这个几个部分进行一个讲解
自己生成的对象,自己持有
使用下面方法名称开头的方法名意味着自己生成的对象只有自己可以持有
- alloc
- new
- copy
- mutablecopy
+ (id)allocMyObject{return [[NSObject alloc] init];
}
+ (id)myObject{return [[NSObject alloc] init];
}
这里我们打一个断点,来看一下这里的内容的内容,这里我们可以看到objc_release
设置了一个标记位.只有用alloc开头的地方做了一个标记,另一个方法就没有标记.
这里博文后面在详细ARC对应实现内容,这里
非自己生成的对象,自己也可以持有
类似于这种代码:
NSMutableArray* ary = [NSMutableArray array];
[ary reatin];
这里是采用reatin
来持有的
不再需要自己持有的对象时释放
[obj release];
非自己持有的对象无法释放
ARC的具体实现
在现在的OC语言中,我们有这些对象的是符合条件的:
- block
- 对象
- 由attribute((NSObject))标记的类型。
编译期和运行期ARC做的事情
- 在编译期,ARC会把互相抵消的retain、release、autorelease操作约简。
- ARC包含有运行期组件,可以在运行期检测到autorelease和retain这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。
ARC实现:
上面我们介绍了他的消息发送的标记,也就是ARC中生成并持有的操作
+ (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。
id
objc_autoreleaseReturnValue(id obj)
{if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;return objc_autorelease(obj);
}
objc_unsafeClaimAutoreleasedReturnValue
:这个函数的作用是:函数作用是对autorelease对象不做处理仅仅返回,对非autorelease对象调用objc_release函数并返回。所以本情景中它创建时执行了 autorelease操作了,就不会对其进行 release操作了。只是返回了对象,在合适的实际autoreleasepool会对其进行释放的。
id
objc_unsafeClaimAutoreleasedReturnValue(id obj)
{if (acceptOptimizedReturn() == ReturnAtPlus0) return obj;return objc_releaseAndReturn(obj);
}
这时候我们在主函数赋值:
id tmp1 = [self allocMyObject];
这里我们可以看到下面有一个objc_storeStrong
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无效的时候:
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
在ARC有效的时候:
@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;
+ (id)myObject{return [[NSObject alloc] init];
}
-
在访问__weak变量的时候,实际上必定要访问注册到 Autoreleasepool的对象,即会自动加入 Autoreleasepool;
-
id的指针或对象的指针(id*,NSError **),在没有显式地指定修饰符时候,会被默认附加上 __autoreleasing修饰符,加入 Autoreleasepool。这里是为了实现一个传递指针值的安全,把它注册到
autoreleasepool
可以保证这个对象不会被以外释放
AutoreleasePool的结构
int main(int argc, const char * argv[]) {@autoreleasepool {}return 0;
}int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }return 0;
}
这里可以看到它对应的是这样一个__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;
};
这里可以看到它的一个结构体样式,从本质上来讲这个释放池也是一个对象.
认识他的一个底层结构:
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
这两个函数是我们之前在上面看到的两个方法:
void *
_objc_autoreleasePoolPush(void)
{return objc_autoreleasePoolPush();
}void
_objc_autoreleasePoolPop(void *ctxt)
{objc_autoreleasePoolPop(ctxt);
}
- 下面这个源码展示对应的一个结构:
#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_AUTORELEASEPOOLPAGE_MAX_SIZE; // must be multiple of vm page size
#elsePAGE_MIN_SIZE; // size and alignment, power of 2 4096字节大小
#endifprivate:static pthread_key_t const key = AUTORELEASE_POOL_KEY;static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasingstatic 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
,下面展示出这个结构体的样式
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{//用来校验AutoreleasePoolPage的结构是否完整magic_t const magic;//16个字节//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()__unsafe_unretained id *next;//8字节 存储指针的一个指针//指向当前线程pthread_t const thread;//8字节//指向父节点,第一个结点的parent值为nilAutoreleasePoolPage * const parent;//8字节//指向子节点,最后一个结点的child值为nilAutoreleasePoolPage *child;//8字节//表示深度,从0开始,往后递增1uint32_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
现在我们学习它的压栈这个函数:
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
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 yetASSERT(!hotPage());bool pushExtraBoundary = false;//判断是否是空占位符,如果是,则压栈哨兵标识符置为YESif (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
所做的事情,下面介绍一下有关于对象是怎么被压入栈中的.
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
这里的出出栈的思路其实大致和压入栈中一样
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
消息)下面看一下这里的源码
popPage(void *token, AutoreleasePoolPage *page, id *stop){if (allowDebug && PrintPoolHiwat) printHiwat();page->releaseUntil(stop);// memory: delete empty childrenif (allowDebug && DebugPoolAllocation && page->empty()) {// special case: delete everything during page-per-pool debuggingAutoreleasePoolPage *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 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();}}}
//释放到stop位置之前的所有对象
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();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRSAutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;// create an obj with the zeroed out top byte and release thatid obj = (id)entry->ptr;int count = (int)entry->count; // grab these before memset
#elseid obj = *--page->next; //类似于处栈的方式处理里面的数据
#endifmemset((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 onefor (int i = 0; i < count + 1; i++) {objc_release(obj);}
#elseobjc_release(obj);
#endif}}setHotPage(this);#if DEBUG// we expect any children to be completely emptyfor (AutoreleasePoolPage *page = child; page; page = page->child) {ASSERT(page->empty());}
#endif}
上面其实就展示了这里的一个移除数据的操作:通过id obj = *--page->next; //类似于处栈的方式处理里面的数据
然后发送release
消息来释放内存
小结
这里其实就分成两个部分分析了AutoreleasePool
的内容
- 在自动释放池的压栈(即push)操作中
- 当没有pool,即只有空占位符(存储在tls中)时,则创建页,
压栈哨兵对象
- 在页中
压栈普通对象
主要是通过next
指针递增
进行的, - 当
页满
了时,需要设置页的child
对象为新建页
- 当没有pool,即只有空占位符(存储在tls中)时,则创建页,
- 在出栈操作中
- 通过next指针递减来实现一个释放
- 当
页空
了时,需要赋值页的parent
对象为当前页