美文网首页
OC对象的引用计数存放在哪里?weak和autorelease是

OC对象的引用计数存放在哪里?weak和autorelease是

作者: 小心韩国人 | 来源:发表于2019-12-20 16:46 被阅读0次

    我们都知道OC是通过引用计数来管理对象的生命周期的.一个新创建的OC对象的默认引用计数是1,调用retain会让对象的引用计数+1,调用release会让对象的引用计数-1.当引用对象为0时,OC对象就会销毁并释放其占用的内存空间.那么这个引用计数是存放在哪里的呢?
    arm64 (5S) 架构开始,引用计数就直接存储在对象的isa指针中:

    isa中存储引用计数
    SideTable的结构如下:
    struct SideTable {
        spinlock_t slock;
        RefcountMap refcnts;//引用计数
        weak_table_t weak_table;
    }
    

    其中有个散列表RefcountMap就存储着引用计数.RefcountMap中以当前对象的地址作为key,引用计数作为value.
    下面我们就从runtime源码中验证一下:
    步骤:NSObject.mm中搜索- (NSUInteger)retainCount:

    - (NSUInteger)retainCount {
        return ((id)self)->rootRetainCount();
    }
    

    进入rootRetainCount:

    objc_object::rootRetainCount()
    {
        //如果是 TaggedPoint 类型就直接返回
        if (isTaggedPointer()) return (uintptr_t)this;
    
        sidetable_lock();
        isa_t bits = LoadExclusive(&isa.bits);
        ClearExclusive(&isa.bits);
        
        //判断指针是否优化过: 0:普通指针 ; 1:优化过指针
        if (bits.nonpointer) {
            
            //取出 extra_rc 的值然后 加1, extra_rc存储的值是引用计数减1
            uintptr_t rc = 1 + bits.extra_rc;
            
            //判断引用计数是否过大,无法存储在 isa 中
            // has_sidetable_rc 如果为1,那么引用计数就存储在 SideTable 中.
            if (bits.has_sidetable_rc) {
                
                //取出 SideTable 中的引用计数
                rc += sidetable_getExtraRC_nolock();
                
            }
            sidetable_unlock();
            return rc;
        }
    

    如果has_sidetable_rc为1就说明引用引用计数存储在SideTables中,我们进入sidetable_getExtraRC_nolock看看是如何从SideTables中获取引用计数的:

    objc_object::sidetable_getExtraRC_nolock()
    {
        assert(isa.nonpointer);
        
        //把自身 this 当做 key 取出 table
        SideTable& table = SideTables()[this];
        
        // Map 说明这是一个 散列表
        //refcnts 是 RefcountMap 类型,也是一个 散列表
        RefcountMap::iterator it = table.refcnts.find(this);
        if (it == table.refcnts.end()) return 0;
        
        //取出 second 然后 位运算 得到 引用计数
        else return it->second >> SIDE_TABLE_RC_SHIFT;
    }
    

    weak实现的原理

    运行下面的代码,可以看到局部变量person再离开其作用域后就销毁了:


    如果我们用一个__strong修饰的强指针指向这个person会发现,过了person的作用域后还不会销毁:
    __strong

    再用weak修饰的person2指向person看看结果:

    weak
    发现weak没有对person产生强引用.
    再用__unsafe_unretained修饰的person3指向person:
    __unsafe_unretained
    会发现崩溃了,但是person对象依然在出了自身作用域后就销毁了,说明__unsafe_unretained同样没有对person产生强引用.
    既然__unsafe_unretainedweak都没有对person产生强引用,那他们有什么区别呢?
    区别就是:wea不会对对象产生强引用,并且当weak指向的对象释放后,weak会把指针置为nil.防止野指针错误.而__unsafe_unretained指向的对象销毁后,__unsafe_unretained并不会把指针置为nil.所以__unsafe_unretained是不安全的,容易出现野指针访问

    那么weak内部是如何实现的把指针置为nil的呢?我们还是从runtime源码中探寻答案:
    步骤:在NSObject.mm中搜索- (void)dealloc:

    - (void)dealloc {
        _objc_rootDealloc(self);
    }
    

    进入_objc_rootDealloc:

    _objc_rootDealloc(id obj)
    {
        assert(obj);
    
        obj->rootDealloc();
    }
    

    进入rootDealloc:

    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return;  // fixme necessary?
    
        if (fastpath(isa.nonpointer  &&  //如果是优化过指针
                     !isa.weakly_referenced  &&  //是否被弱引用指向过
                     !isa.has_assoc  &&  //是否设置过关联对象
                     !isa.has_cxx_dtor  &&  //是否有c++析构函数
                     !isa.has_sidetable_rc))//引用计数是否存储在SideTable中
        {
            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();//如果有c++析构函数
            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的:

    objc_object::clearDeallocating()
    {
        if (slowpath(!isa.nonpointer)) {
            // Slow path for raw pointer isa.
            //普通isa指针
            sidetable_clearDeallocating();
        }
        else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
            // Slow path for non-pointer isa with weak refs and/or side table data.
            //优化过isa指针
            clearDeallocating_slow();
        }
    
        assert(!sidetable_present());
    }
    

    进入优化过的isa指针处理方法clearDeallocating_slow:

    objc_object::clearDeallocating_slow()
    {
        assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
        
        //传入 this ,取出 SideTable 类型的 table
        SideTable& table = SideTables()[this];
        /**
           struct SideTable {
           spinlock_t slock;
           RefcountMap refcnts;//散列表,以当前对象地址作为key,retainCount作为value
           weak_table_t weak_table;//散列表,以当前对象地址作为key,weak修饰的指向此对象的指针作为value
         }
         */
        table.lock();
        if (isa.weakly_referenced) {
            //清除 weak 指针
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        /**
         清除完weak指针后,也会把引用计数表清除
         因为当前对象要销毁了
         */
        if (isa.has_sidetable_rc) {
            table.refcnts.erase(this);
        }
        table.unlock();
    }
    

    进入weak_clear_no_lock看看是如何清除的:

    weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
    {
        objc_object *referent = (objc_object *)referent_id;
    
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    }
    

    weak_entry_for_referent(weak_table, referent)把弱引用表 和 当前对象地址值传进去:

    static weak_entry_t *
    weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
    {
        assert(referent);
    
        weak_entry_t *weak_entries = weak_table->weak_entries;
    
        if (!weak_entries) return nil;
    
        //利用当前对象的地址值 & 一个值 得到一个索引 begin
        size_t begin = hash_pointer(referent) & weak_table->mask;
        size_t index = begin;
        size_t hash_displacement = 0;
        while (weak_table->weak_entries[index].referent != referent) {
            index = (index+1) & weak_table->mask;
            if (index == begin) bad_weak_table(weak_table->weak_entries);
            hash_displacement++;
            if (hash_displacement > weak_table->max_hash_displacement) {
                return nil;
            }
        }
        //根据索引值得到对应的weak指针
        return &weak_table->weak_entries[index];
    }
    

    最后清除:

        //拿到需要清除的weak指针,清除
        weak_entry_remove(weak_table, entry);
    
    static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
    {
        // remove entry
       //释放
        if (entry->out_of_line()) free(entry->referrers);
       //置为nil
        bzero(entry, sizeof(*entry));
    
        weak_table->num_entries--;
    
        weak_compact_maybe(weak_table);
    }
    

    ARC帮我们做了什么?

    • ARC是 LLVM 和 Runtime 的结合
    • 利用LLVM自动生成 release 和 retain autorelease 代码
    • 像弱引用的实现就需要 Runtime 运行时处理

    autorelease

    调用了autorelease的对象不需要我们手动调用release,系统会在适当的时候自动调用release去释放对象.那么系统是怎么做到这点的呢?这就牵扯到了自动释放池@autoreleasepool.我们以一下代码为实例,并且转换为c++代码:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            Person *person = [[[Person alloc]init]autorelease];
            
        }
        return 0;
    }
    

    c++代码:

    { __AtAutoreleasePool __autoreleasepool; 
         //person的初始化代码
        Person *person = ((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"));
        }
    

    我们再把person的初始化代码去掉:

    { __AtAutoreleasePool __autoreleasepool; 
       Person *person = [[[Person alloc]init]autorelease];
        }
    

    会发现autoreleasepool代码就被转成了上面这种样式.__AtAutoreleasePool其实就是结构体:

             struct __AtAutoreleasePool {
             //构造函数,生成结构体变量的时候调用
               __AtAutoreleasePool() {
                      atautoreleasepoolobj = objc_autoreleasePoolPush();
             
             }
             //析构函数,销毁结构体变量的时候调用
               ~__AtAutoreleasePool() {
                      objc_autoreleasePoolPop(atautoreleasepoolobj);
             }
               void * atautoreleasepoolobj;
             };
    

    c++中的结构体和OC中的类很像,可以把c++的结构体看做是OC中的类,所以可以在结构体中声明函数.
    也就是说执行__AtAutoreleasePool __autoreleasepool;代码的时候就会去调用__AtAutoreleasePool的构造函数:

    //构造函数,生成结构体变量的时候调用
               __AtAutoreleasePool() {
                      atautoreleasepoolobj = objc_autoreleasePoolPush();
             
             }
    

    一旦走完autoreleasepool的大括号就会调用__AtAutoreleasePool的析构函数:

    //析构函数,销毁结构体变量的时候调用
               ~__AtAutoreleasePool() {
                      objc_autoreleasePoolPop(atautoreleasepoolobj);
             }
               void * atautoreleasepoolobj;
             };
    

    所以我么写的代码最后实际上的效果就是这样:

    
             调用构造函数 返回 atautoreleasepoolobj
             atautoreleasepoolobj = objc_autoreleasePoolPush();
             
             Person *person = [[[Person alloc]init]autorelease];
             
             调用析构函数,传入 atautoreleasepoolobj
             objc_autoreleasePoolPop(atautoreleasepoolobj);
    
    

    也就是autoreleasepool大括号的开始会调用objc_autoreleasePoolPush,大括号的结束会调用objc_autoreleasePoolPop.所以以后只要看到autoreleasepool就代表被objc_autoreleasePoolPushobjc_autoreleasePoolPop包围:

    @autoreleasepool {
            atautoreleasepoolobj = objc_autoreleasePoolPush();
    
            Person *person = [[[Person alloc]init]autorelease];
            @autoreleasepool {
                atautoreleasepoolobj = objc_autoreleasePoolPush();
    
                objc_autoreleasePoolPop(atautoreleasepoolobj);
            }
            
            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
           
    

    所以我们只要搞清楚objc_autoreleasePoolPush()objc_autoreleasePoolPop ()这两个函数内部做了什么,就能搞清楚autoreleasepool的本质了.
    runtime源码中搜索objc_autoreleasePoolPushobjc_autoreleasePoolPop函数会发现他们长这样:

    objc_autoreleasePoolPush(void)
    {
        return AutoreleasePoolPage::push();
    }
    
    objc_autoreleasePoolPop(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }
    

    发现他们都用到了AutoreleasePoolPage这个类.调用了 autorelease 的对象,最终都是通过 AutoreleasePoolPage这个类来管理的所以我们重点研究一下这个类.
    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;
    {
    
    • 一旦调用objc_autoreleasePoolPush ()函数,就会创建一个AutoreleasePoolPage对象.这个对象占用4096个字节的内存,除了用来存放它内部的成员变量以外,剩下的空间用来存放autorelease对象的地址.比如说person对象调用了autorelease,那么person对象的地址就存在了AutoreleasePoolPage对象中.
    • 所有的AutoreleasePoolPage对象通过双向链表的形式链接在一起.

    比如说现在有一个AutoreleasePoolPage对象,内存地址在0x1000,那么它的结束内存地址就是0x2000.因为十六进制0x1000的十进制就是4096.person又调用了autorelease方法,那么person对象的地址就会被存放在AutoreleasePoolPage对象内部的0x1038这个位置,因为AutoreleasePoolPage对象内部有7个成员变量,每个成员变量占用8个字节,一共占用56个字节,用十六进制表示就是0x38,画图表示如下:


    AutoreleasePoolPage对象内部有两个函数:
    //返回开始存放 autorelease 对象的地址
        id * begin() {
            
            算法:自身的地址 + 自身所占空间大小:  0x1000 + 0x38
            return (id *) ((uint8_t *)this+sizeof(*this));
        }
    //返回存放 autorelease 对象的结束地址
        id * end() {
            SIZE = 4096 , 自身地址 + 4096 个字节 : 0x1000 + 0x1000
            return (id *) ((uint8_t *)this+SIZE);
        }
    

    如图所示:

    bengin & end
    如果一个AutoreleasePoolPage对象不够存储autorelease对象,那么就会再创建AutoreleasePoolPage对象.每个AutoreleasePoolPage对象的parent指向它的上一个AutoreleasePoolPage对象,如果是第一个AutoreleasePoolPage对象,它的parent就为nil;每个AutoreleasePoolPage对象的child指向它的下一个AutoreleasePoolPage对象,如果是最后一个AutoreleasePoolPage对象,child也为nil.关系如下:
    关系图

    现在我们知道了objc_autoreleasePoolPush()会将autorelease对象的地址存放到AutoreleasePoolPage对象中;objc_autoreleasePoolPop会将AutoreleasePoolPage中存放的autorelease对象释放.那么他们底层是怎么存储和释放的呢?

    其实一旦调用objc_autoreleasePoolPush()函数,它的内部就会将POOL_BOUNDARY入栈,并且返回其内存地址,也就是0x1038.这里的入栈并不是内存中的堆区和栈区,而是数据结构的那种栈,先进后出;POOL_BOUNDARY其实就是nil,它的底层就是个宏:(define POOL_BOUNDARY nil);也就是说AutoreleasePoolPage存放的第一个对象并不是autorelease对象person,而是POOL_BOUNDARY.其次才是一个个autorelease对象,如图:

    image.png
    objc_autoreleasePoolPop (0x1038)传入的地址就是objc_autoreleasePoolPush ()返回的地址,也就是0x1038.objc_autoreleasePoolPop拿到这个地址后会从最后一个加入到AutoreleasePoolPageautorelease对象开始一个一个调用它们的release方法,直到POOL_BOUNDARY为止.BOUNDARY就是边界的意思,可见push函数和pop函数结合的非常巧妙.
    • AutoreleasePoolPagenext成员变量永远指向下一个能存放autorelease对象的地址.
    • 使用_objc_autoreleasePoolPrint私有函数查看autoreleasepool自动释放池的情况:
    //使用 extern 关键字声明这个函数,即使这个函数在 Foundation 框架内部
    //编译器会自动去查找这个方法并调用,这是c语言语法
    extern void _objc_autoreleasePoolPrint(void);
    

    我们使用一下:


    person autorelease之前 person autorelease之后 多个autoreleasepool 超出一个 page 容量

    先面我们将从源码上验证上诉结论:

    push源码分析:

    // 第一步:
        static inline void *push() 
        {
            id *dest;
            if (DebugPoolAllocation) {
                // Each autorelease pool starts on a new pool page.
               //如果没有 page 就创建一个 page,把 POOL_BOUNDARY 传进去
                dest = autoreleaseNewPage(POOL_BOUNDARY);
            } else {
                //有page
                dest = autoreleaseFast(POOL_BOUNDARY);
            }
            assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
    
    //第二步
        id *autoreleaseNewPage(id obj)
        {
            //调用hotPage,创建一个page
            AutoreleasePoolPage *page = hotPage();
            //判断 page 是不是满了
            if (page) return autoreleaseFullPage(obj, page);
            else return autoreleaseNoPage(obj);
        }
    
    //第三步
        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);
            //往 page 中添加POOL_BOUNDARY
            return page->add(obj);
        }
    

    autorelease源码分析:

    //第一步:
    objc_object::rootAutorelease2()
    {
        assert(!isTaggedPointer());
        //将调用了 autorelease 的对象传进去
        return AutoreleasePoolPage::autorelease((id)this);
    }
    
    //第二步:
        static inline id autorelease(id obj)
        {
            assert(obj);
            assert(!obj->isTaggedPointer());
            //进入 autoreleaseFast
            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()) {
                //把调用了 autorelease 的对象 加入到 page
                return page->add(obj);
            } else if (page) {
                //如果有page,并且 page 满了
                return autoreleaseFullPage(obj, page);
            } else {
                //如果没有 page,就创建 page
                return autoreleaseNoPage(obj);
            }
        }
    

    pop源码分析:

    //第一步:
    static inline void pop(void *token) {
            //token 就是 POOL_BOUNDARY
            page = pageForPointer(token);
            stop = (id *)token;
            //释放对象,直到遇到 POOL_BOUNDARY
            page->releaseUntil(stop);
    }
    
    //第二步:
        void releaseUntil(id *stop) 
        {
            // Not recursive: we don't want to blow out the stack 
            // if a thread accumulates a stupendous amount of garbage
            
            while (this->next != stop) {
    
                //如果 不是 POOL_BOUNDARY 就一直释放对象
                if (obj != POOL_BOUNDARY) {
                    objc_release(obj);
                }
            }
    
    

    autorelease 对象什么时候释放?

    我们知道调用了autorelease的对象会在适当的时候由系统去调用release释放,但是这个适当的时候是什么时候呢?思考一下以下代码的person对象什么时候会被释放:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Person *person = [[[Person alloc]init]autorelease];
        NSLog(@"viewDidLoad");
    }
    
    - (void)viewWillAppear:(BOOL)animated{
        NSLog(@"viewWillAppear");
    }
    
    - (void)viewDidAppear:(BOOL)animated{
        NSLog(@"viewDidAppear");
    }
    

    有人可能会觉得是走完viewDidLoad的大括号释放的,我们运行一下代码看看结果:

    AutoRelease对象和runloop[18097:3714897] viewDidLoad
    AutoRelease对象和runloop[18097:3714897] viewWillAppear
    AutoRelease对象和runloop[18097:3714897] -[Person dealloc]
    AutoRelease对象和runloop[18097:3714897] viewDidAppear
    

    事实上是走完viewDidLoadviewWillAppear才释放的,为什么会这样呢?其实这和runloop有关系.
    iOS在主线程中注册了两个Observer用来处理自动释放池相关的工作.Observer是用来监听runloop的状态的,比如进入runloop,runloop即将处理timer,runloop即将休眠等等.
    我们打印一下NSLog(@"%@",[NSRunLoop currentRunLoop]);看看:

     // 监听状态为 1
     "<CFRunLoopObserver 0x6000021b0960 [0x7fff80615350]>{valid = Yes, activities =
     0x1, repeats = Yes, order = -2147483647, callout = 
    _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray
     0x600001ec6610 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 :
     <0x7fdbda801038>\n)}}",
     
     //监听状态为160  相当于 kCFRunLoopBeforeWaiting | kCFRunLoopExit
     "<CFRunLoopObserver 0x6000021b0a00 [0x7fff80615350]>{valid = Yes, activities =
     0xa0, repeats = Yes, order = 2147483647, callout =
     _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray
     0x600001ec6610 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 :
     <0x7fdbda801038>\n)}}"
    

    会发现有两个Observer会调用_wrapRunLoopWithAutoreleasePoolHandler函数,并且这两个Observer分别监听了两个状态:

    1. activities = 0x1: 0x1 = 1
    2. activities = 0xa0 =xa0 = 160
      我们再来看看runloop中有哪些状态:
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
         kCFRunLoopEntry = (1UL << 0),  1
         kCFRunLoopBeforeTimers = (1UL << 1),  4
         kCFRunLoopBeforeSources = (1UL << 2),  8
         kCFRunLoopBeforeWaiting = (1UL << 5),  32
         kCFRunLoopAfterWaiting = (1UL << 6),  64
         kCFRunLoopExit = (1UL << 7),  128
         kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
    

    对照runloop的状态我们知道:

    1. activities = 0x1: 就是监听 kCFRunLoopEntry进入runloop.
    2. activities = 0xa0 就是监听 kCFRunLoopBeforeWaiting | kCFRunLoopExit即将休眠和退出.

    那么这两个Observer做了什么事情呢?

    1. 第一个Observer监听kCFRunLoopEntry会调用objc_autoreleasePoolPush ().
    2. 第二个Observer:
      监听kCFRunLoopBeforeWaiting时间会调用objc_autoreleasePoolPop (),然后再调用objc_autoreleasePoolPush ();
      监听kCFRunLoopExit事件会调用objc_autoreleasePoolPop ()

    也就是说person对象什么时候调用release,是由runloop来控制的,它可能是在某次runloop循环中,runloop休眠之前调用了release;并且从上面的打印结果可以分析出来,viewDidLoadviewWillAppear是在同一次runloop循环中.

    ARC环境下,方法中局部变量出了方法会立即释放吗?

    我们可以试一下:

    局部对象的释放时机
    从打印结果可以看到,person对象在viewDidLoad之后,viewWillAppear之前就释放了.说明了ARC自动生成的并不是autorelease,而是在viewDidLoad大括号之前生成了person release.
    所以我们应该分两种情况分析:
    1. 如果ARC自动生成的是autorelease,那么局部对象是在runloop某次循环即将休眠的时候释放.
    2. 如果ARC自动生成的是release,那么局部对象就会出了方法就释放.

    相关文章

      网友评论

          本文标题:OC对象的引用计数存放在哪里?weak和autorelease是

          本文链接:https://www.haomeiwen.com/subject/wjsvnctx.html