iOS 内存管理之AutoReleasePool
背景
自从苹果推出了ARC管理内存后,对于iOS开发这而言,内存管理就变得so easy了,只要正确使用相关规则,再也不用担心double release,野指针的等问题了,而ARC的背后,除了强大的编译器之外,还要得益于运行时起作用的AutoReleasePool。
研究AutoReleasePool
iOS的项目中,除了特别需求外,整个项目就一个地方明确写了autoReleasePool的代码了,就是main函数:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
autoreleasepool做了什么?
我们知道oc代码在编译期间都会转化为c/c++代码,然后转化为汇编,最终转化为对应的架构的二进制文件;也可以这么说,oc的底层实现就是c/c++,既然这样,我们把他转化为对应的c/c++代码应该就可以窥探到其中的密码了:
转化为c/c++代码,Xcode有自带的工具,打开命令行,输入一下命令就可以:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
为了减少代码量,重新建了一个macOS命令行项目:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
转化为cpp文件,看下对应的代码:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_h0x40m15075dxn_z956dk2500000gn_T_main_6e2ecd_mi_0);
}
return 0;
}
从上面的C++源码可以发现@autoreleasepool {}最后变成了:
{ __AtAutoreleasePool __autoreleasepool;//定义了一个__AtAutoreleasePool结构体变量
。。。
}
我们分析下流程:
1、进入大括号,定义了一个__AtAutoreleasePool的结构体局部变量;
2、定义这个结构体变量的时候,会走结构体的构造方法,间接的会调用objc_autoreleasePoolPush函数:
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
3、当走出大括号是,局部变量__autoreleasepool,会被销毁,因此会走结构体的析构函数,间接就会调用objc_autoreleasePoolPop函数:
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
从上面的分析,我们发现了两个重要的函数objc_autoreleasePoolPush和objc_autoreleasePoolPop,这两个函数是全局函数,而且是以objc开头的,应该是在objc的源码中,下载objc源码的地址,macOS 最新系统下面的objc4。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
他们调用的是AutoreleasePoolPage类对应的push跟pop两个静态函数,那么我们就要研究下AutoreleasePoolPage这个类了。
研究AutoreleasePoolPage
class AutoreleasePoolPage :
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
...
}
从AutoreleasePoolPage的成员变量可以分析出,AutoreleasePoolPage是一个双向链表的结构,每个实例都会存在一个parent实例指针,跟一个child实例指针。其他成员变量暂时不知道表示什么意思,只能继续研究AutoreleasePoolPage的实现逻辑了,还是从push跟pop函数入手:
AutoreleasePoolPage 的push函数:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page .debug模式新建一个page对象,实际不需要关注
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {//实际走这里
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
追根溯源autoreleaseFast:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
//拿到当前正在被使用的page,因为每个page都是有对象obj数量限
//制的,当page放满了,就会创建一个child page来继续放。
if (page && !page->full()) {//没满,直接添加到当前的page上
return page->add(obj);//添加obj
} else if (page) {//full,满了
return autoreleaseFullPage(obj, page);//将obj添加到对应的未满的child page里面,并将其设置为hot page
} else {//没有page
return autoreleaseNoPage(obj);
}
}
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
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;//若有child page,将当前的指针指向child page
else page = new AutoreleasePoolPage(page);//new一个新的page,并将其赋值给child page;
} while (page->full());//page是否满了,没满,跳出
setHotPage(page);//将page设置为当前hotpage
return page->add(obj);//添加obj到page
}
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;
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();//设置一个占位的空的page
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.第一次创建一个page
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);//并将它设置为hotpage
// 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);//添加obj
}
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));//page的起始地址+成员变量的大小
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);///一个page的大小是size,4096字节
}
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//将obj的指针赋值给next所在的位置,然后将next指向下一个位置
protect();
return ret;//返回前一个obj
}
总结一下:
- push操作从取出当前的hotpage,然后将一个哨兵对象(其实是nil)放入到page的next位置,并将next指向的位置向下+1;
- 取出的hotpage存在,但是hotpage是一个fullpage(一个page,PAGE_MAX_SIZE = 4096字节大小,除了存放内部成员变量的值之外,其他的都用来存放autorelease对象的地址),这时就会循环查找他的child指向的page对象,知道找到没使用完的child page,如果没有child page,则创建一个,将找到的child page并设置为hotpage,然后将一个哨兵对象添加进去;
- 取出的当前hotpage不存在,则通过autoreleaseNoPage创建一个新的page,并设置为hotpage,然后将然后将一个哨兵对象添加进去;
- 这样做的结果,除了第一层page(没有parent page),每次push返回的obj的都是哨兵对象,最开始的push返回的是第一层page最开始的位置page.begin(),后面的pop会用到这个返回值。
AutoreleasePoolPage 的pop函数:
static inline void pop(void *token)
{//token就是对应push返回值,上面提到过要么是哨兵对象,要么是第一层page最开始的位置
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) {
//要么是第一层page最开始的位置
// 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 {//其他情况不存在,坏的page
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);//释放存放在page的指针所指的对象,直到遇到哨兵对象或者全部释放完成
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *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 pools
page->kill();
setHotPage(nil);
}
else if (page->child) {//将page对象释放掉
// 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();
}
}
}
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释放完,还没遇到哨兵对象,拿到parent page,并设置为hotpage,继续释放,直到遇到哨兵对象或者全部释放完
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;//拿到obj对象,并将next指针指向上一个位置
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));//清空next位置,这里是设置为0x3A
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);//释放掉对象
}
}
setHotPage(this);//释放完后,将当前page设置为hotpage
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
pop函数总结几点:
- 会拿到最近一次push函数(pop函数与push一一对应)返回的哨兵对象,作为pop函数的入参;
- 遍历hotpage的next指针指向的对象,并释放,直到遇到入参的哨兵对象;
- 如果当前page释放完了,还没遇到哨兵对象,就会往parent page遍历,直到遇到哨兵对象,一次类推;
- 最后释放掉为空(empty)的page对象,但是需要注意的是:当他的parent的使用空间超过了1/2,保留它对应的child page。
从上面的分析大致了解了AutoreleasePoolPage的工作流程,在程序运行的时候是怎么样工作的呢?
我们知道,在MRC时代,需要程序员手写对象的retain和release,后面最智能的就是new一个对象的时候,我们需要带上autorelease代码:
[[[NSObject alloc] init] autorelease];//MRC手动管理内存
到了ARC,我们不需要这样写,因为编译器在编译的时候,会自动帮忙加上这些代码,所以说不管是ARC还是MRC时期,oc对象的内存管理入口都是autorelease方法:
autorelease方法的研究:
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;//tagPointer 不需要
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);//调用的是AutoreleasePoolPage的autorelease函数
}
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);//调用autoreleaseFast,上面提到过
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
就是说,每个oc对象创建的时候(alloc/new/copy/mutableCopy),都是通过autorelease方法添加到page对象中的。pop那具体的调用时机又是什么呢?我们知道,项目只在main函数有一个autoreleasepool,其他的地方除非程序员自己手动添加,就不会有了,也就是说:我们暂且认为编译器帮忙添加的autorelease,那也只是添加进page,pop函数还是只有一个,而且是程序退出的时候调用,如果是这种情况的话,程序整个运行期间,内存得不停的增长,因为只有申请,没有释放。显然这种做法是行不通的 。
RunLoop& AutoReleasePool关系几点说明:
- App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
- 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
- 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
总结
通过上面的源码及流程的分析,我们对于autoreleasepool的工作原理及流程有了充分的了解:
- autoreleasepool底层的数据结构是一个autoreleasepage的双向链表,每个page的大小为4096字节,除了存储成员变量的大小,其他的位置都用来存储autorelease对象的地址,next变量永远指向下一个可以存放autorelease对象地址的地址空间,具体结构如下: 数据结构
- 当一个page1存储空间用完后,会创建一个新的page2,新的page2的parent指针指向满了的page1,page1的child指针会指向page2;
- 程序启动就会创建一个autoreleasepool,会调用push,程序结束时,最后会调用pop,回收所有autorelease对象的内存;
- 程序运行期间,同过监听runloop的休眠状态,调用push/pop方法,管理autorelease对象;
- 每次push的时候,会往page里面添加一个哨兵对象,这个哨兵对象作为下次pop函数的入参,遇到哨兵对象,说明这次runloop循环添加到page的autorelease对象release完毕。