Objective-C 引用计数:不讲用法,只说原理

  • 本文所使用的源码为 objc4-647 和 CF-1153.18
  • 实际上这是我本周实习周报的一部分,写的比较仓促,如有差错还请多多指正。
  • 不讲用法,只说原理。

引用计数如何存储

有些对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回;如果当前设备是 64 位环境并且使用 Objective-C 2.0,那么“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数;否则 Runtime 会使用一张散列表来管理引用计数。

其实还有一种情况会改变引用计数的存储策略,那就是是否使用垃圾回收(用UseGC属性判断),但这种早已弃用的东西就不要管了,而且初始化垃圾回收机制的 void gc_init(BOOL wantsGC) 方法一直被传入 NO。

TaggedPointer

判断当前对象是否在使用 TaggedPointer 是看标志位是否为 1 :


1

2

3

4

5

6

7

8

9

10

11

12

13

#if SUPPORT_MSB_TAGGED_POINTERS

#   define TAG_MASK (1ULL<<63)

#else

#   define TAG_MASK 1

inline bool 

objc_object::isTaggedPointer() 

{

#if SUPPORT_TAGGED_POINTERS

    return ((uintptr_t)this & TAG_MASK);

#else

    return false;

#endif

}

id 其实就是 objc_object * 的简写(typedef struct objc_object *id;),它的 isTaggedPointer() 方法经常会在操作引用计数时用到,因为这决定了存储引用计数的策略。

isa 指针(NONPOINTER_ISA)

用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。isa 指针第一位为 1 即表示使用优化的 isa 指针,这里列出不同架构下的 64 位环境中 isa 指针结构:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

union isa_t 

{

    isa_t() { }

    isa_t(uintptr_t value) : bits(value) { }

    Class cls;

    uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA

# if __arm64__

#   define ISA_MASK        0x00000001fffffff8ULL

#   define ISA_MAGIC_MASK  0x000003fe00000001ULL

#   define ISA_MAGIC_VALUE 0x000001a400000001ULL

    struct {

        uintptr_t indexed           : 1;

        uintptr_t has_assoc         : 1;

        uintptr_t has_cxx_dtor      : 1;

        uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000

        uintptr_t magic             : 9;

        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  0x0000000000000001ULL

#   define ISA_MAGIC_VALUE 0x0000000000000001ULL

    struct {

        uintptr_t indexed           : 1;

        uintptr_t has_assoc         : 1;

        uintptr_t has_cxx_dtor      : 1;

        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000

        uintptr_t weakly_referenced : 1;

        uintptr_t deallocating      : 1;

        uintptr_t has_sidetable_rc  : 1;

        uintptr_t extra_rc          : 14;

#       define RC_ONE   (1ULL<<50)

#       define RC_HALF  (1ULL<<13)

    };

# else

    // Available bits in isa field are architecture-specific.

#   error unknown architecture

# endif

// SUPPORT_NONPOINTER_ISA

#endif

};

SUPPORT_NONPOINTER_ISA 用于标记是否支持优化的 isa 指针,其字面含义意思是 isa 的内容不再是类的指针了,而是包含了更多信息,比如引用计数,析构状态,被其他 weak 变量引用情况。判断方法也是根据设备类型:


1

2

3

4

5

6

// Define SUPPORT_NONPOINTER_ISA=1 to enable extra data in the isa field.

#if !__LP64__  ||  TARGET_OS_WIN32  ||  TARGET_IPHONE_SIMULATOR  ||  __x86_64__

#   define SUPPORT_NONPOINTER_ISA 0

#else

#   define SUPPORT_NONPOINTER_ISA 1

#endif

综合看来目前只有 arm64 架构的设备支持,下面列出了 isa 指针中变量对应的含义:

在 64 位环境下,优化的 isa 指针并不是就一定会存储引用计数,毕竟用 19bit (iOS 系统)保存引用计数不一定够。需要注意的是这 19 位保存的是引用计数的值减一。has_sidetable_rc 的值如果为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中,后面会详细讲。

散列表

散列表来存储引用计数具体是用 DenseMap 类来实现,这个类中包含好多映射实例到其引用计数的键值对,并支持用 DenseMapIterator 迭代器快速查找遍历这些键值对。接着说键值对的格式:键的类型为 DisguisedPtr<objc_object>,DisguisedPtr 类是对 objc_object * 指针及其一些操作进行的封装,目的就是为了让它给人看起来不会有内存泄露的样子(真是心机裱),其内容可以理解为对象的内存地址;值的类型为 __darwin_size_t,在 darwin 内核一般等同于 unsigned long。其实这里保存的值也是等于引用计数减一。使用散列表保存引用计数的设计很好,即使出现故障导致对象的内存块损坏,只要引用计数表没有被破坏,依然可以顺藤摸瓜找到内存块的位置。

之前说引用计数表是个散列表,这里简要说下散列的方法。有个专门处理键的 DenseMapInfo 结构体,它针对 DisguisedPtr 做了些优化匹配键值速度的方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

struct DenseMapInfo {

