[关闭]
@qidiandasheng 2022-08-16T13:23:20.000000Z 字数 10878 阅读 3465

细说weak(😁)

iOS理论


前言

我们在学习iOS的时候常常会遇到weak,而网上很多文章都有介绍过weak,基本上有一句是最常见的。weak为弱引用,不拥有对象,不增加对象的引用计数,当对象被释放后自动将指向对象的指针置为nil。但是我们要知其然而更知其所以然。下面我们来一起进入weak的世界。

Runtime如何实现weak属性

Runtime的源码是开源的我们先去Runtime源码地址下载源码,然后来一点一点对照分析。

简单点概括runtime实现weak属性就是:

对于注册为weak的对象,系统会把 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

初始化weak变量

当我们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在runtime源码中的NSObject.mm文件中。具体定义如下:

  1. id
  2. objc_initWeak(id *location, id newObj)
  3. {
  4. if (!newObj) {
  5. *location = nil;
  6. return nil;
  7. }
  8. return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
  9. (location, (objc_object*)newObj);
  10. }

这里的两个参数location表示weak指针的地址,newObj表示对象指针。我们看到当这个newObj是个空指针或者其指向的对象已经被释放了,那么*location = nil;,返回也是nil,则表示weak的初始化其实是失败的。


然后如果成功的话执行storeWeak函数并返回值。那我们来看看storeWeak的定义:

  1. template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
  2. static id
  3. storeWeak(id *location, objc_object *newObj)
  4. {
  5. assert(HaveOld || HaveNew);
  6. if (!HaveNew) assert(newObj == nil);
  7. Class previouslyInitializedClass = nil;
  8. id oldObj;
  9. SideTable *oldTable;
  10. SideTable *newTable;
  11. // Acquire locks for old and new values.
  12. // Order by lock address to prevent lock ordering problems.
  13. // Retry if the old value changes underneath us.
  14. retry:
  15. if (HaveOld) {
  16. oldObj = *location;
  17. oldTable = &SideTables()[oldObj];
  18. } else {
  19. oldTable = nil;
  20. }
  21. if (HaveNew) {
  22. newTable = &SideTables()[newObj];
  23. } else {
  24. newTable = nil;
  25. }
  26. //一些锁的操作
  27. SideTable::lockTwo<HaveOld, HaveNew>(oldTable, newTable);
  28. if (HaveOld && *location != oldObj) {
  29. SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
  30. goto retry;
  31. }
  32. // Prevent a deadlock between the weak reference machinery
  33. // and the +initialize machinery by ensuring that no
  34. // weakly-referenced object has an un-+initialized isa.
  35. if (HaveNew && newObj) {
  36. Class cls = newObj->getIsa();
  37. if (cls != previouslyInitializedClass &&
  38. !((objc_class *)cls)->isInitialized())
  39. {
  40. SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
  41. _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
  42. // If this class is finished with +initialize then we're good.
  43. // If this class is still running +initialize on this thread
  44. // (i.e. +initialize called storeWeak on an instance of itself)
  45. // then we may proceed but it will appear initializing and
  46. // not yet initialized to the check above.
  47. // Instead set previouslyInitializedClass to recognize it on retry.
  48. previouslyInitializedClass = cls;
  49. goto retry;
  50. }
  51. }
  52. // Clean up old value, if any.
  53. if (HaveOld) {
  54. weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
  55. }
  56. // Assign new value, if any.
  57. if (HaveNew) {
  58. newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
  59. (id)newObj, location,
  60. CrashIfDeallocating);
  61. // weak_register_no_lock returns nil if weak store should be rejected
  62. // Set is-weakly-referenced bit in refcount table.
  63. if (newObj && !newObj->isTaggedPointer()) {
  64. newObj->setWeaklyReferenced_nolock();
  65. }
  66. // Do not set *location anywhere else. That would introduce a race.
  67. *location = (id)newObj;
  68. }
  69. else {
  70. // No new value. The storage is not changed.
  71. }
  72. SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
  73. return (id)newObj;
  74. }

