写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 - 关于死锁,你了解多少?
- iOS底层 - 单例 销毁 可否 ?
- iOS底层 - Dispatch Source
- iOS底层 - 一个栅栏函 拦住了 数
- iOS底层 - 不见不散 的 信号量
- iOS底层 GCD - 一进一出 便成 调度组
- iOS底层原理探索 - 锁的基本使用
- iOS底层 - @synchronized 流程分析
- iOS底层 - 锁的原理探索
- iOS底层 - 带你实现一个读写锁
- iOS底层 - 谈Objective-C block的实现(上)
- iOS底层 - 谈Objective-C block的实现(下)
- iOS底层 - Block, 全面解析!
- iOS底层 - 启动优化(上)
- iOS底层 - 启动优化(下)
- iOS底层原理探索 -- 内存管理 之 内存五大区
- iOS底层原理探索 -- 内存管理 之 Tagged Pointer Format Changes
- iOS底层原理探索 -- 内存管理 之 retain & release
- iOS底层原理探索 -- 内存管理 之 弱引用表
以上内容的总结专栏
细枝末节整理
前言
内存管理系列的文章今天我们继续来到 @autoreleasepool
的底层原理探索。话不多说,这就开始今天的内容吧。
自动释放池
自动释放池 @autoreleasepool
最常见的地方就是我们项目的 main
。我们今天来深入探索下其底层结构和实现原理。
想要了解其内部底层结构,我们先通过 xcrun
一下,看下其 编译后是什么结构:
可以看到 编译后 将 @autoreleasepool
注释掉,替换为了 __AtAutoreleasePool __autoreleasepool;
__AtAutoreleasePool 是一个结构体
struct __AtAutoreleasePool {
// 构造函数
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
// 析构函数
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
编译后 的 __AtAutoreleasePool __autoreleasepool;
这行代码就相当于
在作用域开始的地方,调用 构造函数, 在作用域结束的时候,调用析构函数。
也就是分别调用了:
- objc_autoreleasePoolPush();
- objc_autoreleasePoolPop(atautoreleasepoolobj);
通过符号断点 我们定位一下它的源码所在 :
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
AutoreleasePoolPage
/***********************************************************************
Autorelease pool implementation
线程的自动释放池是一个指针堆栈。
每个指针要么是一个要释放的对象,要么是 POOL_BOUNDARY 自动释放池边界。
边界对象用来标记池子的边界,防止在出栈的时候操作越界。
池令牌是指向该池的 POOL_BOUNDARY 的指针。
当 池被弹出,每个 比 哨兵 热的 对象 都被释放。
堆栈被分成一个 双向链表。 页面添加 并在必要时删除。
线程本地存储指向新自动释放的热页 对象存储。
**********************************************************************/
BREAKPOINT_FUNCTION(void objc_autoreleaseNoPool(id obj));
BREAKPOINT_FUNCTION(void objc_autoreleasePoolInvalid(const void *token));
class AutoreleasePoolPage : private 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
...
struct magic_t {
static const uint32_t M0 = 0xA1A1A1A1;
# define M1 "AUTORELEASE!"
static const size_t M1_len = 12;
uint32_t m[4]; // 4*4
...
}
};
配置关闭 ARC ,我们测试下:
通过打印可以看到,哨兵对象也是一个对象。
下面来到 push() 函数:
push()
第一次
首先没有页面(autoreleaseNoPage
),我们会创建第一个页面, 然后将页面设置为热页面,接着加入烧饼对象,再接着加入 其他的对象入池子。
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// 每个自动释放池从一个新的池页开始。
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
...
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);
}
}
...
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" 可能是说泳池没有被推过
// 或者一个空的占位符池已经被推入并且还没有内容
ASSERT(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) { ... }
else if (obj != POOL_BOUNDARY && DebugMissingPools) { ... }
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) { ... }
// 我们正在推入一个对象或一个非占位符池
// 安装第一页。
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page); // 设置为 热页面
// 为先前占位符池推入一个边界。
if (pushExtraBoundary) {
// 加哨兵对象
page->add(POOL_BOUNDARY);
}
// 推入请求的对象或池。
return page->add(obj);
}
AutoreleasePoolPage
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),
objc_thread_self(),
newParent,
newParent ? 1+newParent->depth : 0,
newParent ? newParent->hiwat : 0)
{
if (objc::PageCountWarning != -1) {
checkTooMuchAutorelease();
}
// 这里是双向链表的设置
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
...
id * begin() {
// begin 页面从自动释放池的成员(自动释放池也需要56字节内存空间) 之后的内存开始
return (id *) ((uint8_t *)this+sizeof(*this)); //56
}
其大小为 56 字节。
在这里,我们也可以验证 - begin 页面从自动释放池的成员(自动释放池也需要56字节内存空间) 之后的内存开始。 且自动释放池第一个加进来的对象是哨兵对象;之后才是我们入栈进入的对象。
满了 page->full()
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// 热点页面已满
// 步骤跳转到下一个非完整页面,如有必要,添加新页面。
// 然后将对象添加到该页面。
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
// 满了之后, 找到最后一个子页面,才添加一个page
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
内存分页管理:
- 空间换时间的常规操作:
在这里不使用连续内存,是为了不断的压栈出栈的话对内存的压力以及操作性能的问题,以及全部的数据存在一个页面上,管理上会十分的繁琐,一旦出问题,全部都完蛋;所以采用分页可以有效的隔离降低互相直降的影响,提升性能;分页之后,内存可以不连续,再次提升性能。
自动释放池大小
测试我们发现,每个页面可以管理504个对象:
为什么是504个对象一页呢?
504*8+56+POOL_BOUNDARY = 4096 = 2^12
这一大小在源码中也有体现:
入栈流程: 通过next指针不断内存平移加入对象后,指向下一个位置;
出栈流程: 找到hot页面,找到parent页面设置为hot页面,将当前页面删掉。通过哨兵对象来判断是否已经完成。
扩展
- 自动释放池可以嵌套使用, 并且互不影响。
- 没有 autorelease 并不会交给 @autoreleasepool 来管理(ARC环境下 编译器会自动 添加 autorelease)。
- 以 alloc、 new、 copy、 mutablecopy 为前缀 对象开头的 不会交给 @autoreleasepool 来管理。