  static inline DisguisedPtr getEmptyKey() {

    return DisguisedPtr((T*)(uintptr_t)-1);

  }

  static inline DisguisedPtr getTombstoneKey() {

    return DisguisedPtr((T*)(uintptr_t)-2);

  }

  static unsigned getHashValue(const T *PtrVal) {

      return ptr_hash((uintptr_t)PtrVal);

  }

  static bool isEqual(const DisguisedPtr &LHS, const DisguisedPtr &RHS) {

      return LHS == RHS; 

  }

};

当然这里的哈希算法会根据是否为 64 位平台来进行优化,算法具体细节就不深究了,我总觉得苹果在这里的 hardcode 是随便写的:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

#if __LP64__

static inline uint32_t ptr_hash(uint64_t key)

{

    key ^= key >> 4;

    key *= 0x8a970be7488fda55;

    key ^= __builtin_bswap64(key);

    return (uint32_t)key;

}

#else

static inline uint32_t ptr_hash(uint32_t key)

{

    key ^= key >> 4;

    key *= 0x5052acdb;

    key ^= __builtin_bswap32(key);

    return key;

}

#endif

再介绍下 SideTable 这个类,它用于管理引用计数表和后面将要提到的 weak 表,并使用 spinlock_lock 自旋锁来防止操作表结构时可能的竞态条件。

获取引用计数

在非 ARC 环境可以使用 retainCount 方法获取某个对象的引用计数,其会调用 objc_object 的 rootRetainCount() 方法:


1

2

3

- (NSUInteger)retainCount {

    return ((id)self)->rootRetainCount();

}

在 ARC 时代除了使用 Core Foundation 库的 CFGetRetainCount() 方法,也可以使用 Runtime 的 _objc_rootRetainCount(id obj) 方法来获取引用计数,此时需要引入头文件。这个函数也是调用 objc_object 的 rootRetainCount() 方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

inline uintptr_t 

objc_object::rootRetainCount()

{

    assert(!UseGC);

    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();

    isa_t bits = LoadExclusive(&isa.bits);

    if (bits.indexed) {

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

}

rootRetainCount() 方法对引用计数存储逻辑进行了判断,因为 TaggedPointer 前面已经说过了,可以直接获取引用计数;64 位环境优化的 isa 指针前面也说过了,所以这里的重头戏是在 TaggedPointer 无法使用时调用的 sidetable_retainCount() 方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

uintptr_t

objc_object::sidetable_retainCount()

{

    SideTable *table = SideTable::tableForPointer(this);

    size_t refcnt_result = 1;

    

    spinlock_lock(&table->slock);

    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;

    }

    spinlock_unlock(&table->slock);

    return refcnt_result;

}

sidetable_retainCount() 方法的逻辑就是先从 SideTable 的静态方法获取当前实例对应的 SideTable 对象,其 refcnts 属性就是之前说的存储引用计数的散列表,这里将其类型简写为 RefcountMap:


1

typedef objc::DenseMap RefcountMap;

然后在引用计数表中用迭代器查找当前实例对应的键值对,获取引用计数值,并在此基础上 +1 并将结果返回。这也就是为什么之前说引用计数表存储的值为实际引用计数减一。

需要注意的是为什么这里把键值对的值做了向右移位操作(it->second >> SIDE_TABLE_RC_SHIFT):


1

2

3

4

5

6

7

8

9

10

11

12

#ifdef __LP64__

#   define WORD_BITS 64

#else

#   define WORD_BITS 32

#endif

// The order of these bits is important.

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)

