运用
运用场景
在ARC
下,AutoreleasePool
主要应用在大量创立临时目标的场景,经过AutoreleasePool
操控内存峰值,是一个很好的挑选。
NSAutoreleasePool
在MRC
能够调用NSAutoreleasePool
使目标延迟开释,在ARC
下这个API
现已被禁用。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// ...
[pool release];
@autoreleasepool
除了NSAutoreleasePool
还能够运用@autoreleasepool
,而且苹果推荐运用@autoreleasepool
,由于这个API
功能更好,在ARC
下仍然能够运用@autoreleasepool
。
无论是MRC
还是ARC
,autorelease
最大的作用,是在大量创立目标的同时,经过润饰让内存得到提前开释,从而下降内存峰值。
@autoreleasepool {
NSMutableArray *channelItemsJSONArray = [NSMutableArray arrayWithContentsOfFile:[self cachedChannelItemsFile]];
NSArray *items = [self channelItemsJSONArray];
if (![items writeToFile:[self cachedChannelItemsFile] atomically:YES]) {
[channelItemsJSONArray writeToFile:[self cachedChannelItemsFile] atomically:YES];
}
items = nil;
}
__autoreleasing
在ARC
下,需求被主动开释的目标,能够用__autoreleasing
润饰,让目标延迟开释。
+ (NSArray *)parseString:(NSString *)originalM3U8Str m3u8Host:(NSString *)m3u8url error:(NSError *__autoreleasing *)errorPtr;
源码分析
__AtAutoreleasePool结构体
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
@autoreleasepool
本质上会被体系转换成C++
的__AtAutoreleasePool
结构体,@autoreleasepool
的大括号开端,对应着objc_autoreleasePoolPush
函数。大括号完毕,对应着objc_autoreleasePoolPop
函数。经过clang
指令将OC
代码转成C++
代码,能够看到有一个__AtAutoreleasePool
结构体。
__AtAutoreleasePool
结构体在创立的时分会履行objc_autoreleasePoolPush
函数,在开释的时分会履行析构函数,并履行objc_autoreleasePoolPop
函数。在这两个函数内部,会调用AutoreleasePoolPage
的push
和pop
函数。
AutoreleasePoolPage
在运转时代码中,objc_autoreleasePoolPop
和objc_autoreleasePoolPush
,都调用了AutoreleasePoolPage
类的实现。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
在AutoreleasePoolPage
的界说中,能够看到有parent
和child
的界说,当page
中目标太多存储不下时,会创立其他的page
目标来存储,AutoreleasePoolPage
的结构是一个双向链表。在刺进新的autorelease
目标时,也会从链表头向后查找,直到找到未满的page
。
class AutoreleasePoolPage
{
magic_t const magic; // 校验page的结构是否完好
id *next; // 指向下一个能够存放autorelease目标的地址
pthread_t const thread; // 当时地点的线程
AutoreleasePoolPage * const parent; // 当时page的父节点
AutoreleasePoolPage *child; // 当时page的子节点
uint32_t const depth; // page的深度
uint32_t hiwat;
}
AutoreleasePoolPage
是一个C++
的类,每个page
占4096
个字节,也便是16
进制的0x1000
,也便是4kb
的空间。这些空间中,其本身的成员变量只占56
个字节,也便是下面七个成员变量,每个占8
字节,一共56
个字节。其他的四千多个字节,都用来存放被autorelease
润饰的目标内存地址。
POOL_BOUNDARY
POOL_BOUNDARY
的作用是,区别不同的主动开释池,也便是不同的@autoreleasepool
。调用push
时,会传入POOL_BOUNDARY
并返回一个地址例如0x1038
,0x1038
是不存储@autorelease
目标的地址的,起到一个标识作用,用来切割不同的@autoreleasepool
。
调用pop
时,会传入end
的地址,并从后到前调用目标的release
办法,直到POOL_BOUNDARY
停止。假如存在多个page
,会从child
的page
的最结尾开端调用,直到POOL_BOUNDARY
。page
的结构是一个栈结构,开释的时分也是从栈顶开端开释。
next
指针指向栈顶,是栈里边很常见的一个设计。AutoreleasePoolPage
和POOL_BOUNDARY
的差异在于,AutoreleasePoolPage
担任保护存储区域,而POOL_BOUNDARY
则担任切割存储在page
中的目标地址,以@autoreleasepool
为单位进行切割。
多层嵌套
@autoreleasepool {
NSObject *p1 = [[NSObject alloc] init];
NSObject *p2 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p3 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p4 = [[NSObject alloc] init];
}
}
}
假如是多层@autoreleasepool
的嵌套,会用同一个AutoreleasePoolPage
目标。以下面的三个嵌套为例,在同一个page
中的顺序是下图这样。不同的@autoreleasepool
以POOL_BOUNDARY
做切割。
push
创立一个autoreleasePool
之后,就会调用push
函数。在push
函数中会判别是否调试模式下,假如调试模式会每次生成一个新的page
。debug
环境代码能够直接疏忽,只保存autoreleaseFast
函数。
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
return dest;
}
autoreleaseFast
在函数内部,会经过hotPage
获取当时的page
,hotPage
函数内部本质上是一个page
和key
的映射。
- 假如
page
不为空而且有空间,则调用page
的add
函数将目标添加到page
中,并将POOL_BOUNDARY
添加在当时的方位。 - 假如
page
现已被创立但没有空间,会调用autoreleaseFullPage
函数创立新的page
,而且将链表的结尾指向新创立的page
。 - 假如没有创立
page
,则调用autoreleaseNoPage
函数创立一个新的page
,而且将当时线程的hotPage
设置为新创立的page
。
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);
}
}
autoreleaseFullPage
- 在
autoreleaseFullPage
函数中,会从page
的链表中,从前往后找到结尾的节点。 - 创立一个新的
page
,在创立函数AutoreleasePoolPage
中会处理parent
和child
指针的问题,返回的page
能够直接用。 - 调用
setHotPage
将page
设置到哈希表中,而且调用page
的add
函数将autorelease
润饰的目标,添加到page
中。
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
autoreleaseNoPage
autoreleaseNoPage
函数的中心代码比较简单,便是创立一个新的page
,随后设置POOL_BOUNDARY
标志,而且把目标添加进去。在函数中需求留心POOL_BOUNDARY
标志,很多当地都用来做page
是否为空的判别。
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
return page->add(obj);
}
add
add
函数比较简单,中心逻辑便是将obj
放入next
指针的方位,而且对next
指针进行++
,指向下一个方位。*next++
表明先用后加,先将obj
存入next的地址,随后+1
。
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}
pop
调用pop
函数时,有三步处理。
- 判别
autoreleasepool
是否为空,经过EMPTY_POOL_PLACEHOLDER
占位符判别,为空则清空这个page
。 - 传入的
stop
是否不等于POOL_BOUNDARY
标识,假如不等于则可能是一个有问题的page
。 - 调用
popPage
办法,开释目标。
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
// 1.
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
page = hotPage();
if (!page) {
return setHotPage(nil);
}
page = coldPage();
token = page->begin();
} else {
page = pageForPointer(token);
}
// 2.
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
// 3.
return popPage<false>(token, page, stop);
}
popPage
-
popPage
函数中心代码便是调用releaseUntil
函数,在最开端会调用releaseUntil
函数去完结开释操作。 - 依照
page
达到一半就扩容的原则,后边的if
语句会判别履行pop
后page
链表的状况。- 假如少于半满,就将子节点删去。
- 假如大于半满,则保存子节点,并删去后边的节点。
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
releaseUntil
在releaseUntil
函数内部,中心逻辑是从当时page
,从后到前调用objc_release
,开释被autorelease
润饰的目标。
- 获取当时的
hotPage
。 - 判别
page
是否为空,假如为空则表明里边的目标被开释完,则将page
的父节点page
设置为hotPage
。 - 获得上一个节点,
->
的管用优先级比--
要高,所以是先经过next
获取当时节点地址,这是一个为空的待存入节点,随后履行--
操作获取上一个目标地址。 - 经过
memset
将上一个节点开释。 - 判别上一个节点是否占位符号
POOL_BOUNDARY
,假如不是则调用objc_release
开释目标。 - 在
while
循环完毕后,将当时page
设置为hotPage
。
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
autorelease
目标调用autorelease
办法会被编译器转换为objc_autoreleaseReturnValue
办法,而且经过多层调用,会来到底层的autorelease
函数。
在这个函数中会判别传入的目标是否tagged pointer
,由于tagged pointer
没有引证计数的概念。随后会调用autoreleaseFast
函数,函数内部调用add
函数将obj
目标加入到page
中,而且会判别是否需求创立新的page
。
static inline id autorelease(id obj)
{
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
return obj;
}
hotPage、coldPage
hotPage
hotPage
能够被理解为,page
链表的结尾,也便是调用push
函数被刺进的方位。履行hotPage
函数获取,以及调用setHotPage
设置,都是操作的链表的结尾page
。
AutoreleasePoolPage
目标和线程一一对应,而且都被存储在tls
的哈希表中。经过tls_get_direct
函数并传入key
能够获取到对应的主动开释池。
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
hotPage
函数中的判别是下面的界说,这个标示意思是当时page
为空,也便是从未存储过任何目标。是一个标志位,下面是标志位的界说。
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
coldPage
coldPage
只有获取函数,没有设置函数。这是由于coldPage
函数本质上,便是寻觅page
链表的根节点,从源码中的while
循环能够看到。
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
调试
_objc_autoreleasePoolPrint
假如想调试主动开释池,能够经过_objc_autoreleasePoolPrint
私有API
来进行。将项目改为MRC
,而且在指令行项目中增加下面这些调试代码。
int main(int argc, const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool {
_objc_autoreleasePoolPrint(); // print2
Person *p1 = [[[Person alloc] init] autorelease];
Person *p2 = [[[Person alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
}
_objc_autoreleasePoolPrint(); // print4
return 0;
}
打印结果如下,能够看到POOL_BOUNDARY
在page
中也占了一个方位。
objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 0 releases pending. // 当时主动开释池中没有任何目标
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: ##############
objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 1 releases pending. // 当时主动开释池中有1个目标,这个目标为POOL_BOUNDARY
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: ##############
objc[68122]: ############## (print3)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 3 releases pending. // 当时主动开释池中有3个目标
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: [0x102802040] 0x100704a10 HTPerson //p1
objc[68122]: [0x102802048] 0x10075cc30 HTPerson //p2
objc[68122]: ##############
objc[68156]: ############## (print4)
objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68156]: 0 releases pending. // 当时主动开释池中没有任何目标,由于@autoreleasepool作用域完毕,调用pop办法开释了目标
objc[68156]: [0x100810000] ................ PAGE (hot) (cold)
objc[68156]: ##############
UIApplicationMain
项目中经常会看到下面的代码,很多人的解说是“这个autoreleasepool
是为了开释主线程的autorelease
目标的”。但是,这个说法是错误的。autoreleasepool
只担任自己作用域中添加的目标,而主线程在运转过程中,也会隐式创立autoreleasepool
目标,这个pool
是包含在main
函数的pool
里边的。
所以,主线程runloop
每次履行循环后,开释的目标是主线程的。而main
函数的autoreleasepool
开释的,是main
函数中直接创立的目标。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
开释机遇
区别
假如是在viewDidLoad
办法中创立一个autorelease
目标,并不是在这个办法完毕后开释目标,这个说法是错误的。即使履行到viewDidAppear
,仍然不会开释目标。
被autorelease
润饰的目标,开释机遇有两种。
- 假如经过代码添加一个
autoreleasepool
,在作用域完毕时,随着pool
的开释,就会开释pool
中的目标。这种情况是及时开释的,并不依赖于runloop
。 - 另一种便是由体系主动进行开释,体系会在
runloop
开端的时分创立一个pool
,完毕的时分会对pool
中的目标履行release
操作。
runloop
假如是体系创立的pool
,需求手动开启runloop
,主线程默认现已开启并运转,子线程需求调用currentRunLoop
办法开启并运转runloop
,子线程中体系创立pool
的流程才会正常工作。
包括主线程在内的每个线程,假如在线程中运用到了AutoreleasePool
,则会创立两个Observer
并添加到当时线程的Runloop
中,经过这两个Observer
进行目标的主动内存办理。
// activities = 0x1,kCFRunLoopEntry
<CFRunLoopObserver 0x60000012f000 [0x1135c2bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
// activities = 0xa0,kCFRunLoopBeforeWaiting | kCFRunLoopExit
<CFRunLoopObserver 0x60000012ef60 [0x1135c2bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
首先会创立一个Observer
并监听kCFRunLoopEntry
消息,机遇是在进入Runloop
前,此Observer
的优先级设置为-2147483647
的最高优先级,以确保回调发生在Runloop
其他事件前。
然后创立另一个Observer
,并监听kCFRunLoopBeforeWaiting
和kCFRunLoopExit
消息,机遇分别在进入Runloop
休眠和退出Runloop
时,将Observer
的优先级设置为2147483647
,以确保回调发生在Runloop
其他事件之后。
两个Observer
都有相同的回调函数_wrapRunLoopWithAutoreleasePoolHandler
,在第一次回调时会在内部调用_objc_autoreleasePoolPush
函数,创立主动开释池。
在kCFRunLoopBeforeWaiting
即将进入休眠前,调用_objc_autoreleasePoolPop
函数开释主动开释池中的目标,并调用_objc_autoreleasePoolPush
函数创立一个新的开释池。在kCFRunLoopExit
即将退出Runloop
时调用_objc_autoreleasePoolPop
函数,开释主动开释池中的目标。