iOS内存管理(4)-引用计数的存储、weak原理

1. 引用计数存储

如果想了解引用计数的存储情况我们得需要事先了解三个相关的概念,Tagged Pointer,Non-pointer isa,SideTable.

一.Tagged Pointer

我们在前面已经介绍过Tagged Pointer,我们在之前详细写过Tagged Pointer ,我们再简单介绍一下Tagged Pointer,Tagged Pointer的对象的指针中存储了值的内容,而不用去指向一个地址再去取这个地址所存对象的值;相信你也知道了,如果是Tagged Pointer的话就少了创建对象的操作。

我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
3:在内存读取上有着3倍的效率,创建时比以前快106倍。

二.Non-pointer isa

对于isa指针我们在之前的文章之中也介绍过isa指针,在文章中介绍了isa的结构,源码中是这样写的:

typedef struct objc_object *id
struct objc_object {
    Class _Nonnull isa;
}

其实这是之前版本的代码了,现在版本的代码早就变了。
现在实例对象的isa是一个isa_t联合体,里面存了很多其他的东西,引用计数其实也存储在其中;如果该实例对象启用了Non-pointer,那么会对isa的其他成员赋值,否则只会对cls赋值。

/** isa_t 结构体 */
union isa_t {
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};

对象是否不启用Non-pointer目前有这么几个判断条件,这些都可以在runtime源码objc-runtime-new.m中找到逻辑。

1:包含swift代码;
2:sdk版本低于10.11;
3:runtime读取image时发现这个image包含__objc_rawisa段;
4:开发者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES到环境变量中;
5:某些不能使用Non-pointer的类,GCD等;
6:父类关闭。

我们自己新建一个Person类,通过OBJC_DISABLE_NONPOINTER_ISA=YES/NO来看看isa结构体的具体内容:

①. 不使用Non-pointer的isa
isa_t isa = {
    Class class = Person;
    uintptr_t bits = 4294976320;
    struct {
        uintptr_t nonpointer = 0;
        uintptr_t has_assoc  = 0;
        uintptr_t has_cxx_dtor = 0;
        uintptr_t shiftcls = 536872040; 
        uintptr_t magic = 0;
        uintptr_t weakly_referenced = 0;
        uintptr_t deallocating = 0;
        uintptr_t has_sidetable_rc = 0;
        uintptr_t extra_rc = 0;
    }
}
其实可以简化为
isa_t isa = {
    Class class = Person;
}
因为源码中显示不使用Non-pointer则只对isa的class赋值了,其他的都是默认值,而且除了class其他成员也不会在源码中被使用到。
②. 使用Non-pointer的isa
isa_t isa = {
    Class class = Person;
    uintptr_t bits = 8303516107940673;
    struct {
        uintptr_t nonpointer = 1;
        uintptr_t has_assoc  = 0;
        uintptr_t has_cxx_dtor = 0;
        uintptr_t shiftcls = 536872040; 
        uintptr_t magic = 59;
        uintptr_t weakly_referenced = 0;
        uintptr_t deallocating = 0;
        uintptr_t has_sidetable_rc = 0;
        uintptr_t extra_rc = 0;
    }
}
extra_rc就是存的引用计数,nonpointer = 1表示启用Non-pointer。

isa的赋值是在alloc方法调用时,内部会进入initIsa()方法,你可以进去看一看有啥不同之处。

objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { 
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        isa_t newisa(0);
        ......
        (成员赋值)
        ......
        isa = newisa;
    }
}

三. SideTable

散列表,这是一个比较重要的数据结构,相信你也猜到了这个和对象引用计数有关;如果该对象不是Tagged Pointer且关闭了Non-pointer,那该对象的引用计数就使用SideTable来存。

struct SideTable {
    //锁
    spinlock_t slock;
    //强引用相关
    RefcountMap refcnts;
    //弱引用相关
    weak_table_t weak_table;
      ...
}

启动应用后,我们第一次看到SideTable其实是在runtime读取image的时候。

void map_images_nolock(unsigned mhCount, const char* const mhPaths[],
                  const struct mach_header *const mhdrs[]) {
    ...
    static bool firstTime = YES;
    if (firstTime) {
        AutoreleasePoolPage::init();
        SideTableInit();
    }
    ...  
}
static void SideTableInit() {
    new (SideTableBuf)StripedMap();
}