#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit

#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit

#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2

#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)RefcountMap

可以看出值的第一个 bit 表示该对象是否有过 weak 对象,如果没有,在析构释放内存时可以更快;第二个 bit 表示该对象是否正在析构。从第三个 bit 开始才是存储引用计数数值的地方。所以这里要做向右移两位的操作,而对引用计数的 +1 和 -1 可以使用 SIDE_TABLE_RC_ONE,还可以用 SIDE_TABLE_RC_PINNED 来判断是否引用计数值有可能溢出。

当然不能够完全信任这个 _objc_rootRetainCount(id obj) 函数,对于已释放的对象以及不正确的对象地址,有时也返回 “1”。它所返回的引用计数只是某个给定时间点上的值,该方法并未考虑到系统稍后会把自动释放吃池清空,因而不会将后续的释放操作从返回值里减去。clang 会尽可能把 NSString 实现成单例对象,其引用计数会很大。如果使用了 TaggedPointer,NSNumber 的内容有可能就不再放到堆中,而是直接写在宽敞的64位栈指针值里。其看上去和真正的 NSNumber 对象一样,只是使用 TaggedPointer 优化了下,但其引用计数可能不准确。

修改引用计数

retain 和 release

在非 ARC 环境下可以使用 retain 和 release 方法对引用计数进行加一减一操作,它们分别调用了 _objc_rootRetain(id obj) 和 _objc_rootRelease(id obj) 函数,不过后两者在 ARC 环境下也可使用。最后这两个函数又会调用 objc_object 的下面两个方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

inline id 

objc_object::rootRetain()

{

    assert(!UseGC);

    if (isTaggedPointer()) return (id)this;

    return sidetable_retain();

}

inline bool 

objc_object::rootRelease()

{

    assert(!UseGC);

    if (isTaggedPointer()) return false;

    return sidetable_release(true);

}

这样的实现跟获取引用计数类似,先是看是否支持 TaggedPointer(毕竟数据存在栈指针而不是堆中,栈的管理本来就是自动的),否则去操作 SideTable 中的 refcnts 属性,这与获取引用计数策略类似。sidetable_retain() 将 引用计数加一后返回对象,sidetable_release() 返回是否要执行 dealloc 方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

bool 

objc_object::sidetable_release(bool performDealloc)

{

#if SUPPORT_NONPOINTER_ISA

    assert(!isa.indexed);

#endif

    SideTable *table = SideTable::tableForPointer(this);

    bool do_dealloc = false;

    if (spinlock_trylock(&table->slock)) {

        RefcountMap::iterator it = table->refcnts.find(this);

        if (it == table->refcnts.end()) {

            do_dealloc = true;

            table->refcnts[this] = SIDE_TABLE_DEALLOCATING;

        else if (it->second < SIDE_TABLE_DEALLOCATING) {

            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don‘t change it.

            do_dealloc = true;

            it->second |= SIDE_TABLE_DEALLOCATING;

        else if (! (it->second & SIDE_TABLE_RC_PINNED)) {

            it->second -= SIDE_TABLE_RC_ONE;

        }

        spinlock_unlock(&table->slock);

        if (do_dealloc  &&  performDealloc) {

            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);

        }

        return do_dealloc;

    }

    return sidetable_release_slow(table, performDealloc);

}

看到这里知道为什么在存储引用计数时总是真正的引用计数值减一了吧。因为 release 本来是要将引用计数减一,所以存储引用计数时先预留了个“一”,在减一之前先看看存储的引用计数值是否为 0 (it->second < SIDE_TABLE_DEALLOCATING),如果是,那就将对象标记为“正在析构”(it->second |= SIDE_TABLE_DEALLOCATING),并发送 dealloc 消息,返回 YES;否则就将引用计数减一(it->second -= SIDE_TABLE_RC_ONE)。这样做避免了负数的产生。

除此之外,Core Foundation 库中也提供了增减引用计数的方法。比如在使用 Toll-Free Bridge 转换时使用的 CFBridgingRetain 和 CFBridgingRelease 方法,其本质是使用 __bridge_retained 和 __bridge_transfer 告诉编译器此处需要如何修改引用计数:


1

2

3

4

5

6

NS_INLINE CF_RETURNS_RETAINED CFTypeRef __nullable CFBridgingRetain(id __nullable X) {

    return (__bridge_retained CFTypeRef)X;

}

NS_INLINE id __nullable CFBridgingRelease(CFTypeRef CF_CONSUMED __nullable X) {

    return (__bridge_transfer id)X;

}

此外 Objective-C 很多实现是靠 Core Foundation Runtime 来实现, Objective-C Runtime 源码中有些地方明确注明:”// Replaced by CF“,那就是意思说这块任务被 Core Foundation 库接管了。当然 Core Foundation 有一部分是开源的。还有一些 Objective-C Runtime 函数的实现被诸如 ObjectAlloc 和 NSZombie 这样的内存管理工具所替代:


1

2

3

4

5

6

7

8

9

10

11

12

// Replaced by ObjectAlloc

+ (id)allocWithZone:(struct _NSZone *)zone {

    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);

}

