ios 内存管理-弱引用

1,113 阅读11分钟

前言

内存管理是ios开发中非常重要的部分,适当的时机释放和回收内存才能保证程序的高效运行。内存分栈内存、堆内存,我们真正需要关心是堆内存,栈内存是系统自己管理的。函数、变量都是放在栈区的,它的内存管理是系统完成的,而通过alloc创建的对象、block copy等内存是存在于堆区的,它的内存管理需要我们自己实现,引用计数为0时对象才会释放,所以我们需要关注对象的引用计数是如何增加的?我们经常使用weak修饰对象用来解决循环引用,为什么weak修饰的对象可以解决循环引用呢?带着这些疑问我们探索一下。

TaggedPointer

探索引用计数前,我们先了解一下TaggedPointer,因为下面相关的源码探索首先会判断对象是否是TaggedPointer类型。

TaggedPointer由来?

自2013年苹果推出iphone5s之后,iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位)。那么这个数字的范围是2^63 ,很明显我们一般不会用到这么大的数字,那么在我们定义一个数字时NSNumber *num = @100,实际上内存中浪费了很多的内存空间。苹果也认识到了这个问题,于是就引入了tagged pointer,tagged pointer是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。这点和我们之前探索isa时很相似,当isa不是纯指针时,它不仅存储着关联的对象还存储着对象的引用计数等相关信息,苹果对isa进行了内存优化,让它能存储更多的信息。

TaggedPointer特点

  • Tagged Pointer专⻔用来存储小的对象,例如NSNumberNSDate
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free
  • 在内存读取上有着3倍的效率,创建时比以前快106倍。
  • 字符串的长度为10个以内时,用initwithformat初始化的字符串,字符串的类型是NSTaggedPointerString类型;当超过10个时,字符串的类型是__NSCFString

TaggedPointer的判断

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#elif OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

判断一个对象类型是否为NSTaggedPointerString类型实际上是将对象的地址与_OBJC_TAG_MASK进行按位与操作,结果在跟_OBJC_TAG_MASK进行对比。一个对象地址为64位二进制,结合_OBJC_TAG_MASK的值可以知道,如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer类型。下面举一个字符串初始化的例子

  NSString *str = [NSString stringWithFormat:@"hello"];
    NSLog(@"str:%p-%ld-%@",str,CFGetRetainCount((CFTypeRef)str),str.class);

    NSString *str2 = @"helloword,快了学习不急不躁";
    NSLog(@"str2:%p-%ld-%@",str2,CFGetRetainCount((CFTypeRef)str2),str2.class);

   
    NSString *str3 = [[NSString alloc]initWithString:str2];
    NSLog(@"str3:%p-%ld-%@",str3,CFGetRetainCount((CFTypeRef)str3),str3.class);

    NSString *str4 = [NSString stringWithString:str2];
    NSLog(@"str4:%p-%ld-%@",str4,CFGetRetainCount((CFTypeRef)str4),str4.class);

    NSString *str5 = [[NSString alloc]initWithFormat:@"he%@",@"llo"];
    NSLog(@"str5:%p-%ld-%@",str5,CFGetRetainCount((CFTypeRef)str5),str5.class);

    NSString *str6 = [[NSString alloc]initWithFormat:@"he%@",@"llo,快了学习不急不躁"];
    NSLog(@"str6:%p-%ld-%@",str6,CFGetRetainCount((CFTypeRef)str6),str6.class);

输出:

str:0xa00006f6c6c65685-9223372036854775807-NSTaggedPointerString
str2:0x1085cf0a0-1152921504606846975-__NSCFConstantString
str3:0x1085cf0a0-1152921504606846975-__NSCFConstantString
str4:0x1085cf0a0-1152921504606846975-__NSCFConstantString
str5:0xa00006f6c6c65685-9223372036854775807-NSTaggedPointerString
str6:0x600001759c40-1-__NSCFString