map_images_nolock会多次调用,因为ImageLoader一批加载很多个image到内存,然后通知runtime去读取这一批image,没错这时候runtime开始从image中处理类了;SideTableInit()方法只会执行一次。SideTableInit内部用到了SideTableBuf,SideTableBuf的定义如下

alignas(StripedMap) static uint8_t
    SideTableBuf[sizeof(StripedMap)];
sizeof(StripedMap) = 4096;
alignas(StripedMap)是字节对齐的意思,表示让数组中每一个元素的起始位置对齐到4096的倍数,也把数组中每一个元素都变成了4096大小,能理解吧。
所以这句话就简化为static uint8_t SideTableBuf[4096],也就是定义了一个4096大小类型为uint8_t的数组,每一个元素大小为4096,名字为SideTableBuf;

现在来理解SideTableInit()中的new (SideTableBuf)StripedMap()。你会发现这句话没有
任何意思,你注释后一样可以正常运行。因为上面那句话已经初始化SideTableBuf了,怎么说?看下面。

在SideTableBuf定义上方有这样的一段注释。
We cannot use a C++ static initializer to initialize SideTables because
libc calls us before our C++ initializers run. We also don't want a global 
pointer to this struct because of the extra indirection.
Do it the hard way.
我来翻译一下:我们不能用C++静态初始化方法去初始化SideTables,
因为C++初始化方法运行之前libc就会调用我们;我们同样不想用一个全局的指针去指向SideTables,
因为需要额外的代价。但是没办法我们只能这样。
看不懂没关系,下面就是答案。

什么是C++ static initializer呢,我们依然可以在runtime源码中找到答案。在objc-os.mm中有这样的代码。
size_t count;
Initializer *inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
    inits[i]();
}
是的,这个就是在调用C++的initializer了,这个操作在map_images_nolock之前执行,也就是这时候还没有执行SideTableInit()。