// Replaced by CF (throws an NSException)

+ (id)init {

    return (id)self;

}

// Replaced by NSZombies

- (void)dealloc {

    _objc_rootDealloc(self);

}

alloc, new, copy, mutableCopy

根据编译器的约定,这以这四个单词开头的方法都会使引用计数加一。而 new 相当于调用 alloc 后再调用 init:


1

2

3

4

5

6

7

8

9

10

11

id

_objc_rootAlloc(Class cls)

{

    return callAlloc(cls, false/*checkNil*/true/*allocWithZone*/);

}

+ (id)alloc {

    return _objc_rootAlloc(self);

}

+ (id)new {

    return [callAlloc(self, false/*checkNil*/) init];

}

可以看出 alloc 和 new 最终都会调用 callAlloc,默认使用 Objective-C 2.0 且忽视垃圾回收和 NSZone,那么后续的调用顺序依次是为:


1

2

3

class_createInstance()

_class_createInstanceFromZone()

calloc()

calloc() 函数相比于 malloc() 函数的优点是它将分配的内存区域初始化为0,相当于 malloc() 后再用 memset() 方法初始化一遍。

copy 和 mutableCopy 都是基于 NSCopying 和 NSMutableCopying 方法约定,分别调用各类自己实现的 copyWithZone: 和 mutableCopyWithZone: 方法。这些方法无论实现方式是深拷贝还是浅拷贝,都会增加引用计数。(有些类的策略是懒拷贝,只增加引用计数但并不真的拷贝,等对象内容发生变化时再拷贝一份出来,比如 NSArray)。

在 retain 方法加符号断点会发现 alloc, new, copy, mutableCopy 这四个方法都会通过 Core Foundation 的 CFBasicHashAddValue() 函数来调用 retain 方法。其实 CF 有个修改和查看引用计数的入口函数 __CFDoExternRefOperation,在 CFRuntime.c 文件中实现。

autorelease

本想贴上一堆 Runtime 中关于自动释放池的源码然后说上一大堆,然后发现了太阳神的这篇黑幕背后的Autorelease把我想说的都说了,把我不知道的也说了,简直太屌了。

其实通过看源码可以知道好多细节,没事点进去各种宏定义往往会得到惊喜:哇,原来是这么回事,XX 就是 XX 之类。。。

------------都是代码界的码农,让大家一起成长

时间: 2024-07-29 22:52:48

Objective-C 引用计数:不讲用法,只说原理的相关文章

Netty中ByteBuf的引用计数线程安全的实现原理

原文链接 Netty中ByteBuf的引用计数线程安全的实现原理 ByteBuf 实现了ReferenceCounted 接口,实现了引用计数接口,该接口的retain(int) 方法为了保证线程安全使用了自旋锁来确保操作安全,那么选择了比较重要的实现类AbstractReferenceCountedByteBuf 来查看这一特性. 在JDK 1.5 之后,JDK的并发包提供了Atomic* 的相关类,来帮助开发者更好的完成并发操作,这里我们学习使用CAS来实现线程安全,CAS就是Compose

swift详解之九---------------自动引用计数、循环引用

