引言
首先来看如下代码,思考一下输出以及原因。
// DemoObject
@interface DemoObject : NSObject
@end
@implementation DemoObject
+ (DemoObject*)object {
DemoObject *object = [[DemoObject alloc]init];
return object;
}
@end
// AutoreleasePoolDemoVC
@interface AutoreleasePoolDemoVC ()
@end
@implementation AutoreleasePoolDemoVC
- (void)viewDidLoad {
[super viewDidLoad];
__weak id object1;
{
object1 = [DemoObject object];
}
NSLog(@"object1 = %@", object1);
__weak id object2;
{
object2 = [[DemoObject alloc]init];
}
NSLog(@"object2 = %@", object2);
}
输出如下:
object1 = <DemoObject: 0x2811d0370>
object2 = (null)
对比上述代码可以发现除了获取DemoObject实例的方法不一样其他的一模模一样样,问题应该就在这里了。其实对于第二种方式系统也已经给出了提示:
那么为什么
object1没有被释放呢?
原因就是我们今天需要分析的这个AutoreleasePool自动释放池了。
自动释放池AutoreleasePool简介
以下摘自苹果官方文档:
在引用计数(相对于垃圾回收)环境中,自动释放池包含了接受autorelease消息的对象, 当池子倾倒的时候,会对里面的每一个对象发送release消息。因此对一个对象发送autorelease消息而不是release消息,可以增加对象的生命周期直到它所在的AutoreleasePool被倒掉。
在引用计数环境中,Cocoa期望有一个有效的AutoreleasePool,否则的话,autorelease对象不会被释放进而导致内存泄漏。
在上述代码中,我们可以看到
p的dealloc方法没有被调用。同时我们利用_objc_autoreleasePoolPrint打印当前自动释放池中的内容,发现了确实存在p确实在自动释放池中,但是因为自动释放池并没有触发pop操作,因此p无法调用release方法,造成了内存泄漏,不过在上述情况中,执行完代码程序直接就退出了,应该也没啥问题,仅供实例参考。
此处还有一个问题,我们并没有创建自动释放池,但是打印的结果是有的,这是什么时候创建的呢?在后面的分析中会说明。
源码分析
重写cpp文件
想要了解自动释放池的底层实现,我们需要利用到clang指令
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
我们利用如下指令对上述文件进行重写:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
最后得到一个cpp文件,只摘出有用的部分如下:
// main
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
可以看到重写之后的文件中,@autoreleasepool变成了__AtAutoreleasePool结构体类型的变量,该结构体的初始化方法和析构方法如上图所示,其实就相当于在我们的代码段的开始和结束插入了一下两行代码:
atautoreleasepoolobj = objc_autoreleasePoolPush();
// 自己的代码
...
objc_autoreleasePoolPop(atautoreleasepoolobj);
很显然,这两行代码是内存释放池的核心所在,接下来我们就根据源码来一探究竟。
自动释放池源码位于libobjc中,当前版本为objc4-781.2
objc_autoreleasePoolPush
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
可以看到其实是调用了AutoreleasePoolPage的push函数。自动释放池应该是跟这个AutoreleasePoolPage有着千丝万缕的关系。因此我们先来看一下AutoreleasePoolPage的结构
AutoreleasePoolPage
struct AutoreleasePoolPageData
{
magic_t const magic; // 16
__unsafe_unretained id *next; // 8
pthread_t const thread; // 8
AutoreleasePoolPage * const parent; // 8
AutoreleasePoolPage *child; // 8
uint32_t const depth; // 4
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)
{
}
};
class AutoreleasePoolPage : private AutoreleasePoolPageData {
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();
}
static void * operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
...
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096
}
结构如下图所示:
可以看出
AutoreleasePoolPage是一个双向链表结构,同时内部是一个栈结构。虽然AutoreleasePoolPage内部只有7个变量,一共占了56个字节的大小,但是重写了new方法,每一次都会开辟4096(4k)大小的空间
几个核心变量如下:
- next
id*类型,指向当前page中的下一个可插入位置。 - thread 当前page所在的线程
- parent 前一个page
- child 后一个page
- depth 深度,代表当前page在整个双向链表中的位置
在大致了解了AutoreleasePoolPage的结构之后,我们继续来看push函数
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// 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;
}
# define POOL_BOUNDARY nil
其中的POOL_BOUNDARY可以理解是一个哨兵对象,为nil。
autoreleaseFast
我们直接来看autoreleaseFast的实现
static inline id *autoreleaseFast(id obj)
{
// 获取当前的hotpage
AutoreleasePoolPage *page = hotPage();
// page存在并且没满
if (page && !page->full()) {
return page->add(obj);
// page满了
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
// page不存在
return autoreleaseNoPage(obj);
}
}
首先从当前的线程本地存储tls中取出当前的hotPage,其中tls在之前的文章多线程@synchronized锁中有提到过。
接下来分为三种情况来插入obj,在我们当前的流程中,插入的obj为哨兵对象POOL_BOUNDARY:
hotpage存在并且没有满hotpage满了hotpage不存在 接下来分情况分析
1. hotpage存在并且没有满
此时直接往hotpage中插入即可
id *add(id obj)
{
ASSERT(!full());
unprotect();
// 记录需要插入的位置
id *ret = next; // faster than `return next-1` because of aliasing
// 在对应的位置插入obj,然后自增1
*next++ = obj;
protect();
// 返回,此时ret存放的就是obj的指针
return ret;
}
2. hotpage满了
根据上文中的full()函数,当前next指向当前page的end时,当前page处于满状态,此时需要开辟新的page来存放obj
autoreleaseFullPage
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full. 当前的hotpage满了
// Step to the next non-full page, adding a new page if necessary. 步入下一个不满的page,有需要的话添加一个新的page
// Then add the object to that page.接下来将obj插入到新的page
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
// 沿着链表查找下一个不满的page
if (page->child) page = page->child;
// 找到链接的最后一页也满了,此时需要创建新的page
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
AutoreleasePoolPage
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),
objc_thread_self(),
newParent,
newParent ? 1+newParent->depth : 0,
newParent ? newParent->hiwat : 0)
{
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
主要就是初始化新的page并且建立parent和当前page的关系。
初始化完毕之后需要将当前page设置为hotpage,具体操作就是存入tls,然后调用add方法添加object
3. hotpage不存在
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// 此处省略无关代码
...
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
...
// Push the requested object or pool.
return page->add(obj);
}
如果tls中不存在hotpage,应该是自动释放池第一次添加object。此时的page需要新创建,而且也没有父结点,同时还需要设置hotpage。
至此,objc_autoreleasePoolPush函数分析完了,总结一下,当前场景下主要的操作是:新生成一个AutoreleasePoolPage并设置为hotpage,将哨兵对象POOL_BOUNDARY插入到当前的hotpage中,返回插入的object即哨兵对象在自动释放池中的位置。
autorelease
我们知道自动释放池push()时会会插入哨兵对象,那么在文章最开始的实例代码中object1是怎么插入的呢?如图位置打下断点:
在xcode中查看汇编代码如下:
可以看到[DemoObject object]方法最终调用了
objc_autoreleaseReturnValue方法。源码如下:
// Prepare a value at +1 for return through a +0 autoreleasing convention.
id
objc_autoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
return objc_autorelease(obj);
}
objc_autorelease()->objc_object::autorelease()->rootAutorelease()->objc_object::rootAutorelease2()->AutoreleasePoolPage::autorelease()
最后的最后调用了autoreleaseFast方法,将我们的object插入到了自动释放池中。
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;
}
这里也解释了文章开头,我们没有显示调用push方法,但是自动释放池已经存在。因为autorelease调用了autoreleaseFast函数,参数为需要延迟释放的对象。而objc_autoreleasePoolPush本质是也是调用了autoreleaseFast函数,只不过参数为哨兵对象POOL_BOUNDARY。
objc_autoreleasePoolPop
objc_autoreleasePoolPop(atautoreleasepoolobj);
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// 忽略无关代码
...
} else {
page = pageForPointer(token);
}
stop = (id *)token;
// 忽略无关代码
...
return popPage<false>(token, page, stop);
}
pop函数需要找到当前token所在的page,然后执行popPage方法。
pageForPointer
static AutoreleasePoolPage *pageForPointer(const void *p)
{
return pageForPointer((uintptr_t)p);
}
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;
ASSERT(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
p为当前token的位置,p对SIZE求余可以的得到在当前page中的相对位置,p - offset得到当前page的起始位置,即找到token所在的page
popPage
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
...
page->releaseUntil(stop);
...
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();
}
}
}
- 释放page直到找到stop标记。
- 根据当前page的容量来进行对应的操作。
releaseUntil
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();
// page->next代表下一个需要插入的位置,此时是没有值的,但是上一个8字节地址是有值的,根据取值符*可以拿到对应的obj指针
id obj = *--page->next;
// 将page中next指向的位置改为SCRIBBLE
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
...
}
以上图为例,此时的
hotpage为pageN,需要pop的token为POOL_BOUNDARY。根据上述函数的描述操作如下:
1. 找到hotpage,即pageN
2. 校验pageN是否为空,否则循环寻找pageN的父结点并赋值为hotpage
3. 从hotpage的next减8字节,即page中最后一个autorelease对象开始,依次执行release操作,同时将page中的当前位置赋值为`SCRIBBLE`
4. 循环执行1-3知道当前page的next指向token,如果token不为POOL_BOUNDARY,也已经执行了release操作
5. 当前page设置为hotpage
操作完毕后,page1的结构如下:
接下来我们通过代码来验证一下:
其中:
-
0x7ff23e81e000为当前的AutoreleasePoolPage的起始地址,读取0x7ff23e81e000的内存可以看到更多的数据 -
0x7ff23e81e048为当前的page->next指向的位置 -
0x10e991dc0为当前page所在的线程 -
因为当前的自动释放池只有两个对象,没有父结点,也没有子结点,对应的depth和hiwat都为0,因此
0x7ff23e81e020到0x7ff23e81e030的数据都是0 -
0x7ff23e81e038为插入push的时候插入的哨兵对象POOL_BOUNDARY在当前page中的地址,可以看到和起始地址相差0x38也就是56,和AutoreleasePoolPage结构中的变量大小一致,POOL_BOUNDARY的本质也就是nil,所以也是0 -
0x6000006145a0是object的地址
kill
接着上文中的popPage函数,针对page的不同容量做了不同的处理。
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}