上一篇文章介绍了AutoreleasePool的基本概念,本文将分析AutoreleasePool
的内存结构。
分析入口
在OC中,使用@autoreleasepool {}
代码块可以手动创建一个AutoreleasePool
,若想知道这个代码块内部做了哪些处理,可以通过Clang编译
和汇编调试
这两个方式来分析。
- 在
main
函数中创建AutoreleasePool
int main(int argc, char * argv[]) {
@autoreleasepool {
}
return 0;
}
- 方式一:
Clang
编译
在终端通过Clang
命令行将main.m
文件编译成main.cpp
文件,可以看到底层编译如下:
struct __AtAutoreleasePool {
//构造函数
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析构函数
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
//这里的{}表示autoreleasepool的作用域
{
__AtAutoreleasePool __autoreleasepool;
}
return 0;
}
//Clang命令行
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
- 方式二:汇编调试
在main函数中下断点,再设置Xcode的Debug
-> Debug Workflow
-> Always Show Disassembly
,就可以查看汇编调用流程,如下图所示:
从两种方式的分析结果可知,@autoreleasepool {}
代码块实现逻辑如下:
- 在
{}
作用域中通过构造函数objc_autoreleasePoolPush()
创建了一个AtAutoreleasePool
对象 - 当超出作用域时,再通过析构函数
objc_autoreleasePoolPop()
销毁这个对象。
因此,AutoreleasePool
的底层原理,可以通过在objc
源码中分析这两个函数的实现逻辑来得知。
源码中对AutoreleasePool的描述
在源码中,对AutoreleasePool
作了简单的描述,有助于后面分析其结构。
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.
每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界);
- 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 token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被释放。
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
- Thread-local storage points to the hot page, where newly autoreleased objects are stored.
Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。
通过描述,可知:
- 自动释放池是一个栈的结构,被划分成了一个以
page
为结点的双向链表,根据需要添加和删除页面。 - 每一页里存储着指向
自动释放的对象
或者pool_boundary
哨兵的指针。 -
hot page
表示当前活跃的页,存储新添加的自动释放的对象。 - 自动释放池和线程有关联。
分析自动释放池每页的结构
在源码中,objc_autoreleasePoolPush
和objc_autoreleasePoolPop
的实现如下:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
可以看到,两个函数实际上是调用AutoreleasePoolPage
的push
和pop
方法,这个AutoreleasePoolPage
就是自动释放池列表里的页。
AutoreleasePoolPage
//************宏定义************
#define PROTECT_AUTORELEASEPOOL 0
#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT) //4096
#define PAGE_MIN_MASK (PAGE_MIN_SIZE-1) //4095
//************类定义************
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
//页的大小,为PAGE_MIN_SIZE
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
#endif
private:
...
static size_t const COUNT = SIZE / sizeof(id);
//分配内存
static void * operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
//释放
static void operator delete(void * p) {
return free(p);
}
//构造函数
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),//开始存储的位置
objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
{...}
//析构函数
~AutoreleasePoolPage() {...}
//杀掉
void kill() {...}
//other private funcs
... ...
public:
//自动释放
static inline id autorelease(id obj){...}
//入栈
static inline void *push() {...}
//出栈
static inline void
pop(void *token){...}
//other public funcs
... ...
}
从AutoreleasePoolPage
的结构中可以看出:
- 每一页的大小为
4096
字节。 -
AutoreleasePoolPage
继承自AutoreleasePoolPageData
。
AutoreleasePoolPageData
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)
{
}
};
//总共16字节
struct magic_t {
//静态变量不占用结构体的内存
static const uint32_t M0 = 0xA1A1A1A1;
# define M1 "AUTORELEASE!"
static const size_t M1_len = 12;
//数组里有4个uint32_t类型的成员,总共16字节
uint32_t m[4];
//其他函数
... ...
};
从AutoreleasePoolPageData
结构中可知:
- 结构体内存大小为
56
字节。 - 结构体里存储了当前线程、下次新添加的对象的位置、父节点、子节点、页面深度、最大入栈数量等信息。
由于自动释放池里的每一页都是一个AutoreleasePoolPage
对象,AutoreleasePoolPage
又继承自AutoreleasePoolPageData
,因此可以得知,自动释放池每一页的结构如下:
- 每一页内存大小为
4096
字节,页本身成员占用其中56
字节的内存。 - 每一页里都存储了
next
指针,指向下次新添加的autoreleased
对象的位置。 - 每一页里都包含父节点和子节点,分别指向上一页和下一页。第一页的父节点为
nil
,最后一页的子节点为nil
。 - 每一页都有一个深度标记,第一页深度值为0,后面的页面递增1。
- 每一页里还包当前线程、最大入栈数量。
查看自动释放池的内存情况
上面分析了page
的结构,接下来通过具体Demo来打印自动释放池的内存。
-
首先需要将
Demo
设置为MRC
模式。将工程里的Build Settings
->Objectice-C Automatic Reference Counting
,设置为NO
。
-
在
main
函数里创建AutoreleasePool
,添加自动释放的对象,并打印内存。
//打印AutoreleasePool
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, char * argv[]) {
@autoreleasepool {
//往自动释放池里添加count数量的对象
NSInteger count = 对象数量;
for(NSInteger i=0; i<count; i++){
NSObject *obj = [[NSObject alloc] autorelease];
}
//调用
_objc_autoreleasePoolPrint();
}
return 0;
}
- 设置
count = 3
,即往释放池里添加3个对象,打印结果如下
objc[85067]: ##############
objc[85067]: AUTORELEASE POOLS for thread 0x1113a2dc0
objc[85067]: 4 releases pending.
//page地址,第一页的地址即为自动释放池的地址
objc[85067]: [0x7fbfa380b000] ................ PAGE (hot) (cold)
//哨兵
objc[85067]: [0x7fbfa380b038] ################ POOL 0x7fbfa380b038
//添加的3个对象,存放的是对象的地址指针
objc[85067]: [0x7fbfa380b040] 0x600003e2c020 NSObject
objc[85067]: [0x7fbfa380b048] 0x600003e2c030 NSObject
objc[85067]: [0x7fbfa380b050] 0x600003e2c040 NSObject
objc[85067]: ##############
从打印结果可知,当往自动释放池里添加3
个对象时:
- 当前创建了一页
page
,为hot page
。 - 哨兵地址和
page
地址相隔0x38
,共56
字节,说明哨兵前面存储的是AutoreleasePoolPage
自身的成员。 -
page
里存储的指针即为哨兵和自动释放对象的地址,为8字节。哨兵表示自动释放对象的边界。 - 地址以
0x7
开头,说明自动释放池存储在栈区,page
内部地址从低到高依次存储:AutoreleasePoolPage自身的成员
、哨兵
、自动释放的对象
。
每页page
的大小为4096
字节,减去自身成员占用的56
字节,能存储的指针数量为:(4096 - 56) / 8 = 505
个,这些指针指向的是哨兵和自动释放对象的地址。
- 设置
count = 505 * 2
,多往释放池里添加一些对象,再打印,结果如下
objc[92734]: ##############
objc[92734]: AUTORELEASE POOLS for thread 0x117e48dc0
objc[92734]: 1011 releases pending.
//第1页,page成员 + 1个哨兵 + 504个对象
objc[92734]: [0x7fee2d00f000] ................ PAGE (full) (cold)
objc[92734]: [0x7fee2d00f038] ################ POOL 0x7fee2d00f038
objc[92734]: [0x7fee2d00f040] 0x600001198030 NSObject
objc[92734]: [0x7fee2d00f048] 0x600001198050 NSObject
objc[92734]: [0x7fee2d00f050] 0x600001198060 NSObject
... ...
objc[92734]: [0x7fee2d00ffe8] 0x600001199f90 NSObject
objc[92734]: [0x7fee2d00fff0] 0x600001199fa0 NSObject
objc[92734]: [0x7fee2d00fff8] 0x600001199fb0 NSObject
//第2页,page成员 + 0个哨兵 + 505个对象
objc[92734]: [0x7fee2d011000] ................ PAGE (full)
objc[92734]: [0x7fee2d011038] 0x600001199fc0 NSObject
objc[92734]: [0x7fee2d011040] 0x600001199fd0 NSObject
objc[92734]: [0x7fee2d011048] 0x600001199fe0 NSObject
... ...
objc[92734]: [0x7fee2d011fe8] 0x60000119bf20 NSObject
objc[92734]: [0x7fee2d011ff0] 0x60000119bf30 NSObject
objc[92734]: [0x7fee2d011ff8] 0x60000119bf40 NSObject
//第3页,page成员 + 0个哨兵 + 1个对象
objc[92734]: [0x7fee2d00b000] ................ PAGE (hot)
objc[92734]: [0x7fee2d00b038] 0x60000119bf50 NSObject
objc[92734]: ##############
从这个打印结果可知,当往自动释放池里添加505 * 2
个对象时,当前总共创建了三页page
:
- 第1页,内部存放:
page成员
+1个哨兵
+504个对象
,状态为full page
。 - 第2页,内部存放:
page成员
+505个对象
,状态为full page
。 - 第3页,内部存放:
page成员
+1个对象
,状态为hot page
。
因此,一个自动释放池只有一个哨兵,存放在第一页。每页最多存放505
个指针,第一页满状态存放的是1
个哨兵和504
个对象地址,其他页存放的是505
个对象地址。存满的页被标记为full page
,正在操作的页被标记为hot page
。
综上所述
AutoreleasePool的结构经过分析,AutoreleasePool
的内存结构如上图所示,特点如下:
-
自动释放池是一个栈的结构,是一个以
AutoreleasePoolPage
为结点的双向链表,根据需要来动态添加或删除页面。 -
每一页
AutoreleasePoolPage
的大小为4096
字节,地址从低到高依次存储page自身成员
、哨兵
、对象指针
。其中,自身成员占用56
字节,且哨兵作为对象指针的边界,在释放池里只会有一个,因此:- 第一页,内部存放:
page成员
+1个哨兵
+504个对象指针
。 - 其它页,内部存放:
page成员
+505个对象指针
。
- 第一页,内部存放:
-
已存满的页面被标记为
full page
,当前正在操作的页被标记为hot page
。 -
AutoreleasePoolPage
继承自AutoreleasePoolPageData
,内部成员情况如下:-
magic
:用来校验AutoreleasePoolPage
的结构是否完整。 -
next
:下次新添加的autoreleased
对象的位置,初始化时指向begin()
。 -
thread
:当前线程,说明自动释放池和线程有关联。 -
parent
:指向父节点,即上一个页面,第一个页面的parent
值为nil
。 -
child
:指向子节点,即下一个页面,最后一个页面的child
值为nil
。 -
depth
:表示页面深度,从0开始,往后递增1。 -
hiwat
:即high water mark
,表示最大入栈数量标记
-
网友评论