我们打印出其中一个方法名。
......
libobjc.A.dylib`defineLockOrder() at objc-os.mm:674
......
然后我们去defineLockOrder()方法中打个断点,跟踪一波。
__attribute__((constructor)) static void defineLockOrder() {
    ......
    SideTableLocksPrecedeLock(&crashlog_lock);
    ......
}
void SideTableLocksPrecedeLock(const void *newlock) {
    SideTables().precedeLock(newlock);
}
然后会进入这个方法。
static StripedMap& SideTables() {
    return *reinterpret_cast*>(SideTableBuf);
}
你会发现这里已经在使用SideTableBuf了,说明SideTableBuf肯定提前被赋值了。而我们刚才说了SideTableInit()方法调用是C++的initializer调用之后,这也就是注释说的内容。
白话文翻译一下:通过SideTableInit()来初始化SideTable是不对的,因为在SideTableInit()之前会先执行C++的initializer,而在那个时候就已经用到SideTable了,所以我们才用静态全局变量来初始化SideTable,文件被加载就会初始化。

你会在runtime源码中经常看到这样的代码,其实刚才说到:C++的initializer调用阶段也用到了。

SideTable &table = SideTables()[this];
static StripedMap& SideTables() {
    return *reinterpret_cast*>(SideTableBuf);
}

所以我们很有必要理解这句话什么意思。StripedMap是一个模版类,熟悉C++的应该非常熟悉这个,来看看StripedMap会生成怎样的一个类。

//简化版本,宏啥的都替换了
class StripedMap {
    //存SideTable的结构体
    struct PaddedT {
        SideTable value;
    };
    PaddedT array[64];
    //取得p的哈希值,p就是实例对象的地址
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast(p);
        return ((addr >> 4) ^ (addr >> 9)) % 64;
    }
public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast>(this)[p]; 
    }
    ...
}

这样一来就很清晰了,StripedMap里面有一个PaddedT数组,StripedMap重载了[]符号,根据参数的哈希值取PaddedT数组的内容,数组里存的就是SideTable。
现在来理解reinterpret_cast什么意思。

reinterpret_cast:转换一个指针为其它类型的指针等,我们没必要去深究,这样理解就够了。

所以

SideTable &table = SideTables()[this];
static StripedMap& SideTables() {
    return *reinterpret_cast*>(SideTableBuf);
}
意思就是:将SideTableBuf转换为StripedMap*类型并返回,也就
是把SideTableBuf当成StripedMap使用,这也是为什么要写
alignas(StripedMap)的原因,这样SideTableBuf数组每一个元素都正好
对应一个StripedMap对象。

这里要特别注意了,会有哈希冲突吗?

我们创建两个不同的类Person和Car,打印一下通过indexForPointer得到的哈希值。
哈希值计算公式:((addr >> 4) ^ (addr >> 9)) % 64;addr就是实例对象的地址。这个公式岁随便写的吧,看不出啥端倪。

Person *one = [[Person alloc] init];
NSLog(@"%p",one);//0x60200000bf30 105690555268912
indexForPointer(105690555268912) = 44;

Car *two = [[Car alloc] init];
NSLog(@"%p",two);//0x6030002c9710 105759277618960
indexForPointer(105759277618960) = 58;

计算出来的哈希值确实是不一样的,我们可以手动更改哈希算法把哈希值都设置为1,看看程序是否能正常运行。
也就是更改这个方法。
static unsigned int indexForPointer(const void *p) {
      return 1;
}

然后我们打印one和two的retainCount看是否正确。
[one retain];
NSLog(@"%d",[one retainCount]);//2 
[two retain];
NSLog(@"%d",[two retainCount]);//2
看来都没问题,那么系统是怎么解决哈希冲突并成功的进行存取值的呢?我们下面讲。

现在我们就开始说一下引用计数的存储过程:

在runtime执行时我们需要按照优先级先判断一下:
1:对象是否是Tagged Pointer对象;
2:对象是否启用了Non-pointer;
3:对象未启用Non-pointer。
如果满足1就不会判断2,满足2就不会判断3,然后以此类推.

①. Tagged Pointer对象

retain时:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
    if (isTaggedPointer()) return (id)this;
    ...
}

release时:

bool  objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    if (isTaggedPointer()) return false;
    ...
}

retainCount时:

uintptr_t objc_object::rootRetainCount() {
    if (isTaggedPointer()) return (uintptr_t)this;
    ...
}

由此可见对于Tagged Pointer对象,并没有任何的引用计数操作,引用计数数量也只是单纯的返回自己地址罢了。

②. 开启了Non-pointer

retain:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
    ...
    //其实就是对isa的extra_rc变量进行+1,前面说到isa会存很多东西
    addc(newisa.bits, 1, 0, &carry);
    ...
}

release:

bool  objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    ...
    //其实就是对isa的extra_rc变量进行-1
    subc(newisa.bits, 1, 0, &carry);
    ...
}

retainCount:

uintptr_t objc_object::rootRetainCount() {
    ...
    //其实就是获取isa的extra_rc值再+1,alloc新建一个对象时bits.extra_rc为0并不是1,这个要注意。
    uintptr_t rc = 1 + bits.extra_rc;
    ...
}

如果对象开启了Non-pointer,那么引用计数是存在isa中的,引用计数超过255将附加SideTable辅助存储,这时has_sidetable_rc从0->1,详情可以看这里

③. 未开启Non-pointer isa

retain:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
    ...
   sidetable_retain();
    ...
}
id objc_object::sidetable_retain() {
    SideTable& table = SideTables()[this];
}

在这里不得不讲清楚SideTable的内部实现了,如果不讲清楚则没办法继续看代码。

SideTable中有三个结构体。
spinlock_t:锁,这个就不用说了,一个支持多线程环境运行的库肯定得考虑这个;
weak_table_t:weak表就是这个,用来处理弱引用的,不过本文不讲;
RefcountMap:引用表,引用计数就是这个存的,这个要好好的说明白。

RefcountMap的定义:
typedef objc::DenseMap,size_t,true> RefcountMap;
又臭又长,本来想把类展开出来的,但是发现类会非常的大,而且很难懂;所以我这里讲一下逻辑就可以了,你感兴趣可以深入看看。
当我们第一次通过SideTables()[this]取得table时,这个table中refcnts内容是空的。
(我们省略spinlock_t和weak_table_t):
SideTable table = {
    ...
    RefcountMap refcnts = {  
        BucketT *Buckets = NULL;
        unsigned NumEntries = 0;
        unsigned NumTombstones = 0;
        unsigned NumBuckets = 0;
    }
    ...
}
接下来程序会执行size_t &refcntStorage = table.refcnts[this];这句话是在干嘛呢?
RefcountMap继承自DenseMapBase,DenseMapBase中重载了[]操作符,所以会进入[],下面的代码是我展开后的部分代码,方便理解。
class DenseMapBase {
    ...
    //目的很明确,就是取得一个pair, size_t>
    size_t &operator[](DisguisedPtr &&Key) {
        pair, size_t> &reslut = FindAndConstruct(Key);
        return reslut.second;
    }
    ...
}
这里要注意一个细节,因为我们传进来的是this,而这里是用DisguisedPtr来接收的。
所以会触发DisguisedPtr的初始化方法,所以this被转成了下面的对象。
class DisguisedPtr Key =  {
    uintptr_t  value = 18446638383154282704;
}
下面来看FindAndConstruct()做了什么。
pair, size_t>& FindAndConstruct(const DisguisedPtr &Key) {
    //先定义一个用于接收结果的pair对象,关于pair我们肯定很熟悉了,相当于字典的一个key-value对,
    //pair.first就是实例对象地址转换成的DisguisedPtr< objc_object >类,pair.second就是这个对象的引用计数数量。
    pair, size_t> *TheBucket = nil;
    //如果找到了直接返回,因为TheBucket会在LookupBucketFor中被赋值
    if (LookupBucketFor(Key, TheBucket))
        return *TheBucket;
    //没有找到,就插入一个
    return *InsertIntoBucket(Key, ValueT(), TheBucket);
}
//看看能不能找到key对于的pair, size_t>
bool LookupBucketFor(const LookupKeyT &Val,
                             const BucketT *&FoundBucket) const {
    const  pair, size_t> *BucketsPtr = getBuckets();
    const unsigned NumBuckets = getNumBuckets();
    ......  
    (代码还是不贴出来了,我相信你也不想看,总结起来就是从buckets()中找到该实例对象对应的pair, size_t>)
}
总结:
1:取table时我们知道了对象的哈希值是可能一样的,如果哈希值一样那么会得到相同的table;
2:相同的table又会根据对象转换成的DisguisedPtr对象在buckets中去取pair, size_t>对;如果没有就会插入一条;
插入到buckets哪个位置呢?哈希算法如下。
static inline uint32_t ptr_hash(uint64_t key) {
    key ^= key >> 4;
    key *= 0x8a970be7488fda55;//这个是随意写的吧,没发现啥特别的
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
为此我依然做了一个测试,我把ptr_hash的值都返回1,模拟哈希冲突。
static inline uint32_t ptr_hash(uint64_t key) {
    return (uint32_t)1;
}
然后我们打印one和two的retainCount看是否正确。
[one retain];
NSLog(@"%d",[one retainCount]);//2 
[two retain];
NSLog(@"%d",[two retainCount]);//2
经过测试依然是正确的,说明内部会解决哈希冲突,也说明了这个哈希算法并不能产生唯一的值。这也就解决了
上面留下的问题,获取table的时候没有解决哈希冲突,而是在查找pair对的时候有解决哈希冲突,方法就是
找到下一个可用的位置,这也是很常见的哈希冲突解决方法;另外一个方法是用链表存所有哈希值一样的value,不过系统在这里并没有用这种方法。
3:这个buckets的大小是会动态改变的,这也是
RefcountMap refcnts = {  
    BucketT *Buckets = NULL;
    unsigned NumEntries = 0;
    unsigned NumTombstones = 0;
    unsigned NumBuckets = 0;
}中后三个变量的作用;装逼点的说法就是让数组具有伸缩性,提前处理一些临界值情况。

所以我们可以把refcnts中的Buckets看成一个数组,根据对象地址产生的哈希值和哈希冲突算法肯定能在Buckets中找到其对应的pair;我们接着往下走。

id objc_object::sidetable_retain() {
    ...
    //取得该实例对象在该table中对应pair, size_t>对中size_t的引用,默认值为0。
    size_t &refcntStorage = table.refcnts[this];
    //SIDE_TABLE_RC_PINNED的值在64位系统为1<<63,也就是pow(2,63)
    if ((refcntStorage & SIDE_TABLE_RC_PINNED) == false) {
        //SIDE_TABLE_RC_ONE的值为4,为什么要以4为单位,我不知道,估计是控制最大引用计数的值吧。
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    return (id)this;
}

我们知道了refcntStorage值最大为pow(2,63)-4,因为再加refcntStorage & SIDE_TABLE_RC_PINNED就为false了。
而引用计数最大为(pow(2,63) - 4) >> 2  + 1 = pow(2,61) = 2305843009213693952,为什么要+1?后面会说。

还是需要数据说话,不然有人不相信。
unsigned long number = SIDE_TABLE_RC_PINNED;//pow(2,63)
unsigned long first = (unsigned long)pow(2, 63) - 4;
unsigned long second = (unsigned long)pow(2, 63);
unsigned long max = first >> 2;
unsigned long max111 = (unsigned long)pow(2, 61);
(lldb) po second & number
9223372036854775808
(lldb) po first & number
0
(lldb) po max
2305843009213693951
(lldb) po max111
2305843009213693952

retain我们就说完了,其实release也是这样的逻辑。
release:

前面的逻辑一样,拿到实例对象对应的pair。
uintptr_t objc_object::sidetable_release(bool performDealloc) {
    ...
    //迭代器,it指向的就是pair, size_t>
    RefcountMap::iterator it = table.refcnts.find(this);
    ...
    //SIDE_TABLE_RC_ONE = 4
    it->second -= SIDE_TABLE_RC_ONE;
    ...
}

retainCount:

uintptr_t objc_object::sidetable_retainCount() {
    SideTable& table = SideTables()[this];
    //这就是上面为什么说要+1的原因
    size_t refcnt_result = 1;
    //迭代器,it指向的就是pair, size_t>
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        //SIDE_TABLE_RC_SHIFT = 2
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    ...
}

2. weak

①. weak基本用法

weak是弱引用,用weak描述修饰或者所引用对象的计数器不会加一,并且会在引用的对象被释放的时候自动被设置为nil,大大避免了野指针访问坏内存引起崩溃的情况,另外weak还可以用于解决循环引用。

②. weak基本用法

weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址数组。weak的底层实现的原理是什么?
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash表,Key是所指对象的地址,value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
为什么value是数组?因为一个对象可能被多个弱引用指针指向.

③. weak原理实现步骤

weak 的实现原理可概括三步:
1.初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2.添加引用时:objc_initWeak函数会调用 objc_storeWeak()函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

weak实现三步骤详细过程:
一. 初始化时:
runtime会调用objc_initWeak函数,objc_initWeak函数会初始化一个新的weak指针指向对象的地址。

NSObject *obj = [[NSObject alloc] init];
 id __weak obj1 = obj;

当我们初始化一个weak变量时,runtime会调用 NSObject.mm 中的objc_initWeak函数。
这个函数在Clang中的声明如下:
id objc_initWeak(id *object, id value);
而对于 objc_initWeak() 方法的实现如下:

id objc_initWeak(id *location, id newObj) {
// 查看对象实例是否有效,无效对象直接导致指针释放
    if (!newObj) {
        *location = nil;
        return nil;
    }
    // 这里传递了三个 bool 数值
    // 使用 template 进行常量参数传递是为了优化性能
    return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
    (location, (objc_object*)newObj);
}

这里先判断了其指针指向的类对象是否有效,无效直接释放返回,不再往深层调用函数。否则,object将通过bjc_storeWeak函数被注册为一个指向value的__weak对象。
注意:objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是null,或者指向一个有效的对象。
二. 添加引用时:
objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
objc_storeWeak的函数声明如下:

id objc_storeWeak(id *location, id value);

三. 释放时:
调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录.
当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

1、调用objc_release
2、因为对象的引用计数为0,所以执行dealloc
3、在dealloc中,调用了_objc_rootDealloc函数
4、在_objc_rootDealloc中,调用了object_dispose函数
5、调用objc_destructInstance
6、最后调用objc_clear_deallocating,详细过程如下:
   a. 从weak表中获取废弃对象的地址为键值的记录
   b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为   nil
   c. 将weak表中该记录删除
   d. 从引用计数表中删除废弃对象的地址为键值的记录

runtime部分的源码:

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();
    }

    return obj;
}

补充的部分:__unsafe_unretained 与 weak 比较,使用 weak 是有代价的,因为通过上面的原理可知,__weak需要检查对象是否已经消亡,而为了知道是否已经消亡,自然也需要一些信息去跟踪对象的使用情况。也正因此,__unsafe_unretained 比 __weak快,所以当明确知道对象的生命期时,选择__unsafe_unretained 会有一些性能提升,这种性能提升是很微小的。但当很清楚的情况下,__unsafe_unretained 也是安全的,自然能快一点是一点。而当情况不确定的时候,应该优先选用 __weak 。

                            想了解更多iOS学习知识请联系:QQ(814299221)

你可能感兴趣的:(iOS内存管理(4)-引用计数的存储、weak原理)