我们可以看到里面有一个比较重要的类SideTable,我们再来看看SideTable的定义:

  1. struct SideTable {
  2. spinlock_t slock;
  3. RefcountMap refcnts;
  4. weak_table_t weak_table;
  5. SideTable() {
  6. memset(&weak_table, 0, sizeof(weak_table));
  7. }
  8. ~SideTable() {
  9. _objc_fatal("Do not delete SideTable.");
  10. }
  11. void lock() { slock.lock(); }
  12. void unlock() { slock.unlock(); }
  13. bool trylock() { return slock.trylock(); }
  14. // Address-ordered lock discipline for a pair of side tables.
  15. template<bool HaveOld, bool HaveNew>
  16. static void lockTwo(SideTable *lock1, SideTable *lock2);
  17. template<bool HaveOld, bool HaveNew>
  18. static void unlockTwo(SideTable *lock1, SideTable *lock2);
  19. };

SideTable里有一个RefcountMap refcnts;weak_table_t weak_table;
RefcountMap里存储了一个对象的引用计数信息。而weak_table_t weak_table;维护和存储了一个对象的所有弱引用的信息。具体的结构定义可以看runtime源码中的objc-weak.h文件。


知道了这些定义,我们现在就来看看整个storeWeak函数的作用:

  1. if (HaveOld) {
  2. oldObj = *location;
  3. oldTable = &SideTables()[oldObj];
  4. } else {
  5. oldTable = nil;
  6. }
  7. if (HaveNew) {
  8. newTable = &SideTables()[newObj];
  9. } else {
  10. newTable = nil;
  11. }
  1. //移除老对象weak表中的信息
  2. if (HaveOld) {
  3. weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
  4. }
  5. // 在新对象的weak表中建立关联
  6. if (HaveNew) {
  7. newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
  8. (id)newObj, location,
  9. CrashIfDeallocating);
  10. // weak_register_no_lock returns nil if weak store should be rejected
  11. // Set is-weakly-referenced bit in refcount table.
  12. if (newObj && !newObj->isTaggedPointer()) {
  13. newObj->setWeaklyReferenced_nolock();
  14. }
  15. // 弱引用指针指向新对象
  16. *location = (id)newObj;
  17. }
  18. else {
  19. // No new value. The storage is not changed.
  20. }
  21. SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
  22. //返回新对象
  23. return (id)newObj;

weak引用指向的对象被释放

当weak引用指向的对象被释放时,其基本流程如下:

其中objc_release_objc_rootDeallocNSObject.mm中定义。

object_disposeobjc_destructInstanceclearDeallocatingobjc-runtime-new.mm文件中定义。

我们看源码中的定义其实就能知道最后clearDeallocating最关键的部分的是执行objc-weak.mm文件中的weak_clear_no_lock函数:

  1. void
  2. weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
  3. {
  4. objc_object *referent = (objc_object *)referent_id;
  5. weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
  6. if (entry == nil) {
  7. /// XXX shouldn't happen, but does with mismatched CF/objc
  8. //printf("XXX no entry for clear deallocating %p\n", referent);
  9. return;
  10. }
  11. // zero out references
  12. weak_referrer_t *referrers;
  13. size_t count;
  14. if (entry->out_of_line) {
  15. referrers = entry->referrers;
  16. count = TABLE_SIZE(entry);
  17. }
  18. else {
  19. referrers = entry->inline_referrers;
  20. count = WEAK_INLINE_COUNT;
  21. }
  22. for (size_t i = 0; i < count; ++i) {
  23. objc_object **referrer = referrers[i];
  24. if (referrer) {
  25. if (*referrer == referent) {
  26. *referrer = nil;
  27. }
  28. else if (*referrer) {
  29. _objc_inform("__weak variable at %p holds %p instead of %p. "
  30. "This is probably incorrect use of "
  31. "objc_storeWeak() and objc_loadWeak(). "
  32. "Break on objc_weak_error to debug.\n",
  33. referrer, (void*)*referrer, (void*)referent);
  34. objc_weak_error();
  35. }
  36. }
  37. }
  38. weak_entry_remove(weak_table, entry);
  39. }

