OC内存管理--引用计数器

栏目: Objective-C · 发布时间: 5年前

内容简介:下面这个实现用来反映在64位系统下我们知道,所有对象都有其对应的
  1. 有些对象如果支持使用 TaggedPointer ,苹果会直接将其指针值作为引用计数返回;
  2. 如果当前设备是 64 位环境并且使用 Objective-C 2.0 ,那么“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数;
  3. 否则 Runtime 会使用一张散列表来管理引用计数。

Tagged Pointer

Tagged Pointer 用来优化内存,其特点:

  1. Tagged Pointer 专门用来存储小的对象,例如 NSNumberNSDate 等;
  2. Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 mallocfree
  3. 在内存读取上有着3倍的效率,创建时比以前快106倍。

下面这个实现用来反映在64位系统下 Tagged Pointer 的应用:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *number4 = @4;
        
        NSNumber *numberLager = @(MAXFLOAT);
        
        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"number4 pointer is %p", number4);
        NSLog(@"numberLager pointer is %p", numberLager);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

//打印结果:
2018-09-25 15:26:05.788382+0800 NSObjectProject[68029:24580896] number1 pointer is 0x9c344c19d780bc93
2018-09-25 15:26:05.789257+0800 NSObjectProject[68029:24580896] number2 pointer is 0x9c344c19d780bca3
2018-09-25 15:26:05.789383+0800 NSObjectProject[68029:24580896] number3 pointer is 0x9c344c19d780bcb3
2018-09-25 15:26:05.789489+0800 NSObjectProject[68029:24580896] number4 pointer is 0x9c344c19d780bcc3
2018-09-25 15:26:05.789579+0800 NSObjectProject[68029:24580896] numberLager pointer is 0x600001e60d80
复制代码

我们知道,所有对象都有其对应的 isa 指针,那么引入 Tagged Pointer 会对 isa 指针产生影响。

我们看下对象中的 Tagged Pointer 的使用

inline bool 
objc_object::isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}
复制代码

那么如何判断是否是 Tagged Pointer 的对象:

  1. 看对象。前面说到 Tagged Pointer 专门用来存储小的对象,这些对象有 NSDateNSNumberNSString
  2. 自己设置。在环境变量中设置 OBJC_DISABLE_TAGGED_POINTERSYES 表示强制不启用 Tagged Pointer

isa指针

isa的本质——isa_t联合体

objc_object 这个结构体中定义了 isa 指针:

struct objc_object {
    isa_t isa;
}

//isa_t的定义
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        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
    struct {
        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

// SUPPORT_PACKED_ISA
#endif


#if SUPPORT_INDEXED_ISA

# if  __ARM_ARCH_7K__ >= 2

#   define ISA_INDEX_IS_NPI      1
#   define ISA_INDEX_MASK        0x0001FFFC
#   define ISA_INDEX_SHIFT       2
#   define ISA_INDEX_BITS        15
#   define ISA_INDEX_COUNT       (1 << ISA_INDEX_BITS)
#   define ISA_INDEX_MAGIC_MASK  0x001E0001
#   define ISA_INDEX_MAGIC_VALUE 0x001C0001
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t indexcls          : 15;
        uintptr_t magic             : 4;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 7;
#       define RC_ONE   (1ULL<<25)
#       define RC_HALF  (1ULL<<6)
    };

# else
#   error unknown architecture for indexed isa
# endif

// SUPPORT_INDEXED_ISA
#endif

};
复制代码

这里定义了很多环境,我们主要看64位CPU( if __arm64__ )的定义:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        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)
    };
复制代码

nonpointer

该变量占用 1bit 内存空间,可以有两个值: 01 ,分别代表不同的 isa_t 的类型:

  1. 0 表示 isa_t 没有开启指针优化,不使用 isa_t 中定义的结构体。访问 objc_objectisa 会直接返回 isa_t 结构中的 cls 变量, cls 变量会指向对象所属的类的结构;
  2. 1 表示 isa_t 开启指针优化,不能直接访问 objc_objectisa 成员变量 (此时的isa而是一个 Tagged Pointer ), isa 中包含了类信息、对象的引用计数等信息。

