面试遇到内存管理的第三天-copy和autorelease

500 阅读15分钟

本文又到了内存管理部分的最后一篇了,主要分享一下面试中常被问到的copyautorelease这两个知识点。

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,接下来的轨迹是:

  1. dealloc
- (void)dealloc {
    _objc_rootDealloc(self);
}
  1. _objc_rootDealloc
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
  1. 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);
    }
}
  1. object_dispose
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
  1. 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.

原理分析

自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage,调用了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的对象在什么时机释放呢?

这要分两种情况:

  1. 如果是被@autoreleasepool包着的, 就是@autoreleasepool的{}结束的时候就直接释放了。

  2. 如果只是单独调用autorelease的对象,则是有runloop来控制的。

  3. 方法中的局部对象(没有调用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去做释放。