自动引用计数.循环引用(这个必须理解,必须看) 注:本文详细介绍自动引用计数,以及各种循环引用问题.一网打尽! 1. 自动引用计数原理 Swift 使用ARC机制来跟踪和管理你的内存,一般情况下,Swift 的内存管理机制会一直起着作用,你无须自己来考虑内存的管理.ARC 会在类的实例不再被使用时,自动释放其占用的内存. 然而,在少数情况下,ARC 为了能帮助你管理内存,需要更多的关于你的代码之间关系的信息.本章描述了这些情况,并且为你示范怎样启用 ARC 来管理你的应用程序的内存. 为了确保在

深拷贝&amp;浅拷贝&amp;引用计数&amp;写时拷贝

(1).浅拷贝: class String { public: String(const char* str="") :_str(new char[strlen(str)+1]) { strcpy(_str,str); } ~String() { if(NULL!=_str) { delete[] _str; _str=NULL; } } private: char* _str; }; int main() { String s1("hello"); String

Objective-C中的引用计数

导言 Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器.如果想使某个对象继续存活,那就递增其引用计数:用完了之后,就递减其计数.计数为0,就表示没人关注此对象了,于是,就可以把它销毁. 从Mac OS X 10.8开始,“垃圾收集器”(garbage collector)已经正式废弃了,以Objective-C代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集.因此,掌握引用计数机制对于学好Objective-C来说十分重要.

引用计数

在引用计数中,每一个对象负责维护对象所有引用的计数值.当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减.当引用计数到零时,该对象就将释放占有的资源.中文名引用计数原 因程序调试原 理每一个对象负责维护对象所有引用的计数值类 型最直观的垃圾收集策略目录1简介2引用计数的使用? 原因? 规则? 接口? 调试? 优化? 规则1简介编辑 最直观的垃圾收集策略是引用计数.引用计数很简单,但是需要编译器的重要配合,并且增加了赋值函数 (mutator) 的开销(这个术语是针对用户

ARC自动引用计数

启动自动引用计数选项. 选择项目的属性文件 --> 搜索 automatic Reference --> Objective-C Automatic Reference Counting --> Yes ARC 和手动管理内存的区别. ARC 并不是GC在运行中判断引用计数是否为0,从而清除内存.而是在代码编译之前通过静态分析工具Analyze自动生成内存管理代码. 开启ARC后,不能再使用retain等系列手动内存管的方法,可以重写dealloc方法但不能再方法中[super deal

Objective-C内存管理之引用计数

初学者在学习Objective-c的时候,很容易在内存管理这一部分陷入混乱状态,很大一部分原因是没有弄清楚引用计数的原理,搞不明白对象的引用数量,这样就当然无法彻底释放对象的内存了,苹果官方文档在内存管理这一部分说的非常简单,只有三条准则: 当你使用new.alloc或copy方法创建一个对象时,该对象的保留指针为1,当不再使用该对象的时候,你应该想该对象发送一条release或autorelease消息,这样,该对象在其寿命结束时将被销毁. 当你通过其他方法获得一个对象时,假设该对象的保留计数

python的引用计数分析(二)

python所有对象引用计数被减少1的情况: 一.对象的别名被赋予新的对象; a = 23345455 # 增加了一个引用 b = a # 增加了一个引用 print(sys.getrefcount(a)) b = 1.4 # 减少了一个23345455整数的引用 print(sys.getrefcount(a)) 结果:3:2 二.对象的别名被显式销毁; a = 23345455 # 增加了一个引用 b = a # 增加了一个引用 list = [a, b] # 增加了2个引用 del a p

进击的雨燕--------------自动引用计数

Swift 使用自动引用计数(ARC)机制来跟踪和管理你的应用程序的内存.通常情况下,Swift 内存管理机制会一直起作用,你无须自己来考虑内存的管理.ARC 会在类的实例不再被使用时,自动释放其占用的内存. 然而,在少数情况下,ARC 为了能帮助你管理内存,需要更多的关于你的代码之间关系的信息.本章描述了这些情况,并且为你示范怎样启用 ARC 来管理你的应用程序的内存. 注意:引用计数仅仅应用于类的实例.结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递. 自动引用计数的工