has_assoc

该变量与对象的关联引用有关。

has_cxx_dtor

表示该对象是否有析构函数,如果有析构函数,则需要做析构逻辑;如果没有,则可以更快的释放对象。

shiftcls

在开启指针优化的情况下,用33bits存储类指针的值。在 initIsa() 中有 newisa.shiftcls = (uintptr_t)cls >> 3; 这样的代码,就是将类指针存在isa中。

magic

用于调试器判断当前对象是真的对象还是没有初始化的空间

weakly_referenced

标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。

deallocating

标志对象是否正在释放内存。

extra_rc

extra_rc 占了19位,可以存储的最大引用计数应该是 (为什么要这么写是因为 extra_rc 保存的是值-1,而在获取引用计数的时候会+1),当超过它就需要 SideTablesSideTables 内包含一个 RefcountMap ,用来保存引用计数,根据对象地址取出其引用计数,类型是 size_t

这里有个问题,为什么既要使用一个 extra_rc 又要使用 SideTables

可能是因为历史问题,以前cpu是 32 位的, isa 中能存储的引用计数就只有 。因此在 arm64 下,引用计数通常是存储在 isa 中的。

更具体的会在retain操作的时候讲到。

has_sidetable_rc

当引用计数器过大的时候,那么引用计数会存储在一个叫 SideTable 的类的属性中。

ISA_MAGIC_MASK

通过掩码方式获取 magic 值。

ISA_MASK

通过掩码方式获取 isa 的类指针值。

RC_ONE 和 RC_HALF

用于引用计数的相关计算。

isa_t联合体里面的宏

SUPPORT_PACKED_ISA

表示平台是否支持在 isa 指针中插入除 Class 之外的信息。

  1. 如果支持就会将 Class 信息放入 isa_t 定义的struct内,并附上一些其他信息,例如上面的 nonpointer 等等;
  2. 如果不支持,那么不会使用 isa_t 内定义的 struct ,这时 isa_t 只使用 cls (Class 指针)。

在iOS以及MacOSX设备上,SUPPORT_PACKED_ISA定义为1。

SUPPORT_INDEXED_ISA

SUPPORT_INDEXED_ISA 表示 isa_t 中存放的 Class 信息是 Class 的地址。在 initIsa() 中有:

#if SUPPORT_INDEXED_ISA
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
复制代码

iOS设备上SUPPRT_INDEXED_ISA是0。

isa类型有关的宏

SUPPORT_NONPOINTER_ISA

用于标记是否支持优化的 isa 指针,其定义:

#if !SUPPORT_INDEXED_ISA  &&  !SUPPORT_PACKED_ISA
#   define SUPPORT_NONPOINTER_ISA 0
#else
#   define SUPPORT_NONPOINTER_ISA 1
#endif
复制代码

那如何判断是否支持优化的isa指针?

  1. 已知iOS系统的 SUPPORT_PACKED_ISA 为1, SUPPORT_INDEXED_ISA 为0,从上面的定义可以看出,iOS系统的 SUPPORT_NONPOINTER_ISA 为1;
  2. 在环境变量中设置 OBJC_DISABLE_NONPOINTER_ISA

这里需要注意的是,即使是64位环境下,优化的 isa 指针并不是就一定会存储引用计数,毕竟用19bit iOS 系统)保存引用计数不一定够。另外这19位保存的是引用计数的值减一。

SideTable

在源码中我们经常会看到 SideTable 这个结构体。它的定义:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    //省略其他代码
};
复制代码

从上面可知, SideTable 中有三个成员变量:

  1. slock 用于保证原子操作的自旋锁;
  2. refcnts 用于引用计数的 hash 表;
  3. weak_table 用于weak引用的 hash 表。