这个函数的首要任务就是找出对象对应的weak_entry_t链表,然后挨个将弱引用置为nil。最后清理对象的记录。

结构体描述

SideTable

  1. struct SideTable {
  2. spinlock_t slock;
  3. RefcountMap refcnts;
  4. weak_table_t weak_table;
  5. }
  • spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable。
  • RefcountMap refcnts :用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
  • weak_table_t weak_table : 存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。

weak_table_t

  1. struct weak_table_t {
  2. weak_entry_t *weak_entries;
  3. size_t num_entries;
  4. uintptr_t mask;
  5. uintptr_t max_hash_displacement;
  6. };
  • weak_entries: hash数组,用来存储弱引用对象的相关信息weak_entry_t
  • num_entries: hash数组中的元素个数
  • mask:hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
  • max_hash_displacement:可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)

weak_table_t是一个典型的hash结构。weak_entries是一个动态数组,用来存储weak_entry_t类型的元素,这些元素实际上就是OC对象的弱引用信息。

weak_entry_t

weak_entry_t的结构也是一个hash结构,其存储的元素是弱引用对象指针的指针, 通过操作指针的指针,就可以使得weak 引用的指针在对象析构后,指向nil。

  1. #define WEAK_INLINE_COUNT 4
  2. #define REFERRERS_OUT_OF_LINE 2
  3. struct weak_entry_t {
  4. DisguisedPtr<objc_object> referent; // 被弱引用的对象
  5. // 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers
  6. union {
  7. struct {
  8. weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
  9. uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
  10. uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
  11. uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
  12. uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
  13. };
  14. struct {
  15. // out_of_line_ness field is low bits of inline_referrers[1]
  16. weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
  17. };
  18. };
  19. bool out_of_line() {
  20. return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
  21. }
  22. weak_entry_t& operator=(const weak_entry_t& other) {
  23. memcpy(this, &other, sizeof(other));
  24. return *this;
  25. }
  26. weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
  27. : referent(newReferent) // 构造方法,里面初始化了静态数组
  28. {
  29. inline_referrers[0] = newReferrer;
  30. for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
  31. inline_referrers[i] = nil;
  32. }
  33. }
  34. };

可以看到在weak_entry_t的结构定义中有联合体,在联合体的内部有定长数组inline_referrers[WEAK_INLINE_COUNT]和动态数组weak_referrer_t *referrers两种方式来存储弱引用对象的指针地址。通过out_of_line()这样一个函数方法来判断采用哪种存储方式。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT时,使用定长数组。当超过WEAK_INLINE_COUNT时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。

在ARC中模拟Weak功能

分析

我们知道weakunsafe_unretained的不同之处是,设置weak属性后,系统会在对象被释放后自动将这个指向对象的指针设为nil。而unsafe_unretained修饰的属性会在对象释放后产生悬空指针。

这里我们解释一下什么是空指针、悬空指针、野指针。

空指针:也就是weak修饰的指针,在对象释放后指针设为nil,也就成了空指针,在OC里给nil发送消息是不会崩溃的。

悬空指针:对象被释放后那块对象的内存已经失效了,此内存就是垃圾内存,但是还有指针指向这块垃圾内存,这个指针就是悬空指针。也就是unsafe_unretained修饰的指针在对象释放后会成为悬空指针,给悬空指针发送消息是会崩溃的。

野指针:某些编程语言允许未初始化的指针的存在,而这类指针即为野指针。

unsafe_unretained设置属性

1: 首先,创建一个ClassA类

2: 创建一个ClassB类,并在里面添加一个测试方法print,用于等下向ClassB实例对象发送消息,确认对象是否仍在使用

  1. - (void)print {
  2. NSLog(@"Object is %@", self);
  3. }

3: 给ClassA添加一个ClassB对象属性,并设置为unsafe_unretained

  1. @property (nonatomic, unsafe_unretained) ClassB *objectB;

4: 在main函数中添加如下代码:

  1. @autoreleasepool {
  2. ClassA *instanceA = [ClassA new];
  3. ClassB *instanceB = [ClassB new];
  4. instanceA.objectB = instanceB;
  5. [instanceA.objectB print];
  6. // release instanceB
  7. instanceB = nil;
  8. [instanceA.objectB print];
  9. }
  10. return 0;