分析:分别使用stringWithFormat字符串直接赋值initWithStringstringWithString、initWithFormat初始化字符串,并且打印指针、引用计数以及字符串类型,总结如下

  • NSCFConstantString类型的字符串的引用计数都是1152921504606846975,指针都是0x1085cf0a0,说明NSCFConstantString是一个字符串常量,创建之后便是放不掉的对象,相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例
  • NSCFString和 __NSCFConstantString 不同, _NSCFString 对象是在运行时创建的一种 NSString 子类,他并不是一种字符串常量,所以和其他的对象一样在被创建时获得了 1 的引用计数
  • NSTaggedPointerString和NSCFConstantString类似,它的引用计数是9223372036854775807也非常大说明也是一个释放不掉的单例常量,并且值相同的话指针也相同,只不过NSTaggedPointerString是一个特殊的指针,它的值存储在它的指针地址中。
  • 只有使用stringWithFormatinitWithFormat初始化的字符串对象并且字符串长度在10以下才是TaggedPointer类型,否则是NSCFString类型

总结:

  • taggedpointer也是有类型的,比如OBJC_TAG_NSString、OBJC_TAG_NSNumber、OBJC_TAG_NSIndexPath,可以通过判断TaggedPointer标志位来确定属于哪种类型,这里就不详细研究了。
  • tagged pointer跟之前探索对象的本质那篇文章一样,对象里有个属性isa,苹果对isa做了优化,让isa不仅能存储关联的对象还能存储一些其他的值,比如对象的引用计数等等,通过第一个标志位来判断isa是否是nonpointer类型,0表示纯指针,1表示isa中包含了类信息、对象、引用计数等,占1位[0]。extra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,extra_rc占8位[57 64]

对象的引用计数

首先写个demo,多线程对NSString进行读写。

self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
   //多线程示例1
    for (int i = 0; i<1000000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"hello"];
             NSLog(@"%@",self.nameStr);
        });
    }
     //多线程示例2
    for (int i = 0; i<10000000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"hello_和谐学习不急不躁"];
            NSLog(@"%@",self.nameStr);
        });

    }

分析:示例1多线程读写不会奔溃,但是示例2多线程读写会奔溃(最好在模拟器下测试,真机性能好的话很难模拟出奔溃),根据上面的分析,在示例1下nameStr是TaggedPointer类型,而在示例2下nameStr是NSCFString类型,NSCFString类型和普通对象类型很类似,为什么示例1内存读写安全,示例2内存读写不安全?首先内存读写就是对新值的retain旧值的release,那么就重点分析下这两个函数。

objc_retain

objc_retain(id obj)
{
    if (obj->isTaggedPointerOrNil()) return obj;
    return obj->retain();
}

首先判断对象是否是TaggedPointer类型,如果是直接就返回了,没有经历新值的retain和旧值的release,所以它的内存读写是安全的以及高效的。如果不是TaggedPointer类型接着源码跟下去,找到rootRetain函数。

rootRetain

objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;
    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;
    oldisa = LoadExclusive(&isa.bits);
    //...省略一些干扰代码
    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        //如果是纯isa,sidetable中引用计数表+1,
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // 如果isa正在析构
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        uintptr_t carry;
        //isa中标志位extra_rc++
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        //extra_rc占[57-64]共8位,也就是2的8次方共256
        //如果extra_rc存满了也就是大于256,那么就拿出一半存入sidetable中引用计数表里,
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            //保留一半引用计数,并准备将另一半复制到散列哈希表。
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
     //..省略
    return (id)this;
}
  • 如果isa是纯指针,那么引用计数就存入sidetable引用计数表
  • 对象的本质我们探索过isa,如果不是纯指针,标志位第57位到64位共8位存储着对象的引用计数extra_rc,也就是说extra_rc最多存储2的8次方共256个引用计数
  • 如果extra_rc存满了也就是大于256,那么就拿出一半存入sidetable引用计数表里

SideTable

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
   //...
}
static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
 //...
}

SideTable散列表里有一把锁slock、一张引用计数表refcnts、一张弱引用表weak_table,SideTable其实就是一张StripedMap哈希表,StripedMap我们在探索时也看过,真机情况下有8张,其它环境下64张。对象的引用计数除了存储在isa的extra_rc中,还可以存储在SideTable中引用计数表里。