这里我们主要看引用计数的哈希表。 RefcountMap 的定义: typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

可以看出 SideTable 用来保存引用计数具体是用 DenseMap 这个类(在 llvm-DenseMap.h 中)实现的。 DenseMapDisguisedPtr<objc_object>keysize_tvalueDisguisedPtr 类是对 objc_object * 指针及其一些操作进行的封装,其内容可以理解为对象的内存地址,值的类型为 __darwin_size_t ,在 darwin 内核一般等同于 unsigned long 。其实这里保存的值也是等于引用计数减1。

引用计数的获取

通过 retainCount 可以获取到引用计数器,其定义:

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

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

    sidetable_lock();
    //加锁,用汇编指令ldxr来保证原子性
    isa_t bits = LoadExclusive(&isa.bits);
    //释放锁,使用汇编指令clrex
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

//sidetable_retainCount()函数实现
uintptr_t
objc_object::sidetable_retainCount() {
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}
复制代码

从上面的代码可知,获取引用计数的时候分为三种情况:

  1. Tagged Pointer 的话,直接返回isa本身;
  2. Tagged Pointer ,且开启了指针优化,此时引用计数先从 extra_rc 中去取(这里将取出来的值进行了+1操作,所以在存的时候需要进行-1操作),接着判断是否有 SideTable ,如果有再加上存在 SideTable 中的计数;
  3. Tagged Pointer ,没有开启了指针优化,使用 sidetable_retainCount() 函数返回。

手动操作对引用计数的影响

objc_retain()

#if __OBJC2__
__attribute__((aligned(16)))
id 
objc_retain(id obj) {
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}
#else
id objc_retain(id obj) { return [obj retain]; }
复制代码

首先判断是否是 Tagged Pointer 的对象,是就返回对象本身,否则通过对象的 retain() 返回。

inline id 
objc_object::retain() {
    assert(!isTaggedPointer());
    // hasCustomRR方法检查类(包括其父类)中是否含有默认的方法
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
复制代码

首先判断是否是 Tagged Pointer ,这个函数并不希望处理的对象是 Tagged Pointer ;接着通过 hasCustomRR 函数检查类(包括其父类)中是否含有默认的方法,有则调用自定义的方法;如果没有,调用 rootRetain() 函数。

ALWAYS_INLINE id 
objc_object::rootRetain() {
    return rootRetain(false, false);
}

//将源码精简后的逻辑
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;
    
    isa_t oldisa;
    isa_t newisa;

    // 加锁,用汇编指令ldxr来保证原子性
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    
    if (newisa.nonpointer = 0) {
        // newisa.nonpointer = 0说明所有位数都是地址值
        // 释放锁,使用汇编指令clrex
        ClearExclusive(&isa.bits);
        
        // 由于所有位数都是地址值,直接使用SideTable来存储引用计数
        return sidetable_retain();
    }
    
    // 存储extra_rc++后的结果
    uintptr_t carry;
    // extra_rc++
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
    
    if (carry == 0) {
        // extra_rc++后溢出,进位到side table
        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
        sidetable_addExtraRC_nolock(RC_HALF);
    }
        
    // 将newisa写入isa
    StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)
    return (id)this;
}
复制代码

从上面的可以看到:

  • Tagged Pointer 直接返回对象本身;
  • newisa.nonpointer == 0 没有开启指针优化,直接使用 SideTable 来存储引用计数;
  • 开启指针优化,使用isa的 extra_rc 保存引用计数,当超出的时候,使用 SideTable 来存储额外的引用计数。

objc_release()

