IOS-AutorealeasePool 解决上次面试留下的坑
这篇算是解决以前留下的坑吧,这块之前看过不少东西,但是看过也只是看过没有正儿八经的研究过,最直接的就是上次去优信面试被问的贼尴尬。
先扔个问题:
第一个
for (int i = 0; i < 100000000; i++) {
UIImage *image = [UIImage imageNamed:@"logo"];
[self.muarr addObject:image];
}
第二个
for (int i = 0; i < 100000000; i++) {
@autoreleasepool{
UIImage *image = [UIImage imageNamed:@"logo"];
[self.muarr addObject:image];
}
}
第一个跟第二个的区别是什么?内存会发生什么变化?
实际测试结果,第一个跑完占用的内存是第二个的两倍。
那为什么呢?
答案在这里,加了autoreleasepool后,每当一个循环跑完,里面的临时变量image就会被释放掉,所以跑完内存会小很多。
剩下的就剩下刨根问底了,走起吧!
autoreleasepool{}的本地函数是什么样的?
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
这就是autoreleasepool函数原来的面目,两部,push 和 release操作。
然后我注意到了这个 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;
};
magic是用于对当前 autoreleasepoolpage的完整性的校验,
thread是当前所在线程
每个自动释放池都由好多个autoreleasepoolpage组成的
从别人那搞的图片看看吧! 其中parent 和 child 就很好理解了,这两个就是构造双向链表的指针。
说了这么多还是看下push和 pop是怎么实现的吧。
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
static inline void *push() {
return autoreleaseFast(POOL_SENTINEL);
}
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 inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
上面的add过程说白了就是压栈的过程。每当对象调用autorelease方法的时候,都会将对象加入到* AutoreleasePoolPage*栈中。
调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息
autoreleasepool什么时候释放?
我最开始的理解也是在括号结束之后才会去释放,但是忘了一点,runloop,
系统在每个runloop中都会加入 push 和 pop的监听,。所以肯定是在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,优先级最低,保证其释放池子发生在其他所有回调之后。
所以 _objc_autoreleasePoolPush 和 _objc_autoreleasePoolPop 的优先级肯定是最高的,以保证能正确的push和pop
看完上面不知道能理解多少,对于博客还是写的有点乱。
看看下面这个问题
- (void)viewDidLoad {
[super viewDidLoad];
//Init
_memoryUsageList1 = [NSMutableArray new];
//创建一个串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("test.autoreleasepool", DISPATCH_QUEUE_SERIAL);
__block NSString *strTest;
dispatch_sync(serialQueue, ^{
for (int i = 0; i < kIterationCount; i++) {
@autoreleasepool {
NSNumber *num = [NSNumber numberWithInt:i]; // 1
NSString *str = [NSString stringWithFormat:@"%d ", i]; // 2
//Use num and str...whatever...
strTest = [NSString stringWithFormat:@"%@%@", num, str]; // 3
if (i % kStep == 0) {
[_memoryUsageList1 addObject:@(getMemoryUsage())]; // getM方法是获取内存的函数
NSLog(@"000----%f", getMemoryUsage());
}
}
}
});
} // 4
看上面 1 2 都是临时变量,所以在一次循环之后就会被释放掉。
这是我刚开始的理解(而strTest 的作用域是viewdidload,所以每次循环,strTest的指针都指向了一个新的对象,但是原来的对象没有被释放掉,所以内存就会一直增加。)感觉有不对的地方。
然后我看到这句话
自己创建的对象:使用 alloc new copy mutablecopy 以及他们的驼峰变形 allocObject newObject copyObject mutablecopyObject。这八种创建的才是自己创建的对象。
不是自己创建的:除去以上八中都不是自己创建的。
autoreleasepool:只有非自己创建的对象才会注册到离该对象最近的autoreleasepool中去。
也就是说 [NSString stringWithFormat:@"%@%@", num, str] 不会自动加入到自动释放池中,这时候就体现了我们自己加的这个autoreleasepool的作用了,强行给他加了一个作用域,所以每当一个autoreleasepool结束的时候, [NSString stringWithFormat:@"%@%@", num, str] 就会被释放掉。