开启生长之旅!这是我参与「日新计划 12 月更文挑战」的第6天,点击检查活动概况
一、AutoreleasePool初探
咱们先看一个比如代码:
@autoreleasepool {
// insert code here...
NSLog(@"Hello world!");
NYPerson *p = [NYPerson alloc];
_objc_autoreleasePoolPrint();//检查主动开释池
}
运转检查打印结果: 在打印中没有看到NYPerson的内存信息。
假如咱们加上 __autoreleasing NYPerson *p = [NYPerson alloc];
在看下打印:
现在咱们看到了NYPerson的内存信息,NYPerson目标被增加到主动开释池中。
为什么不增加__autoreleasing
关键字,不会增加到autoreleasepool
中呢?
由于alloc、new、copy、allocWith...、copyWith...
等关键字创立的目标是不会增加到autoreleasepool中的。
那么假如增加如下代码:
NSString *str = [NSString stringWithFormat:@"%@",@"123"];
会增加到autoreleasepool吗?答应是:不会。 为什么呢?由于这个目标是tagged point目标。
假如改成如下代码:
NSString *str = [NSString stringWithFormat:@"%@",@"1234567890"];
答应是:会增加到autoreleasepool。由于现已不是tagged point目标
,而且一个nsstring目标了。超越9个字符就无法保存在地址中了。
咱们在来看一段代码:
for (int i = 0; i < 100000000; i ++) {
NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
}
来看运转: 发现履行了100000000次创立NSString目标,cpu和内存快速上涨到1G。
接下来,咱们来修改代码:
for (int i = 0; i < 100000000; i ++) {
@autoreleasepool {
NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
}
}
咱们发现,只有cpu上涨了,内存保持在8.9MB没有太大的变化。
为什么加了AutoreleasePool
的代码能够确保内存不大涨呢?AutoreleasePool
底层做了什么呢?咱们接着探究。
小结
:
AutoreleasePool
(主动开释池)是OC中的一种内存主动收回机制,它能够推迟参加AutoreleasePool中的变量release的机遇
。在正常状况下,创立的变量会在超出起作用域的时分release
,可是假如将变量参加AutoreleasePool
,那么release将会推迟履行
。
二、AutoreleasePool的数据结构
咱们持续探究:
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
clang main.m 生成 main.cpp 翻开main.cpp检查源码: 查找__AtAutoreleasePool源码: 咱们看到代码:
atautoreleasepoolobj = objc_autoreleasePoolPush();//结构函数
objc_autoreleasePoolPop(atautoreleasepoolobj);//析构函数
查找objc_autoreleasePoolPush
进入源码:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
依据AutoreleasePoolPage::push(); 来看下AutoreleasePoolPage
的数据结构
//AutoreleasePoolPage 结构承继自AutoreleasePoolPageData
class AutoreleasePoolPage 结构承继自 : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
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
//........................省掉...........................//
}
发现AutoreleasePoolPage 结构承继自AutoreleasePoolPageData 数据结构:
struct AutoreleasePoolPageData
{
//........................省掉...........................//
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
//........................省掉...........................//
};
-
magic
:用来校验AutoreleasePoolPage的结构是否完好; -
next
:指向最新增加的autoreleased目标的下一个位置,初始化时指向begin(); -
thread
:指向当前线程; -
parent
:指向父结点,第一个结点的parent值为nil; -
child
:指向子结点,最后一个结点的child值为nil; -
depth
:代表深度,从0开始,往后递加1; -
hiwat
:代表 high water mark 最大入栈数量标记;
三、AutoreleasePool的增加数据
咱们先来看下AutoreleasePoolPage 的注释:
/***********************************************************************
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,作为主动开释池的鸿沟。
(主动开释池里面的数据有两种类型,一种是行将被开释的目标,另一种是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的指针。当池被弹出时,每个比岗兵更热的目标
都会被开释。(在调用pop函数的时分,整个主动开释池里面的目标都会被开释。)
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
//栈被分红一个双向链接的页面列表。依据需要增加和删去页面。(主动开释池是一个双向列表
的栈结构。它的每个节点都是AutoreleasePoolPage。)
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
//线程的本地存储指向热页,其间存储了新的主动开释目标。(
主动开释池里面的数据也有线程缓存)
**********************************************************************/
咱们在进入AutoreleasePoolPage::push();检查push函数。
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {//判别是否初始化
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);//初始化 newpage
} else {
dest = autoreleaseFast(POOL_BOUNDARY);//参加岗兵目标
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
检查autoreleaseNewPage(POOL_BOUNDARY)源码:
id *autoreleaseNewPage(id obj)
{
AutoreleasePoolPage *page = hotPage();//线程的快速缓存中找到AutoreleasePoolPage 没有回来nil
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);//nil 就履行创立AutoreleasePoolPage第一页并增加增加岗兵目标
}
这个hotpage函数做了什么?得进入源码检查:
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);//线程的快速缓存中找到AutoreleasePoolPage
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;//没找到回来nil
if (result) result->fastcheck();
return result;
}
大概意思是:线程的快速缓存中找到AutoreleasePoolPage
则回来nil,假如有值就回来找到的AutoreleasePoolPage
。
先看没有找到AutoreleasePoolPage
的状况:autoreleaseNoPage(obj)
做了什么?
主要是说,创立一个AutoreleasePoolPage
并设置它为热页,把传入的obj(岗兵目标)增加到这页中(第一页)。
在看下new AutoreleasePoolPage(nil);
详细做了什么?
看下begin()做了什么?
//this=AutoreleasePoolPage+(page巨细56)等于内存平移,的地址
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
然后咱们接着看下autoreleaseFullPage(obj, page);
线程的快速缓存中找到AutoreleasePoolPage
的状况。
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;//满了,找子节点
else page = new AutoreleasePoolPage(page);//子节点也满了,创立一个AutoreleasePoolPage
} while (page->full());//判别是否满了
setHotPage(page);//设置成热页
return page->add(obj);
}
找到这个page,假如满了->找子节点。假如子节点也满了,创立一个AutoreleasePoolPage页。 设置page为热页,并增加obj到page中。
双向链表示意图:
四、Autorelease目标的开释
先来看下autorelease和runloop的联系图:
- App启动后,苹果在主线程RunLoop里注册了两个Observer其回调都是
_wrapRunLoopWithAutoreleasePoolHandler()
。 -
第一个Observer
监视的事情是Entry(行将进入Loop),其回调内会调用_objc_autoreleasePoolPush()创立主动开释池。其order是-2147483647,优先级最高,确保创立开释池产生在其他一切回调之前。 -
第二个Observer
监视了两个事情:BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()开释旧的池并创立新池;Exit(行将退出Loop)时调用_objc_autoreleasePoolPop()来开释主动开释池。这个Observer的order是2147483647,优先级最低,确保其开释池产生在其他一切回调之后。
⼦线程的autorelease目标
子线程中一旦呈现了Autorelease目标
,就会创立主动开释池。子线程中创立主动开释池的方法和主线程是相同的,也是调用autoreleaseNoPage
。也便是说在子线程中的Autorelease目标
,咱们同样不需要进行手动的内存管理,也不会内存走漏。
AutoreleasePool的开释数据
咱们在源码中看到: AutoreleasePoolPage 的巨细在4KB 4096。 一切一页page能够存 4096-56-8(岗兵)= 4032/8 = 504 个目标。
咱们在经过一个比如验证,上述理论:
@autoreleasepool {
for(int i=0;i<1010;i++){
__autoreleasing NYPerson *p = [NYPerson alloc];
}
_objc_autoreleasePoolPrint();//检查主动开释池
}
检查打印: 第一页为full cold页存504个目标,岗兵目标占用一个。 第二页为full页存505个目标。 第三页为hot页存了一个目标。
所以应该autoreleasepoolpage
第一页能够存504个目标,其他页能够存505个目标。
咱们接着看objc_autoreleasePoolPop(atautoreleasepoolobj)
进入到源码:
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
在进入pop源代码:
static inline void
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);//设置热页为nil
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
page = coldPage();//获取cold页
token = page->begin();//把cold页第一个设置为岗兵
} else {
page = pageForPointer(token);//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);//开释链表中的目标到stop位置。
}
先看下pageForPointer
做了什么:
在接着看popPage<false>(token, page, stop);
源码:
进行跟踪page->releaseUntil
的源码:
void releaseUntil(id *stop)
{
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();
//........................省掉...........................//
memset((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 one
for (int i = 0; i < count + 1; i++) {
objc_release(obj);
}
#else
objc_release(obj);//将这段代码插入开释池中的每目标
#endif
}
}
setHotPage(this);//设置当前页为热页
//........................省掉...........................//
}
小结
:
objc_autoreleasePoolPop
函数经过token(岗兵目标)地点的page,怎么依据hotpage经过releaseUntil
函数从hotpage页一直插入开释代码objc_release(obj)
直到token(岗兵目标)停止。
在ARC环境下,autorelease 目标在什么时分开释?
- 假如是手动增加到@autoreleasepool里面的
autorelease目标
,是在出了@autoreleasepool的作用域之后被开释。 - 假如是体系增加到主动开释池的
autorelease目标
的开释机遇便是由RunLoop操控的,会在每一次的RunLoop循环结束时开释。 - 假如是子线程没有开启RunLoop,便是在子线程毁掉的时分开释。
总结
-
AutoreleasePool初探
:AutoreleasePool
(主动开释池)是OC中的一种内存主动收回机制,它能够推迟参加AutoreleasePool中的变量release的机遇
。在正常状况下,创立的变量会在超出起作用域的时分release
,可是假如将变量参加AutoreleasePool
,那么release将会推迟履行
。 -
AutoreleasePool的数据结构
:发现AutoreleasePoolPage 结构承继AutoreleasePoolPageData 数据结构,56个字节是一个双向链表结构。 -
AutoreleasePool的增加数据
:经过objc_autoreleasePoolPush
函数压栈,第一页第一个会创立一个岗兵目标也便是autoreleaseNoPage(obj)函数,然后autoreleaseFullPage(obj, page);
线程的快速缓存中找到热页增加目标到热页中。 -
Autorelease目标的开释
:objc_autoreleasePoolPop
函数经过token(岗兵目标)地点的page,怎么依据hotpage经过releaseUntil
函数从hotpage页一直插入开释代码objc_release(obj)
直到token(岗兵目标)停止。