#if __OBJC2__
__attribute__((aligned(16)))
void 
objc_release(id obj) {
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
#else
void objc_release(id obj) { [obj release]; }
#endif

//release()源码
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
复制代码

这边的逻辑和 objc_retain() 的逻辑一致,所以直接看 rootRelease() 函数,与上面一样,下面的代码也是经过精简的。

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    if (isTaggedPointer()) return false;
    
    isa_t oldisa;
    isa_t newisa;
    
retry:
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    if (newisa.nonpointer == 0) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return sidetable_release(performDealloc);
    }
    
    uintptr_t carry;
    // extra_rc--
    newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
    if (carry == 0) {
        // 需要从SideTable借位,或者引用计数为0
        goto underflow;
    }
    
    // 存储引用计数到isa
    StoreReleaseExclusive(&isa.bits,
                          oldisa.bits, newisa.bits)
    return false;
    
underflow:
    // 从SideTable借位
    // 或引用计数为0,调用delloc
    
    // 此处省略N多代码
    // 总结一下:修改Side Table与extra_rc,
    
    // 引用计数减为0时,调用dealloc
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}
复制代码

从上面可以看到:

  1. 判断是否是 Tagged Pointer 的对象,是就直接返回;
  2. 没有开启指针优化,使用 SideTable 存储的引用计数-1;
  3. 开启指针优化,使用isa的 extra_rc 保存的引用计数-1,当 carry==0 表示需要从 SideTable 保存的引用计数也用完了或者说引用计数为0,所以执行最后一步;
  4. 最后调用 dealloc ,所以这也回答了之前的《OC内存管理--对象的生成与销毁》中 dealloc 什么时候被调用这个问题,在 rootRelease(bool performDealloc, bool handleUnderflow) 函数中如果判断出引用计数为0了,就要调用 dealloc 函数了。

总结

  1. 引用计数存在什么地方?

    • Tagged Pointer 不需要引用计数,苹果会直接将对象的指针值作为引用计数返回;
    • 开启了指针优化( nonpointer == 1 )的对象其引用计数优先存在 isaextra_rc 中,大于 524288 便存在 SideTableRefcountMap 或者说是 DenseMap 中;
    • 没有开启指针优化的对象直接存在 SideTableRefcountMap 或者说是 DenseMap 中。
  2. retain/release的实质

    • Tagged Pointer 不参与 retain / release
    • 找到引用计数存储区域,然后+1/-1,并根据是否开启指针优化,处理进位/借位的情况;
    • 当引用计数减为0时,调用 dealloc 函数。
  3. isa是什么

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    复制代码
    • 首先要知道,isa指针已经不一定是类指针了,所以需要用 ISA() 获取类指针;
    • Tagged Pointer 的对象没有 isa 指针,有的是 isa_t 的结构体;
    • 其他对象的isa指针还是类指针。
  4. 对象的值是什么

    Tagged Pointer
    Tagged Pointer
    

补充: 一道多线程安全的题目

以下代码运行结果

@property (nonatomic, strong) NSString *target;
//....

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
    });
}
复制代码

答案:Crash。

Crash的原因:过度释放。

关键知识点:

  1. 全局队列和自定义并行队列在异步执行的时候会根据任务系统决定开辟线程个数;
  2. target 使用 strong 进行了修饰,Block是会截获对象的修饰符的;
  3. 即使使用 _target 效果也是一样,因为默认使用 strong 修饰符隐式修饰;
  4. strong 的源代码如下:
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
复制代码

假设这个并发队列创建了两个线程A和B,由于是异步的,可以同时执行。因此会出现这么一个场景,在线程A中,代码执行到了 objc_retain(obj) ,但是在线程B中可能执行到了 objc_release(prev) ,此时 prev 已经被释放了。那么当A在执行到 objc_release(prev) 就会过度释放,从而导致程序crash。

解决方法:

DISPATCH_OBJ_BARRIER_BIT

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

编码

编码

查尔斯•佩措尔德 (Charles Petzold) / 左飞、薛佟佟 / 电子工业出版社 / 2012-10-1 / 59.00元

编码:隐匿在计算机软硬件背后的语言,ISBN:9787121181184,作者:(美)佩措尔德(Petzold,C.)著 左飞,薛佟佟译一起来看看 《编码》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具