lldb断点看一下extra_rc验证一下是否存储着引用计数 image.png 对象obj初始化时引用计数为1,进行retain引用计数+1,总的引用计数应该为2。obj的isa右移57位即为extra_rc,extra_rc值为1,引用计数等于extra_rc值加上1。

弱引用表

散列表中有两张表,一张引用计数表我们已经知道是怎么回事了,那么看一下剩下的这张弱引用表。写个demo,新建NSObject对象,弱引用这个对象,如下。

NSObject *obj = [[NSObject alloc] init];
__weak typeof(obj) weakobj=obj;
 NSLog(@"%@",weakobj);

断点在NSLog处看一下汇编 image.png 弱引用一个对象会调用objc_initWeak初始化这个对象,源码看下这个函数。

id objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

lldb断点跟进去看下

image.png obj的指针和newObj的指针指向同一个对象,说明newObj就是需要弱引用的对象,而location应该是弱引用的指针storeWeak是存储弱引用的关键函数,里面传递了三个模板变量:没有老对象DontHaveOld=false、有新对象DoHaveNew=true、dealloc中可以crash DoCrashIfDeallocating=true,以及两个实际参数弱引用的指针需要弱引用的对象

storeWeak

static id 
storeWeak(id *location, objc_object *newObj)
{
    //弱引用对象时,有新对象没有老对象,即haveNew=true haveOld=false
    //如果对象释放了,也会走这个函数,此时haveNew=false,haveOld=true
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);
    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;
 retry:
    //如果对象释放了,haveOld=true,获取弱引用指针指向的值即需要释放的弱引用对象
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
       /*
       newObj为需要弱引用的对象,即weak修饰的对象
       根据newObj从哈希表中找到对应的散列表SideTable(散列表就是哈希表,哈希表真机有8张,其他环境64张)
       SideTable中有这个对象的引用计数表以及弱引用表
       */
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }
    if (haveNew  &&  newObj) {
        //获取对象的类
        Class cls = newObj->getIsa();
        //如果类没有初始化
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);
            previouslyInitializedClass = cls;
            goto retry;
        }
    }
    //对象释放了,那么就删除弱引用表中对应的对象和指针
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
    //weak指针被添加到弱引用表
    if (haveNew) {
        /*
        weak_register_no_lock参数:
        &newTable->weak_table:对象对应的SideTable中取出弱引用表地址
        newObj:需要弱引用的对象
        location:弱引用的指针
        CrashIfDeallocating:dealloc释放crash标志位
        */
       newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location,crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
       
        //newObj是有值并且不是TaggedPointer类型,在refcount表中设置弱引用标志位
        if (!newObj->isTaggedPointerOrNil()) {
            newObj->setWeaklyReferenced_nolock();
        }
        //不要在其他任何地方设置 *location
        *location = (id)newObj;
    }
    else {
        //没有新值,存储没有改变
    }
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
    callSetWeaklyReferenced((id)newObj);
    return (id)newObj;
}

分析:location是弱引用指针,newObj是需要弱引用的对象即weak修饰的对象,该函数的返回值也是这个对象。弱引用对象时,首先根据newObj从哈希表中找到对应的散列表SideTable(散列表就是哈希表,哈希表真机有8张,其他环境64张),SideTable中有这个对象的引用计数表以及弱引用表。根据这张SideTable获取到弱引用表,然后调用函数weak_register_no_lock添加进弱引用表的数组中。

weak_register_no_lock

/*
weak_table:对象对应的弱引用表
referent_id:弱引用的对象
referrer_id:对象的弱引用指针
*/
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    if (referent->isTaggedPointerOrNil()) return referent_id;

    // ...省略
    weak_entry_t *entry;
    //在弱引用表中找到对应对象的弱引用数组entry
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //如果存在这个弱引用数组,就把弱引用指针添加进这个数组
        append_referrer(entry, referrer);
    } 
    else {
        //如果不存在这个弱引用数组,就创建这个数组,把弱引用对象和弱引用指针添加进数组,并且把这个数组添加进弱引用表中
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }
    return referent_id;
}

