Objc源码之引用计数实现

Objc源码之对象创建alloc和init
Objc源码之initialize实现
Objc源码之Load方法实现
Objc源码之NSObject和isa
Objc源码之引用计数实现
objc源码之Method消息发送

前言

   我们都知道OC的内存管理是引用计数,可是对象的引用计数是存储在哪里的呢?今天我们就从源码入手,来揭开这个谜底,下面我们就从对象创建开始说起。

一、从alloc和retainCount引用计数

首先我们看下retainCount方法,这个是获取引用计数的方法:

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

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

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    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();
}

看到这里,我们会发现引用计数包含三部分:
1. isTaggedPointer,是否是Tagged Pointer类型
2. nonpointer 是否是优化的isa指针。
3. sidetable_retainCount 其它类型

1.isTaggedPointer的情况

Tagged Pointer是苹果在64位系统之后,用来优化内存的。
1.Tagged Pointer专门用来存储小的对象,例如NSString、NSNumber、NSDate等;
2.Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free;
3.在内存读取上有着3倍的效率,创建时比以前快106倍。
具体可以看看这个iOS Tagged Pointer (源码阅读必备知识) - 掘金

总结就是isTaggedPointer情况下引用计数返回的是对象本身。

2.nonpointer优化的情况

nonpointer表示是否开启指针优化,

  • 0表示isa_t没有开启指针优化,不使用isa_t中定义的结构体。
  • 1表示isa_t开启指针优化,不能直接访问objc_object的isa成员变量 ,isa中包含了类信息、对象的引用计数等信息。

要了解nonpointer,首先看alloc是怎么创建一个对象,其中nonpointerextra_rc都是走嗯么赋值的,下面是简化的alloc函数的调用过程:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id  _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
      ...
      id obj = class_createInstance(cls, 0);
      return obj;
      ...
}

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
        ...
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
        return obj;
}

inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
        ...
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
}

#   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

我们总结一下调用过程alloc->_objc_rootAlloc->callAlloc->class_createInstance->_class_createInstanceFromZone->initIsa
关于引用计数的部分,在最后初始化isa指针中initIsa,下面我们看下initIsa中的具体过程:

newisa.bits = ISA_MAGIC_VALUE;

在给bits赋值的时候,通过ISA_MAGIC_VALUE这个宏来赋值的,arm64下这个宏的值是0x000001a000000001ULL,通过ISA_BITFIELD,我们来看下具体含义:

#   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

extra_rc代表的就是对象的引用计数,它包含19位,nonpointer只包含一位,下面我们看下arm64下ISA_MAGIC_VALUE:

ISA_MAGIC_VALUE

紫色部分是extra_rc的值,我们看到extra_rc的值是0,我们知道,刚创建的对象,应用计数应该是1,但是这里怎么是0,那是因为extra_rc保存的是引用计数-1的值,在获取的时候,会进行+1.在arm64下nonpointer默认是1,默认开启isa指针优化的。

nonpointer情况下,我们可以看到rootRetainCount代码包含两部分:

    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

第一部分就是上面说的extra_rc,获取到extra_rc以后,在又会判断has_sidetable_rc的值。
has_sidetable_rc是用来做什么的?
has_sidetable_rc是用来判断extra_rc是否存储不下引用计数数量的,如果引用计数大于extra_rc的最大存储数量,那么就会超出的引用计数存储到SideTables,下面看下源码:

size_t objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

了解weak原理的话,应该会熟悉SideTables,这也是weak对象存储的地方,上面的代码是取的SideTablesrefcnts中以对象为key的值,这里面refcnts是一个哈希表。

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

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

总结:如果extra_rc能够存储下引用计数,就只使用extra_rc,如果存储不下,就会使用SideTables进行辅助存储。

3.sidetable_retainCount的情况

sidetable_retainCount是在没有开启指针优化的情况,对象的引用计数直接存储在SideTable中。

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情况,直接返回对象指针
2. 优化的isa指针,也就是nonpointer为1时,通过extra_rc和SideTables一起来管理引用计数。
3.未开启指针优化,也就是nonpointer为0时,直接使用SideTables来管理引用计数 。

参考:
objc4-750源码
OC内存管理--引用计数器
【译】采用Tagged Pointer的字符串
从 NSObject 的初始化了解 isa.md
isa详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容