5: 运行一下代码,不出意外的话,程序会挂掉,因为instanceA.objectB所指向的内存已经在instanceB = nil;时候被释放掉,instanceA.objectB仅指向一个悬空指针:

  1. 2016-03-09 15:25:10.427 WeakDemo[98402:1613532] Object is 0x7fa080d0f100
  2. 2016-03-09 15:25:10.432 WeakDemo[98402:1613532] Object is 0x7fa080d0f100
  3. Process finished with exit code 139

当然,也有可能不会挂,这取决于执行print函数时,instanceB所在的内存是否完全释放掉。

6: 如果将ClassA中的属性改为@property (nonatomic, weak) ClassB *objectB;则不会出现crash,这就是weak的作用了,objectB对象如果被释放掉,则该指针变为nil,而向nil发送消息是不会出现问题的。

给unsafe_unretained添加weak功能

demo代码这里给出了具体的实现代码。具体的原理我简单说一下:

就是创建了一个NSObjectcategorycategory里有一个方法传入一个block并使用runtime Associate方法关联一个delloc对象,这个对象在delloc的时候,会调用block。所以当NSObjec对象释放的时候,这个delloc对象也就会执行block,我们直接在block里面设置指针为nil就不会产生悬空指针了。


这里有一个问题,我们给NSObject对象关联的delloc对象是在什么时候delloc的呢?
我们看到对象销毁时流程是如下这样的:

runtime源码的objc-runtime-new.mm文件中我们看到objc_destructInstance的定义如下,可以看到里面有个_object_remove_assocations执行了移除关联对象的操作:

  1. void *objc_destructInstance(id obj)
  2. {
  3. if (obj) {
  4. // Read all of the flags at once for performance.
  5. bool cxx = obj->hasCxxDtor();
  6. bool assoc = !UseGC && obj->hasAssociatedObjects();
  7. bool dealloc = !UseGC;
  8. // This order is important.
  9. if (cxx) object_cxxDestruct(obj);
  10. if (assoc) _object_remove_assocations(obj);
  11. if (dealloc) obj->clearDeallocating();
  12. }
  13. return obj;
  14. }

NSObjectcategoryNSObject+deallocBlock.h

  1. @interface NSObject (deallocBlock)
  2. - (void)runBlockOnDealloc:(voidBlock)block;
  3. @end
  4. @implementation NSObject (deallocBlock)
  5. - (void)runBlockOnDealloc:(voidBlock)block {
  6. if (block) {
  7. DeallocBlock *deallocBlock = [[DeallocBlock alloc] initWithBlock:block];
  8. objc_setAssociatedObject(self, _cmd, deallocBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  9. }
  10. }
  11. @end

NSObjeccategory中要关联的对象的类:DeallocBlock.h

  1. typedef void (^voidBlock)();
  2. @interface DeallocBlock : NSObject
  3. @property(nonatomic, copy) voidBlock block;
  4. - (instancetype)initWithBlock:(voidBlock)block1;
  5. @end
  6. @implementation DeallocBlock
  7. - (instancetype)initWithBlock:(voidBlock)block1 {
  8. self = [super init];
  9. if (self) {
  10. _block = [block1 copy];
  11. }
  12. return self;
  13. }
  14. - (void)dealloc {
  15. if (_block) {
  16. NSLog(@"DeallocBlock dealloc!");
  17. _block();
  18. }
  19. }
  20. @end

使用就是如下面代码这样,重写classA的setObjectB方法:

  1. - (void)setObjectB:(ClassB *)objectB {
  2. _objectB = objectB;
  3. // 仅对objectB != nil case做处理
  4. if (_objectB) {
  5. [_objectB runBlockOnDealloc:^{
  6. NSLog(@"_objectB dealloc");
  7. _objectB = nil;
  8. }];
  9. }
  10. }

参考

Runtime的编译工程

Runtime如何实现weak属性?

如何实现ARC中weak功能?

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注