本文又到了内存管理部分的最后一篇了,主要分享一下面试中常被问到的copy和autorelease这两个知识点。
copy
先说copy的目的是产生一个副本,而副本的特点是跟原对象互不影响的,就是修改了原对象不会影响副本对象,修改了副本对象不会影响原对象。
可变和不可变
iOS提供了两个拷贝方法:
- copy:不可变拷贝,产生不可变副本
- mutableCopy:可变拷贝,产生可变副本
了解了副本的特点以及两种拷贝方式,我们再继续看看iOS自带的几种带copy功能的对象。比如常见的NSArray,NSDictionary,NSString。
NSString *str = [NSString stringWithFormat:@"test"];
// 指针copy 浅拷贝 没有产生新的对象
NSMutableString *str1 = [str copy];//返回的是NSString 类型改为NSMutableString 调用appendFormat
// 深拷贝 内容拷贝 产生了新的对象
NSMutableString *str2 = [str mutableCopy];//返回的是NSMutableString
可以通过打印类型或者调用appendFormat来分别测试str1和str2是一个可变副本还是不可变副本,他们的类型跟定义的NSString或者NSMutableString无关。比如str1类型给为NSMutableString,再调用appendFormat也依然会crash。
这里最终的结论是, str不管是不是可变的, 副本是否可变取决于是调用了copy还是mutableCopy。
深拷贝和浅拷贝
下面说几个常见的面试题:
问题1:比如给一段代码,问你是深拷贝还是浅拷贝?
上面代码注释中也写过了,这里再总结一下:
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,不产生新的对象
换言之,深浅拷贝的区别就在于是否产生新的对象。
还是用str为例:
NSString *str = [NSString stringWithFormat:@"testtesttesttesttest"];
// 指针copy 浅拷贝 没有产生新的对象
NSMutableString *str1 = [str copy];//返回的是NSString 类型改为NSMutableString 调用appendFormat
// 深拷贝 内容拷贝 产生了新的对象
NSMutableString *str2 = [str mutableCopy];//返回的是NSMutableString
[str2 appendFormat:@"123"];
NSLog(@"%p %p %p",str , str1 , str2);//0xcbc7dd038bd1ff60 0xcbc7dd038bd1ff60 0x600002210900
通过打印地址也可以证明,str1是浅拷贝,str2是深拷贝。这里还有一个问题需要注意,str 和 str1内存地址相同 , 因为str是个不可变的字符串, 他的内容是没法改变的 , 所以指向同一块内存也可以达到copy的目的 ,互不影响,因为两者都无法改变不存在改的操作,所以指向同一块内存 ,就算再分配一块内存也没有意义还浪费内存。而str2是可变的字符串,所以必须是新的一块内存。
再看一段代码:
NSMutableString *str = [NSMutableString stringWithFormat:@"test"];
//深拷贝
NSString *str1 = [str copy];
//深拷贝
NSMutableString *str2 = [str mutableCopy];
NSLog(@"%p %p %p",str , str1 , str2);//0x600002601cb0 0xb76a50e438a02845 0x600002601c80
这次,str1和str2都是深拷贝。
继续看代码:
NSArray *array1 = [[NSArray alloc] initWithObjects:@"a",@"b", nil];
NSArray *array2 = [array1 copy];//浅拷贝
NSMutableArray *array3 = [array1 mutableCopy];//深拷贝
NSLog(@"%p %p %p",array1 , array2 , array3);//0x6000037cb780 0x6000037cb780 0x600003981f20
我们把每一种可变不可变的拷贝都测试一遍,然后分别打印地址查看是否相同
NSMutableArray *array1 = [[NSMutableArray alloc] initWithObjects:@"a",@"b", nil];
NSArray *array2 = [array1 copy];//深拷贝
NSMutableArray *array3 = [array1 mutableCopy];//深拷贝
NSLog(@"%p %p %p",array1 , array2 , array3);//0x600003c4c330 0x6000032021c0 0x600003c4c360
NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndKeys:@"jack",@"name",nil];
NSDictionary *dic1 = [dic copy];//浅拷贝
NSDictionary *dic2 = [dic mutableCopy];//深拷贝
NSLog(@"%p %p %p",dic , dic1 , dic2);//0x6000024e71a0 0x6000024e71a0 0x6000024e7160
经过这一系列的测试,其实就可以找到规律了,或者说可以得出结论,对于可变对象,不管是调用copy或者mutableCopy都是深拷贝,对于不可变对象,copy是浅拷贝,mutableCopy才是深拷贝.
用一张图表来对比,加深理解记忆:
这样,再遇到深拷贝和浅拷贝的面试题,是不是就觉得简单多了呢。
@property中的copy
我们知道@property的修饰词中只有copy,而没有mutableCopy,在MRC下我们可以自己实现setter,而ARC下setter中的实现不需要开发者去关心,那对于可变的对象,调用setter最后会得到一个可变还是不可变的对象呢?
根据下面一段MRC环境下的测试代码,验证一下上面的疑问
@interface Person : NSObject <NSCopying>
@property (nonatomic, copy) NSMutableArray *data;
@end
根据@property的修饰词copy,实现data的setter方法
- (void)setData:(NSArray *)data {
if (_data != data) {
[_data release];
_data = [data copy];//这里一定是调用copy 不存在mutableCopy
}
}
然后使用一下Person
Person *p = [[Person alloc] init];
p.data = @[@"jack",@"elena"];
p.data = [NSMutableArray array];
[p.data addObject:@"jack"];
[p.data addObject:@"elena"];
NSLog(@"%@",p.data);
[p release];
在调用addObject的时候会报一个错误:'-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x7fff805efd90',因为data的setter得到了一个不可变数组。
对象的copy
对象的copy,大家应该也都熟悉,这里还有一个面试点
Person *p = [[Person alloc] init];
p.age = 20;
Person *p2 = [p copy];
p2.age = 30;
NSLog(@"%@",p);
NSLog(@"%@",p2);
[p2 release];
[p release];
在调用copy的时候会产生一个crash, '-[Person copyWithZone:]: unrecognized selector sent to instance 0x600002003bc0',这个crash的原因是,person类没有实现NSCopying协议。
一个自定义的类如果想调用copy,就必须实现NSCopying协议,并实现协议方法copyWithZone,返回一个对象。
@interface Person : NSObject <NSCopying>
@property (nonatomic, assign) NSInteger age;
@end
- (id)copyWithZone:(struct _NSZone *)zone {
Person *p = [[Person alloc] init];
p.age = self.age;
return p;
}
autorelease
继续分享一下之前提过一嘴的autorelease,也是面试中的高频词汇。要说autorelease,还得从引用计数说起。iOS中使用引用计数来管理OC对象的内存,一个新创建的对象引用计数是1,当引用计数为0,对象就会销毁,释放占用的内存空间。
调用retain会让对象的引用计数+1,而调用release会让对象的引用计数-1,这些都是上一篇分享过的内容,这里复习一下。
isa - 位域
这里补充一点儿知识,主要看看引用计数到底是存在什么地方的。
runtime源码中对ISA的定义如下:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
简单理解一下没一个值代表的意义:
- nonpointer:0,代表普通指针,存储着Class,Meta-Class对象的内存地址;1,代表优化过,使用位域存储更多信息
- has_assoc:是否有设置过关联对象,如果没有,释放时会更快
- has_cxx_dtor:是否有C++的析构函数,如果没有,释放时会更快
- shiftcls:存储着Class,Meta-Class对象的内存地址信息
- magic:用于在调试时,分辨对象是否未完成初始化
- weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
- deallocating:对象是否正在释放
- has_sidetable_rc:引用计数是否过大,无法存储再ISA中,如果为1,那么引用计数会存储再一个叫sidetable的类的属性中
- extra_rc:里面存储的值是引用计数减一
通过对上面这些值所代表的的意义的分析,可以知道,引用计数的存储是分两种情况的,要么是存在isa指针中,如果存不下就存在sidetable中。
上面有几个值都会影响释放的速度,所以我们在一起看一下释放的流程(即dealloc流程)
dealloc
当一个对象要释放时,会自动调用dealloc,接下来的轨迹是:
- dealloc
- (void)dealloc {
_objc_rootDealloc(self);
}
- _objc_rootDealloc
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
- rootDealloc 在这一步就判断了上面说的has_assoc等几个属性,如果都是false则直接释放
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
- object_dispose
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
- objc_destructInstance
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);//移除关联对象
obj->clearDeallocating();//将指向当前对象的弱指针置为Nil
}
return obj;
}
clearDeallocating中的实现,感兴趣的可以继续往下跟,这里就不在一一贴代码了,直接说结论,就是将指向当前对象的弱指针置为nil。
到这里dealloc流程结束,可以看到,弱引用其实是runtime在运行时清空的。
自动释放池
终于开始这一节的重要知识点了,自动释放池(autorelease pool),AppKit 和 UIKit 框架在事件循环(RunLoop)的每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease对象。
在MRC下,可以使用NSAutoreleasePool或者@autoreleasepool来创建自动释放池,这里就有一个知识点需要了解一下:
释放NSAutoreleasePool对象,使用[pool release]与[pool drain]的区别?
这个问题在Developer document中有相关的解释,两个方法在不同的系统平台下,作用是有区别的,在支持垃圾回收的环境(garbage-collected environment)下,release是不起作用的,而在iOS环境下,release是可以让NSAutoreleasePool对象的释放的;而对于drain,在iOS环境下,作用和release相同,在支持GC的环境下drain就会触发GC.
原理分析
自动释放池的主要底层数据结构是:__AtAutoreleasePool和AutoreleasePoolPage,调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
__AtAutoreleasePool
写一段简单的测试代码,通过clang命令编译成C++代码,在结合runtime的源码一起,我们看一下@autoreleasepool的底层原理。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"1");
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
}
NSLog(@"2");
}
return 0;
}
clang命令编译之后:
int main(int argc, const char * argv[]) {
/* autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_056d11_mi_0);
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;//调用 __AtAutoreleasePool的构造函数
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}//调用__AtAutoreleasePool的析构函数
NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_056d11_mi_1);
}
return 0;
}
这里就出现了自动释放池的第一个重要的数据结构:__AtAutoreleasePool
__AtAutoreleasePool是一个C++的结构体,定义如下:
//c++结构体 可以理解为一个类
struct __AtAutoreleasePool {
__AtAutoreleasePool() {//构造函数 在创建结构体变量的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {//析构函数 在结构体销毁时调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
- 在创建__AtAutoreleasePool结构体时会在构造函数中调用objc_autoreleasePoolPush()函数,并返回一个atautoreleasepoolobj;
- 在释放__AtAutoreleasePool结构体时会在析构函数中调用objc_autoreleasePoolPop()函数,并将atautoreleasepoolobj传入。
这两个函数的调用,我们可以在runtime源码中找到,就可以看到自动释放池的另一个重要角色AutoreleasePoolPage。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
AutoreleasePoolPage
objc_autoreleasePoolPush()和objc_autoreleasePoolPop()两个函数其实是调用了AutoreleasePoolPage类的两个类方法push()和pop()。所以@autoreleasepool底层就是使用AutoreleasePoolPage对象来管理的。
AutoreleasePoolPage类的定义如下,省略了一些属性:
class AutoreleasePoolPage
{
# define POOL_BOUNDARY nil
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
//SIZE 4096
static void * operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
}
每个
AutoreleasePoolPage对象占用4096个字节的内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址。
所有AutoreleasePoolPage对象是以栈为节点通过双链表的形式连接在一起的,parent和child正是指向前驱和后继指针。
thread是指当前 page 所对应的线程。
next指向当前可插入对象的地址。
AutoreleasePoolPage本身的大小远不及 4096,而超出的空间正是用来存放“期望被自动管理的对象”。begin()和end()方法标记了这个范围。 sizeof(*this)表示AutoreleasePoolPage本身的大小,那么(uint8_t *)this+sizeof(*this)就是最低地址,(uint8_t *)this+SIZE就是最高地址。逐个插入对象时,next指针从begin()到end()逐个移动,后面的full()方法就是指next == end(),empty()就是指next == begin()。
POOL_BOUNDARY
POOL_BOUNDARY在这里是一个哨兵的角色,用来区分不同的自动释放池和解决释放池嵌套问题的。
push
每当创建一个自动释放池,就会调用push()将一个POOL_BOUNDARY入栈,并返回其存放的内存地址,当往池中添加autorelease对象时,将对象的内存地址入栈。
static inline void *push()
{
id *dest;
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;
}
如果已有自动释放池,则push()方法中调用了autoreleaseFast()方法并传入了POOL_BOUNDARY哨兵对象。
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);
}
}
autoreleaseFast()中先是调用了hotPage()方法获得未满的Page,从AutoreleasePoolPage类的定义可知,每个Page的内存大小为4096个字节,每当Page满了的时候,就会创建一个新的Page。hotPage()方法就是用来获得这个新创建的未满的Page。 autoreleaseFast()在执行过程中有三种情况:
① 当前Page存在且未满时,通过page->add(obj)将autorelease对象入栈,即添加到当前Page中;
② 当前Page存在但已满时,通过autoreleaseFullPage(obj, page)创建一个新的Page,并将autorelease对象添加进去;
③ 当前Page不存在,即还没创建过Page,通过autoreleaseNoPage(obj)创建第一个Page,并将autorelease对象添加进去。
再看一下add()的实现:
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
page->add(obj)其实就是将autorelease对象添加到Page中的next指针所指向的位置,并将next指针指向这个对象的下一个位置,然后将该对象的位置返回。
在看第二种情况autoreleaseFullPage,page已满
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 {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
autoreleaseFullPage()方法中通过while循环,一层层通过Page的child指针找到最后一个未满的Page,如果最后一个Page已满,就创建一个新的Page,然后将该Page设置为hotPage,通过page->add(obj)将autorelease对象添加进去。
然后在看第三种情况autoreleaseNoPage,这个方法核心是会创建一个新的page并设置它为hotPage,最后将autorelease对象添加进去。
id *autoreleaseNoPage(id obj)
{
...
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
...
return page->add(obj);
}
到这里就分析完了push的流程,这是对于@autoreleasepool包裹的对象加入page的流程,其实调用autorelease的对象也是autoreleaseFast()方法加入到page中的,感兴趣的可以自己跟踪一下源码,这里就不贴了。
hotPage
既然自动释放池是由AutoreleasePoolPage组成的双向链表,那这个链表该如何访问呢?上面的代码中也看到多次使用了hotPage(),还有个coldPage()方法是根据hotPage()找到第一个 page。
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;
}
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
tls_get_direct(...)和tls_set_direct(...)内部就是使用线程的局部存储(TLS: Thread Local Storage)将 page 存储起来,这样可以避免维护额外的空间来记录尾部的 page。由此也验证了自动释放池与线程一一对应的关系。
pop
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(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 (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
pop()方法的传参token即为POOL_BOUNDARY对应在Page中的地址。当销毁自动释放池时,会调用pop()方法将自动释放池中的autorelease对象全部释放(实际上是从自动释放池的中的最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY)。pop()方法的执行过程如下:
① 判断token是不是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池; ② 如果不是的话,就通过pageForPointer(token)拿到token所在的Page(自动释放池的首个Page); ③ 通过page->releaseUntil(stop)将自动释放池中的autorelease对象全部释放,传参stop即为POOL_BOUNDARY的地址; ④ 判断当前Page是否有子Page,有的话就销毁。
释放时机
调用了autorelease的对象在什么时机释放呢?
这要分两种情况:
-
如果是被@autoreleasepool包着的, 就是@autoreleasepool的{}结束的时候就直接释放了。
-
如果只是单独调用autorelease的对象,则是有runloop来控制的。
-
方法中的局部对象(没有调用autorelease),方法结束,立刻释放。
Runloop和autorelease
iOS在主线程的runloop中注册了两个Observer,第一个监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();第二个监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPush()、objc_autoreleasePoolPop(),以及kCFRunLoopExit事件,会调用objc_autoreleasePoolPop()。
对于上面这一段是不是还有点儿懵,可以通过打印[NSRunLoop currentRunLoop]找到下面这两个CFRunLoopObserver
/*
监听的activities = 0x1 就是1 kCFRunLoopEntry
"<CFRunLoopObserver 0x600001a14140 [0x7fff805eff70]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47571f14), context = <CFArray 0x600002528bd0 [0x7fff805eff70]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fc029003048>\n)}}",
监听的activities = 0xa0 就是160 kCFRunLoopBeforeWaiting | kCFRunLoopExit
"<CFRunLoopObserver 0x600001a141e0 [0x7fff805eff70]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47571f14), context = <CFArray 0x600002528bd0 [0x7fff805eff70]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fc029003048>\n)}}"
*/
在找到CFRunLoopActivity的枚举值定义
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
};
这样对照着再去理解Runloop和autorelease的关系,还有优先级order = -2147483647优先级最高,保证创建缓存池发生在其他所有回调之前;order = 2147483647,优先级最低,保证其释放缓存池发生在其他所有回调之后。
所以对于单独调用autorelease的对象,是有runloop来控制释放时机的,对象所处的runloop休眠之前调用了release去做释放。