写在前面
内存管理一直是Objective-C 的重点,在MRC环境下,通过调用[obj autorelease]来延迟内存的释放,在现在ARC环境下,我们都知道编译器会在合适的地方插入release/autorelease内存释放语句,我们甚至可以不需要知道Autorelease就能很好的管理内存。虽然现在已经几乎用不到MRC,但是了解 Objective-C 的内存管理机制仍然是十分必要的,看看编译器帮助我们怎么来管理内存。本文仅仅是记录自己的学习笔记。
AutoreleasePool简介
1.什么是AutoreleasePool
AutoreleasePool:自动释放池是 Objective-C 开发中的一种自动内存回收管理的机制,为了替代开发人员手动管理内存,实质上是使用编译器在适当的位置插入release、autorelease等内存释放操作。当对象调用 autorelease方法后会被放到自动释放池中延迟释放时机,当缓存池需要清除dealloc时,会向这些 Autoreleased对象做 release 释放操作。
2.对象什么时候释放(ARC规则)
一般的说法是对象会在当前作用域大括号结束时释放, 有这样一个ARC环境下简单的例子🌰:首先创建一个ZHPerson类:
//// ZHPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ZHPerson : NSObject
+(instancetype)object;
@end
////ZHPerson.m
#import "ZHPerson.h"
@implementation ZHPerson
-(void)dealloc
{
NSLog(@"ZHPerson dealloc");
}
+(instancetype)object
{
return [[ZHPerson alloc] init];
}
@end
然后在ViewController.m导入头文件ZHPerson.h,然后在写一段这样的代码:
__weak id temp = nil;
{
ZHPerson *person = [[ZHPerson alloc] init];
temp = person;
}
NSLog(@"temp = %@",temp);
解释一下这个代码:先声明了一个 __weak 变量temp,因为 __weak 变量有一个特性就是它不会影响所指向对象的生命周期,然后让变量temp指向创建的person对象,输出如下:
这里超出了person的作用域,它就被释放了,看来是正常的。
把上面的创建对象的方法,变一变写法:
__weak id temp = nil;
{
ZHPerson *person = [ZHPerson object];
temp = person;
}
NSLog(@"temp = %@",temp);
输出如下:
这里person对象超出了其作用域还是存在的,被延迟释放了,也就是说其内部调用了autorelease方法。
小总结:
查询得知:以 alloc, copy, ,mutableCopy和new这些方法会被默认标记为 __attribute((ns_returns_retained)) ,以这些方法创建的对象,编译器在会在调用方法外围要加上内存管理代码retain/release,所以其在作用域结束的时候就会释放,而不以这些关键字开头的方法,会被默认标记为__attribute((ns_returns_not_retained)),编译器会在方法内部自动加上autorelease方法,这时创建的对象就会被注册到自动释放池中,同时其释放会延迟,等到自动释放池销毁的时候才释放。
3.AutoreleasePool的显示创建
1.MRC下的创建
//1.生成一个NSAutoreleasePool对象
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//2.创建对象
id object = [[NSObject alloc] init];
//3.对象调用autorelease方法
[object autorelease];
//4.废弃NSAutoreleasePool对象,会对释放池中的object发送release消息
[pool drain];
2.ARC下的创建
@autoreleasepool {
//LLVM会在内部插入autorelease方法
id object = [[NSObject alloc] init];
}
AutoreleasePool 的作用前面有提到过,每当一个对象调用 autorelease方法时,实际上是将该对象放入当前 AutoreleasePool 中,当前AutoreleasePool 释放时,会对添加进该 AutoreleasePool 中的对象逐一调用 release 方法。在ARC环境下,并不需要特别的去关注Autoreleasepool的使用,因为系统已经做了处理。
AutoreleasePool探索学习
为了看一下AutoreleasePool到底做了什么,先来创建一个main.m文件(Xcode -> File -> New Project -> macOS -> Command Line Tool -> main.m);
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
然后,使用编译器clang编译main.m转化成main.cpp文件(在终端使用命令:clang -rewrite-objc main.m),滑到main.cpp文件的最后,有这样一段代码:
这个代码是把@autoreleasePool转换成一个__AtAutoreleasePool类型的局部私有变量__AtAutoreleasePool __autoreleasepool;
接着在 main.cpp文件中查询__AtAutoreleasePool,来看一下它具体的实现:
可以看到__AtAutoreleasePool是结构体类型,并且实现了两个函数:构造函数__AtAutoreleasePool()和析构函数~__AtAutoreleasePool()。
也就是说在声明 __autoreleasepool 变量时,构造函数 __AtAutoreleasePool() 被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;当出了当前作用域时,析构函数 ~__AtAutoreleasePool()被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj); 那么上面的main.m中的代码可以用这种形式代替:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
// @autoreleasepool
{
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// insert code here...
NSLog(@"Hello, World!");
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
接下来看一下析构函数和构造函数分别实现了什么内容?这里需要一份objc_runtime的源码(源码地址),这里使用的是objc4-756.2.tar.gz:
这里两个函数本质上就是分别调用了AutoreleasePoolPage的push方法和pop方法(这里::是C++调用方法的形式,类似于点语法)。
1.AutoreleasePoolPage
AutoreleasePoolPage是一个C++实现的类,它的具体实现代码是:
class AutoreleasePoolPage
{
# define POOL_BOUNDARY nil //哨兵对象(可以看做是一个边界)
static size_t const COUNT = SIZE / sizeof(id); // 对象数量
magic_t const magic; //用来校验 `AutoreleasePoolPage`的结构是否完整;
id *next; //指向最新添加的 `autoreleased` 对象的下一个位置,初始化时指向 `begin()` ;
pthread_t const thread; //指向当前线程;
AutoreleasePoolPage * const parent; //指向父结点,第一个结点的 `parent` 值为 `nil` ;
AutoreleasePoolPage *child; //指向子结点,最后一个结点的 `child` 值为 `nil` ;
uint32_t const depth; //代表深度,从 `0` 开始,往后递增 `1`;
uint32_t hiwat; //代表 `high water mark` ;
//剩下代码省略......
}
通过源码可以知道这是一个典型的双向列表结构,所以AutoreleasePool是由若干个AutoreleasePoolPage以双向链表的形式组合而成。
AutoreleasePoolPage每个对象会开辟4096字节内存(虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址,AutoreleasepoolPage 通过压栈的方式来存储每个autorelease的对象(从低地址到高地址)。其中next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置,当 next指针指向begin时,表示 AutoreleasePoolPage为空;当 next指针指向end时,表示 AutoreleasePoolPage 已满,此时会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的AutoreleasePoolPage插入,同样的新AutoreleasePoolPage的next指针被初始化在栈底(指向begin的位置)。
2.AutoreleasePoolPage::push()
既然已经知道了autorelease的对象会通过压栈的方式插入到AutoreleasePoolPage当中,那么显然AutoreleasePoolPage的push方法就承包了AutoreleasePoolPage的创建和插入。
接着看下push方法的源码:
static inline void *push()
{
id *dest;
//判断是否已经初始化AutoreleasePoolPage
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
这里的POOL_BOUNDARY可以理解为哨兵对象,或者理解为一种边界标识,而且这个POOL_BOUNDARY值为0,是个nil。
接下来,先来看一下autoreleaseFast这个方法,
static inline id *autoreleaseFast(id obj)
{
//获取到当前page,这个hotPage是从当前线程的局部私有空间取出来的
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
我们知道链表是有空间的,所以上面👆的源码可以理解为:
(1). 当前page存在且没有满时,直接将对象添加到当前page中,即next指向的位置;
(2). 当前page存在并且已满时,创建一个新的page,并将对象添加到新创建的page 中,然后将这两个链表节点进行链接。
(3). 当前page不存在时,创建第一个page ,并将对象添加到新创建的page中。
这里重点看一下page->add(obj)这个方法,
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
可以看到这里返回的ret其实next指针指向的地址,由上面的push方法的源码可知,这里page->add(obj)传入的obj其实就是POOL_BOUNDARY,也就是说每一次调用push方法,都会插入一个POOL_BOUNDARY,所以objc_autoreleasePoolPush的返回值就是这个哨兵对象的地址。
3.AutoreleasePoolPage::pop(ctxt)
通过上面对构造函数objc_autoreleasePoolPush的学习,已经知道objc_autoreleasePoolPush返回的是哨兵对象的地址,那么在调用析构函数objc_autoreleasePoolPop的时候传入的也就是这个哨兵对象的地址。随着方法的一步步调用,紧接着来看下AutoreleasePoolPage的pop方法的实现:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
page = pageForPointer(token); //根据传入的哨兵对象的地址,获取到page中的哨兵对象之后的地址空间
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop); //对当前链表当中的对象进行release操作
if (DebugPoolAllocation && page->empty()) {
//释放 `Autoreleased` 对象后,销毁多余的 page
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
page->kill();
setHotPage(nil);
}
else if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
这里重点看一下page->releaseUntil(stop)方法:
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);
}
这里的stop同样是POOL_BOUNDARY的地址,这里分析一下这个方法:
(1). 外部循环挨个遍历autoreleased 对象,直到遍历到哨兵对象POOL_BOUNDARY。
(2). 如果当前page没有 POOL_BOUNDARY,并且为空,则将hotPage设置为当前page的父节点。
(3). 给当前autoreleased对象发送release消息。
(4). 最后再次配置hotPage。
4.autorelease
通过上面的分析已经知道了构造方法objc_autoreleasePoolPush会创建AutoreleasePoolPage,并插入哨兵对象POOL_BOUNDARY,析构方法objc_autoreleasePoolPop会对哨兵对象之后插入的对象发送release消息,那么在这两个方法之间,对象通过调用autorelease是怎么插入到AutoreleasePoolPage的呢?下面来看下autorelease的源码实现:
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;
}
这里的重点还是autoreleaseFast(obj);由于这里插入对象的方法和AutoreleasePoolPage调用push方法的实现是一样的,只不过push操作插入的是一个 POOL_BOUNDARY,而autorelease操作插入的是一个具体的autoreleased对象,在此处就不做多余分析。
通过上面👆的这些分析,已经大概知道AutoreleasePool是怎样的一个构造,以及内如是如何实现自动释放的。
5.AutoreleasePool的嵌套
对于嵌套的AutoreleasePool也是同样的原理,在pop的时候总会释放对象到上次push的位置为止,也就是哨兵位置,多层的pool就是插入多个哨兵对象而已,然后根据哨兵对象来进行释放,就像剥洋葱一样一层一层的,互不影响。
那么这里有个疑问,如果在AutoreleasePool多层嵌套中是同一个对象呢,那么会怎么释放?下面通过一个小例子🌰来看一下:
@autoreleasepool {
ZHPerson *person = [ZHPerson object];
NSLog(@"current count %d",_objc_rootRetainCount(person));
@autoreleasepool {
ZHPerson *person1 = person;
NSLog(@"current count %d",_objc_rootRetainCount(person));
@autoreleasepool {
ZHPerson *person2 = person;
NSLog(@"current count %d",_objc_rootRetainCount(person));
}
}
}
打印结果如下:
这里dealloc方法只调用了一次,由上面的代码可知:当前person1和person2是对person的引用,如果系统会为每一次引用都自动插入一个autorelease,那么对象在执行第一个autorelease的时候,会调用objc_release(obj)来释放当前的对象,那么当调用rootRelease()的时候就会报错,因为当前对象已经被释放了,那么也就是说对于引用的对象只会被释放一次。(同一个对象不能够反复的autorelease)
NSthread、NSRunLoop、AutoReleasePool
1.NSthread和AutoReleasePool
先来看个简单的例子:
在temp的位置设置一个断点,然后在控制台输入watchpoint set variable temp,
等到这个线程执行结束之后,来看一下左侧边栏的内容:
当执行到NSLog(@”thread end”);这句代码,表示线程执行结束,这里,其实线程会先调用[NSthread exit],然后执行_pthread_tsd_cleanup,清除当前线程的有关资源,然后调用tls_dealloc,也就是把当前线程关联的AutoReleasePool释放掉,最后调用weak_clear_no_lock清除指针。
那么这一系列过程就说明了:在NSThread退出了之后,与NSThread对应的AutoReleasePool也会被自动清空,所以当一个线程结束的时候,就会回收♻️AutoReleasePool中自动释放的对象。
总结
每一个线程都会维护自己的AutoReleasePool,而每一个AutoReleasePool都会对应唯一一个线程,但是线程可以对应多个AutoReleasePool。
2.NSRunLoop和AutoReleasePool
对于NSThread只是一个简单的线程,如果把它换成一个常驻线程呢?
这里创建一个NSTimer,并将其常驻。利用同样的方式,watchpoint set variable temp,:
可以看到这里NStimer是被加入到子线程当中的,但是在子线程中,我们并没有去写关于AutoReleasePool的内容,我们只知道test做了autorelease操作。下面回到源码中来看:
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;
}
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);
}
}
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
}
//这里省略了部分代码
所以从上面的源码我们可以得出结论:子线程在使用autorelease对象的时候,会懒加载出来一个AutoreleasePoolPage,然后将对象插入进去。
那么问题又来了,autorelease对象在什么时候释放的呢?也就说AutoreleasePoolPage在什么时候调用了pop方法?
其实在上面创建一个NSThread的时候,在调用[NSthread exit]的时候,会释放当前资源,也就是把当前线程关联的autoReleasePool释放掉,而在这里当RunLoop执行完成退出的时候,也会执行pop方法,这就说明了为什么在子线程当中,我们没有显示的调用pop,它也能释放当前AutoreleasePool的资源的原因。
3.主线程的NSRunLoop和AutoReleasePool
那么在主线程的RunLoop到底什么时候把对象进行释放回收的呢?
简单粗暴点,直接在控制台通过po [NSRunloop currentRunloop]打印主线程的RunLoop:
这里,系统在主线程的RunLoop里注册了两个Observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler,第一个Observer的状态是activities = 0x1,第二个Observer的状态是activities = 0xa0,这两种状态代表什么意思呢?
先在这里插入一点RunLoop的内容(RunLoop的状态枚举):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1
kCFRunLoopBeforeTimers = (1UL << 1), // 2
kCFRunLoopBeforeSources = (1UL << 2), // 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 32
kCFRunLoopAfterWaiting = (1UL << 6), // 64
kCFRunLoopExit = (1UL << 7), // 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
0x1代表的是kCFRunLoopEntry,也就是说第一个 Observer监视的事件是Entry(即将进入Loop时),其回调内会调用_objc_autoreleasePoolPush()创建一个自动释放池。其order优先级是-2147483647,优先级最高,保证创建自动释放池发生在其他所有回调之前。
0xa0对应的是kCFRunLoopBeforeWaiting和kCFRunLoopExit,也就是说第二个Observer监视了两个事件:kCFRunLoopBeforeWaiting准备进入休眠,kCFRunLoopExit即将退出RunLoop。在kCFRunLoopBeforeWaiting事件时调用 _objc_autoreleasePoolPop()和_objc_autoreleasePoolPush() 释放旧的自动释放池并创建新的自动释放池;在kCFRunLoopExit事件时调用_objc_autoreleasePoolPop() 来释放自动释放池,同时这个Observer的order优先级是 2147483647,优先级最低,保证其释放自动释放池的操作发生在其他所有回调之后。
所以在没有手动增加AutoreleasePool的情况下,Autorelease对象都是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池push和pop操作。
总结
对于不同线程,应当创建自己的AutoReleasePool。如果应用长期存在,应该定期drain和创建新的AutoReleasePool,AutoReleasePool与RunLoop 与线程是一一对应的关系,AutoReleasePool在RunLoop在开始迭代时做push操作,在RunLoop休眠或者迭代结束时做pop操作。
AutoreleasePool的应用场景
通常情况下我们是不需要手动创建AutoreleasePool,但是也有一些特殊的:
编写的程序不基于UI框架,如命令行程序。
在循环中创建大量临时对象时用以降低内存占用峰值。
在主线程之外创建新的线程,在新线程开始执行处,创建自己的AutoreleasePool,否则将导致内存泄漏。
下面就来简单看下第二种情况,直接来个for循环:
for (int i = 0; i < 100000000; i ++) {
NSString * str = [NSString stringWithFormat:@"noAutoReleasePool"];
NSString *tempstr = str;
}
}
来看一下Memory的使用情况:
这个对比伤害就很明显了。
这个做个备注:在主函数main.m文件中的@autoreleasepool,如果在这里做个测试,使用for循环创建大量的临时对象,是否加上这个@autoreleasepool对Memory的使用情况没有特别大的影响。