分析:首先在弱引用表中找到对象对应的弱引用数组entry,如果entry为空,就创建这个数组entry,把弱引用对象和弱引用指针添加进数组,并且把这个数组添加进弱引用表中。如果entry不为空,就把弱引用指针添加进这个数组。

weak变量添加进弱引用表步骤总结:

  • weak修饰的变量,底层调用objc_initWeak,传递弱引用指针和需要弱引用的对象作为参数,然后调用storeWeak
  • storeWeak函数中,根据对象在哈希表中找到对应的SideTable(有两张表,引用计数表和弱引用表),从SideTable中取出弱引用表的地址,调用weak_register_no_lock函数进行弱引用变量的添加。
  • weak_register_no_lock函数中,首先在弱引用表中找到对应对象的弱引用数组entry,如果存在这个弱引用数组,就把弱引用指针添加进这个数组,如果不存在这个弱引用数组,就创建这个数组,把弱引用对象和弱引用指针添加进数组,并且把这个数组添加进弱引用表中

对象被释放后,弱引用对象如何释放呢?

NSObject *obj = [[NSObject alloc] init];
__weak typeof(obj) weakobj=obj;
 obj=nil;
 NSLog(@"%@",weakobj);

NSLog处打个断点看下汇编 image.png 弱引用的对象释放后,会调用objc_destroyWeak函数

void
objc_destroyWeak(id *location)
{
    (void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
        (location, nil);
}

分析:弱引用的对象释放后,也会调用storeWeak函数,里面传递了三个模板变量:有老对象DontHaveOld=true、没有新对象DoHaveNew=false、dealloc中不可以crash DoCrashIfDeallocating=false,以及两个实际参数弱引用的指针和nil,storeWeak函数上面已经分析过,注释中也详细说明了如果弱引用对象释放了,首先会根据弱引用的指针获取所指向的对象,然后获取对象对应的弱引用表,调用weak_unregister_no_lock进行弱引用对象的减减。

weak_unregister_no_lock

voidweak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    weak_entry_t *entry;
    if (!referent) return;
    /在弱引用表中找到对应对象的弱引用数组entry
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        remove_referrer(entry, referrer);
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }
        if (empty) {
          //移除弱引用表中弱引用数组
            weak_entry_remove(weak_table, entry);
        }
    }
}

分析:weak_unregister_no_lock其实就是对weak_register_no_lock函数添加进弱引用表中的对象进行减减,直到弱引用表中弱引用数组没有值了就把这个数组从弱引用表中移除

实例分析

通过上面的分析weak修饰的对象并不会使对象的引用计数增加,系统使用弱引用表管理对象和弱引用的关系,写个例子看下引用计数

    NSObject *objc = [NSObject new];
    NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    __weak typeof(objc) weakobj=objc;
    NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(weakobj)));
    void(^strongBlock)(void) = ^{ 
          NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
          NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(weakobj)));
     };
    strongBlock();

输出:

引用计数:1
引用计数:1
引用计数:2
引用计数:3
引用计数:4

分析:初始化对象objc引用计数为1weak弱引用objc后并没有增加引用计数依然为1,但是weakobj的引用计数为2,毕竟它也是一个对象,必然会使得引用计数增加,但是我们不需要关心弱引用对象的引用计数,因为弱引用对象的释放是弱引用表管理的,当objc对象释放的时候,同时会释放掉弱引用表中的这些弱引用对象,所以我们会使用weak解决block的循环引用,造成循环引用的本质就是对象的引用计数增加导致释放不掉,而使用weak正好解决这个问题。但进入block后objc引用计数+2,weakobj引用计数也+2,为什么+2?这个和block的拷贝有关,下一篇文章会解释这个原因。

总结

弱引用之所以可以解决循环引用是因为没有增加对象的引用计数,它的内存管理是通过弱引用表管理的。