内存管理剖析(二)——定时器问题markdown
经历过MRC时代的开发者,确定都用过autorelease
方法,用于把对象交给AutoreleasePool
管理,在合适的时候,自动释放对象。其实所谓的自动释放对象,就是对所管理的对象调用release
方法。要想知道autorelease
方法的原理,首先就须要弄清楚AutoreleasePool
是个什么东东。iphone
下面来看一个段MRC环境下的代码,为何要在MRC下讨论这个问题呢?由于ARC会为咱们在合适的地方自动加上autorelease
代码,而且不容许咱们手动调用该方法了,为了方便研究autorelease
原理,咱们仍是得回到MRC。函数
****************** main.m *****************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
NSLog(@"pool--start");
@autoreleasepool {
CLPerson *p = [[[CLPerson alloc] init] autorelease];
}
NSLog(@"pool--end");
return 0;
}
************** CLPerson.m **************
#import "CLPerson.h"
@implementation CLPerson
- (void)dealloc
{
NSLog(@"%s", __func__);
[super dealloc];
}
@end
****************** 打印结果 *******************
2019-08-27 16:37:15.141523+0800 Interview16-autorelease[11602:772121] pool--start
2019-08-27 16:37:15.141763+0800 Interview16-autorelease[11602:772121] -[CLPerson dealloc]
2019-08-27 16:37:15.141775+0800 Interview16-autorelease[11602:772121] pool--end
复制代码
归纳一下看到的表面现象:CLPerson
实例对象p
是在@autoreleasepool {}
大括号结束的时候被释放的。 那么@autoreleasepool {}
到底作了什么呢?咱们在命令行窗口里对main.m
文件执行以下命令oop
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
布局
在生成的中间代码main.cpp
中,找到main
函数的底层实现以下post
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
return 0;
}
复制代码
其实若是你熟悉消息机制,上述的代码能够转化成以下形式ui
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
CLPerson *p = [[[CLPerson alloc] init] autorelease];
}
return 0;
}
复制代码
咱们观察可发现@autoreleasepool {}
通过编译以后发生了以下转变
这里多了个__AtAutoreleasePool
,它实际上是个c++的结构体,能够在main.cpp
里搜索到它的定义以下
struct __AtAutoreleasePool {
//构造函数-->能够类比成OC的init方法,在建立时调用
__AtAutoreleasePool()
{
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析构函数-->能够类比成OC的dealloc方法,在销毁时调用
~__AtAutoreleasePool()
{
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
复制代码
若是你还不了解C++语法也无妨,它跟OC的类类似,能够有函数(方法),上面的这个结构体__AtAutoreleasePool
里面有已经有两个函数,
__AtAutoreleasePool()
--> atautoreleasepoolobj = objc_autoreleasePoolPush();
,结构体被建立时调用,用于结构体的初始化~__AtAutoreleasePool()
--> objc_autoreleasePoolPop(atautoreleasepoolobj);
,结构体被销毁时调用再回到咱们的main
函数,其实它本质上就是下面这个形式上面是单层
@autoreleasepool {}
的状况,那么若是有多层@autoreleasepool {}
嵌套在一块儿,就能够按照一样的规则来拆解
接下来咱们就来探究一下这两个函数的实现逻辑。在objc4源码的NSObject.mm
文件里能够找到它们的实现
*************** NSObject.mm (objc4) ******************
void * objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
复制代码
能够看到,它们分别调用了C++类 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
的两个函数
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
复制代码
begin()
函数返回一个指针,指向自身最后一个成员变量以后的内存地址(至关于越过了自身所占用的内存空间) end()
里面有一个SIZE
,咱们看看它的定义
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
********************************************
#define PAGE_MAX_SIZE PAGE_SIZE
********************************************
#define PAGE_SIZE I386_PGBYTES
********************************************
#define I386_PGBYTES 4096 /* bytes per 80386 page */
复制代码
能够看到,SIZE
其实是4096
。这就是说end()
函数,获得的是一个指针,指向AutoreleasePoolPage
对象地址以后的第4096
个字节的内存地址。
经过以上掌握的信息,咱们先抛出结论,而后再继续经过源码加深理解。
每一个
AutoreleasePoolPage
对象占4096
个字节,其中成员变量共占用8字节
*7
=56个字节
。剩余的4040
个字节的空间就是用来存储自动释放对象的。由于一个
AutoreleasePoolPage
对象的内存是有限的,程序里面可能有不少对象会被加入自动释放池,所以可能会出现多个AutoreleasePoolPage
对象来共同存放自动释放对象。全部的AutoreleasePoolPage
对象是以双向链表的形式(数据结构)链接在一块儿的。
AutoreleasePoolPage
对象的各成员变量含义以下
magic_t const magic;
id *next;
指向AutoreleasePoolPage
内下一个能够用来存放自动释放对象的内存地址pthread_t const thread;
自动释放池所属的线程,说明它不能跟多个线程关联。AutoreleasePoolPage * const parent;
指向上一页释放池的指针AutoreleasePoolPage *child;
指向下一页释放池的指针uint32_t const depth;
uint32_t hiwat;
接下来,咱们就正式开始研究AutoreleasePoolPage::push();
。假设咱们如今是处在项目的main函数的第一个@autoreleasepool {}
开始的地方,也就是整个程序将会第一次去调用push()
函数:
# define POOL_BOUNDARY nil
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {//Debug模式下,每一个autorelease pool都会建立新页
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {//标准状况下,调用autoreleaseFast()函数
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
复制代码
其中POOL_BOUNDARY
就是nil
的宏定义,忽略Debug模式,咱们只看正常模式,那么push()
将会调用autoreleaseFast(POOL_BOUNDARY)
获得一个id *dest
并将其返回给上层函数。查看一下这个autoreleaseFast()
,看看它到底能给咱们返回什么
static inline id *autoreleaseFast(id obj) {
//拿到当前可用的AutoreleasePoolPage对象page
AutoreleasePoolPage *page = hotPage();
//(1)若是page存在&&page未满,则直接增长obj
if (page && !page->full()) {
return page->add(obj);
} else if (page) {//(2)若是满了,则调用autoreleaseFullPage(obj, page);
return autoreleaseFullPage(obj, page);
} else {//(3)若是没有页面,则调用autoreleaseNoPage(obj);
return autoreleaseNoPage(obj);
}
}
复制代码
由于是整个程序第一次push操做,所以page对象还不存在,因此会按照状况(3)走,也就是autoreleaseNoPage(obj);
,实现以下
static __attribute__((noinline))
id *autoreleaseNoPage(id obj) {
/*--"No page" 1.能够表示当前尚未任何pool被建立(pushed) 2.也能够表示已经建立了一个empty placeholder pool(空释放池占位符),只是还没添加任何内容 */
assert(!hotPage());
//标签-->是否须要增长额外的POOL_BOUNDARY
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
/* 若是存在EmptyPoolPlaceholder(空占位符pool),就修改标签为true, 后面就须要依据此标签增长额外的POOL_BOUNDARY */
pushExtraBoundary = true;
}
/* 若是传入的obj不等于POOL_BOUNDARY(nil)而且找不到当前pool(丢失了),返回nil */
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
_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;
}
/* ♥️♥️♥️♥️若是传入的是POOL_BOUNDARY,而且不在Debug模式, 会调用setEmptyPoolPlaceholder()设置一个EmptyPoolPlaceholder */
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
return setEmptyPoolPlaceholder();
}
// 初始化第一个AutoreleasePoolPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//将其设置成当前页(hot)
setHotPage(page);
// 根据pushExtraBoundary标签决定是否多入栈一个POOL_BOUNDARY
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// 将传入的obj入栈,经过 add()函数
return page->add(obj);
}
复制代码
由于此时尚未建立过AutoreleasePoolPage
,而且也没有设置过EmptyPoolPlaceholder
,所以程序会命中代码中♥️♥️♥️♥️标记出的代码,调用setEmptyPoolPlaceholder();
,该函数实现以下
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
********************************************
static inline id* setEmptyPoolPlaceholder() {
assert(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}
复制代码
能够看到实际上就是将key
与(id*)1
绑定起来,这个key
是一个静态常量,最后将这个(id*)1
做为一个空释放池池占位符返回,这样整个程序的第一个push()
函数结束,结果是生成了一个EMPTY_POOL_PLACEHOLDER (也就是(id*)1)
做为释放池占位符。
接着上面的过程,咱们在push()
后,第一次对某个对象执行autorelease
方法时,看一下autorelease
的内部作了什么,先找到其源码以下
- (id)autorelease {
return ((id)self)->rootAutorelease();//🈯️从这里往下走
}
************************************************
inline id objc_object::rootAutorelease() {
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();//🈯️从这里往下走
}
************************************************
__attribute__((noinline,used))
id objc_object::rootAutorelease2() {
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);//🈯️从这里往下走
}
************************************************
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;
}
复制代码
经过逐层递进,咱们看到autorelease
方法最终又来到了autoreleaseFast()
函数
static inline id *autoreleaseFast(id obj) {
//拿到当前可用的AutoreleasePoolPage对象page
AutoreleasePoolPage *page = hotPage();
//(1)若是page存在&&page未满,则直接增长obj
if (page && !page->full()) {
return page->add(obj);
} else if (page) {//(2)若是满了,则调用autoreleaseFullPage(obj, page);
return autoreleaseFullPage(obj, page);
} else {//(3)若是没有页面,则调用autoreleaseNoPage(obj);
return autoreleaseNoPage(obj);
}
}
复制代码
那么这一次,咱们看看第一句代码里面hotPage();
获得的是什么
static inline AutoreleasePoolPage *hotPage() {
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
//若是检查到key有绑定EMPTY_POOL_PLACEHOLDER,返回nil
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;//将当前页对象返回
}
复制代码
由于咱们一开始将key
与EMPTY_POOL_PLACEHOLDER
绑定过,所以这里返回空,代表当前页空,还未被建立,所以咱们返回到autoreleaseFast
方法里面,将会调用autoreleaseNoPage(obj)
函数,根据咱们上面对这个函数步骤的注释,这一次程序应该会走到函数的最后一部分 主要作了下面几件事:
AutoreleasePoolPage
EMPTY_POOL_PLACEHOLDER
会使pushExtraBoundary
置为true
,所以这里须要为第一个AutoreleasePoolPage
先入栈一个POOL_BOUNDARY
add(obj)
将传入的自动释放对象obj
入栈上面add()
函数的具体功能,其实就是将obj
的值赋值给当前AutoreleasePoolPage
的next
指针指向的内存空间,而后next
再进行++
操做,移向下一段可用内存空间,方便下一次存放自动释放对象的时候使用。以下
id *add(id obj) {
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//先赋值,再++
protect();
return ret;
}
复制代码
另外须要注意一下这里的setHotPage(page)
函数,实现以下
static inline void setHotPage(AutoreleasePoolPage *page) {
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
复制代码
它的做用就是把当前新建立的AutoreleasePoolPage
与key
绑定起来,往后hotPage()
函数就能够经过key
直接拿到当前页。
若是咱们继续对新的对象执行autorelease
操做,一样会来到函数,但因为AutoreleasePoolPage
对象已经存在了,若是当前page
未满,会走以下函数 也就是直接经过
add(obj)
函数将obj
对象入栈
咱们以前说过,一个AutoreleasePoolPage
对象能存放的自动释放对象数量是有限的,一个自动释放对象就是一个指针,占8字节,而AutoreleasePoolPage
对象可用的空间是4040个字节,也就是能够存放505个对象(指针),因此一页AutoreleasePoolPage
是有可能满页的,这个时候,autoreleaseFast
就会调用autoreleaseFullPage(obj, page);
函数,它的实现以下
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 {//经过child指针拿到下一个没有满的page对象
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);//先将上面获取的page设置为当前页(hot)
return page->add(obj);//经过add函数将obj存入该page
}
复制代码
其实上面就是经过AutoreleasePoolPage
对象的child
指针去寻找下一个未满的page
。AutoreleasePoolPage
对象之间是经过child
和parent
指针造成的双向链表结构,就是为了在这个时候使用的。一样,在清空释放池对象的时候,若是当前释放池彻底空了,则会经过parent
指针去寻找上层的释放池。
除了系统在main
函数里加上的最初的一层@autoreleasepool {}
以外,有时候咱们本身的代码里面可能会也会使用@autoreleasepool {}
,方便对一些对象进行更为灵活的内存管理。那么咱们手动加的@autoreleasepool {}
确定是嵌套在main函数@autoreleasepool {}
内部的,至关于
int main(int argc, const char * argv[]) {
@autoreleasepool {//这是系统加的第一层
@autoreleasepool {}//这是咱们可能会添加的内层嵌套
}
}
复制代码
如今咱们再次来看一下这一次AutoreleasePoolPage::push();
会如何执行。一样程序会执行到autoreleaseFast(POOL_BOUNDARY);
POOL_BOUNDARY
会被传入autoreleaseFast
函数,而且也会经过add()
或者autoreleaseFullPage()
被添加到AutoreleasePoolPage
对象的页空间上。其实就是和普通的[obj autorelease]
的流程同样,只不过此次是obj
= POOL_BOUNDARY
,显然这是为了一个新的@autoreleasepool{}
作准备。
POOL_BOUNDARY
究竟是拿来干吗的呢?一会你就知道了。
分析完了源码,如今经过图例来展现一下
@autoreleasepool
的实现原理。 【假设】为方便展现每页AutoreleasePoolPage
只能存放3个释放对象,以下
这个问题就要搞清楚@autoreleasepool{}
的另外一半AutoreleasePoolPage::pop(atautoreleasepoolobj);
作了什么。一块儿来看一看其中的核心函数即是
releaseUntile(stop)
,这里的stop
实际上传入的就是POOL_BOUNDARY
,进入该函数
void releaseUntil(id *stop) {
while (this->next != stop) {//🥝若是next指向POOL_BOUNDARY,跳出循环🥝
//🥝拿到当前页
AutoreleasePoolPage *page = hotPage();
//🥝🥝当前页若是为空,经过parent拿到上一个AutoreleasePoolPage对象做为当前页
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
//🥝🥝🥝经过 --next 拿到当前页栈顶的对象
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
//🥝🥝🥝🥝若是obj不是POOL_BOUNDARY,就进行[obj release]
objc_release(obj);
}
}
setHotPage(this);
}
复制代码
pop()
核心步骤已经在上面函数里的注释体现出来。也就是说,当最内层的@autoreleasepool{}
做用域结束调用其对应的pop()
函数时,会从AutoreleasePoolPage
链表的当前页里面找到栈顶的对象,逐个开始释放,直到遇到POOL_BOUNDARY
就停下来,这样,就表明这一层的@autorelease{}
内所包含的全部对象都完成了release
方法调用。
当程序走到上一层的@autoreleasepool{}
做用域结束的地方,又回执行上面的流程,对其包含的对象一次调用release
方法。能够经过下图的示例来体会一下。
经过上面的研究,咱们知道@autoreleasepool{}
的做用,实际上就是在做用域的头和尾分别调用了objc_autoreleasePoolPush();
和objc_autoreleasePoolPop()
函数,可是在iOS项目当中,@autoreleasepool{}
的做用域是何时开始,何时结束呢?这就须要了解咱们以前研究过的另外一个知识点RunLoop。咱们知道,除非咱们手动启动子线程的RunLoop,不然程序里面只有主线程有RunLoop,这是系统默认开启的。下面咱们来看一下主线程的RunLoop肚子里都有什么宝贝。
咱们能够随便新建一个iOS项目,在ViewController
的viewDidLoad
方法里能够直接打印当前RunLoop对象(即主线程的RunLoop对象)
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
@end
复制代码
打印结果是洋洋洒洒的一大堆,若是你还不熟悉RunLoop的结构,能够参考个人Runloop的内部结构与运行原理,里面应该说的比较清楚了。咱们能够在打印结果的common mode items
部分,找到两个跟autorelease
相关的observer
,以下图所示 具体以下
<CFRunLoopObserver 0x600003f3c640 [0x10a2fdae8]>
{
valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d),
context =
<CFArray 0x6000000353b0 [0x10a2fdae8]>
{
type = mutable-small, count = 1, values = (0 : <0x7f91ff802058>)
}
}
<CFRunLoopObserver 0x600003f3c500 [0x10a2fdae8]>
{
valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d),
context =
<CFArray 0x6000000353b0 [0x10a2fdae8]>
{
type = mutable-small, count = 1, values = (0 : <0x7f91ff802058> )
}
}
复制代码
咱们能够看到,这两个监听器分监听的状态分别是
activities = 0xa0
(对应十进制的160
)activities = 0x1
(对应十进制的1
)这两个状态怎么解读呢?咱们能够在CF框架的RunLoop源码里面找到对应的定义
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),************十进制1---(进入loop)
kCFRunLoopBeforeTimers = (1UL << 1),****十进制2
kCFRunLoopBeforeSources = (1UL << 2),**十进制4
kCFRunLoopBeforeWaiting = (1UL << 5),***十进制32----(loop即将休眠)
kCFRunLoopAfterWaiting = (1UL << 6),*****十进制64
kCFRunLoopExit = (1UL << 7),**************十进制128----(退出loop)
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
复制代码
根据RunLoop状态的枚举值能够看出,160 = 128 + 32
,也就是说
activities = 0xa0
=(kCFRunLoopExit | kCFRunLoopBeforeWaiting)activities = 0x1
=(kCFRunLoopEntry)所以这三个状态被监听到的时候,就会调用_wrapRunLoopWithAutoreleasePoolHandler
函数。这个函数其实是按照下图的示意运做
objc_autoreleasePoolPush();
objc_autoreleasePoolPop()
,而后调用objc_autoreleasePoolPush();
objc_autoreleasePoolPop()
根据上面的分析,咱们能够总结,除了程序启动(对应kCFRunLoopEntry)和程序退出(对应kCFRunLoopExit)会调用一次objc_autoreleasePoolPush();
和objc_autoreleasePoolPop()
外,程序的运行过程当中,每当RunLoop即将休眠,被observer
监听到kCFRunLoopBeforeWaiting状态时,会先调用一次objc_autoreleasePoolPop()
,这样就将当前的autoreleasepool
里面的对象逐个调用release
方法,至关于清空释放池子;紧接着再调用一次objc_autoreleasePoolPush();
,至关于开启一个新的释放池,等待RunLoop醒来后的下一次循环使用。
自动释放池的对象何时会被调用release方法呢?
RunLoop的每一圈循环过程当中,调用过autorelease
方法的对象(也就是被加入AutoreleasePoolPage
的对象),会在当次循环即将进入休眠状态的时候,被调用release
方法,也能够说是被释放了。
好了,AutoreleasePool的原理以及它和RunLoop的关系就分析到这里。