@qidiandasheng
2022-08-16T13:23:20.000000Z
字数 10878
阅读 4078
iOS理论
我们在学习iOS的时候常常会遇到weak,而网上很多文章都有介绍过weak,基本上有一句是最常见的。weak为弱引用,不拥有对象,不增加对象的引用计数,当对象被释放后自动将指向对象的指针置为nil。但是我们要知其然而更知其所以然。下面我们来一起进入weak的世界。
Runtime的源码是开源的我们先去Runtime源码地址下载源码,然后来一点一点对照分析。
简单点概括runtime实现weak属性就是:
对于注册为weak的对象,系统会把 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
当我们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在runtime源码中的NSObject.mm文件中。具体定义如下:
idobjc_initWeak(id *location, id newObj){if (!newObj) {*location = nil;return nil;}return storeWeak<false/*old*/, true/*new*/, true/*crash*/>(location, (objc_object*)newObj);}
这里的两个参数location表示weak指针的地址,newObj表示对象指针。我们看到当这个newObj是个空指针或者其指向的对象已经被释放了,那么*location = nil;,返回也是nil,则表示weak的初始化其实是失败的。
然后如果成功的话执行storeWeak函数并返回值。那我们来看看storeWeak的定义:
template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>static idstoreWeak(id *location, objc_object *newObj){assert(HaveOld || HaveNew);if (!HaveNew) assert(newObj == nil);Class previouslyInitializedClass = nil;id oldObj;SideTable *oldTable;SideTable *newTable;// Acquire locks for old and new values.// Order by lock address to prevent lock ordering problems.// Retry if the old value changes underneath us.retry:if (HaveOld) {oldObj = *location;oldTable = &SideTables()[oldObj];} else {oldTable = nil;}if (HaveNew) {newTable = &SideTables()[newObj];} else {newTable = nil;}//一些锁的操作SideTable::lockTwo<HaveOld, HaveNew>(oldTable, newTable);if (HaveOld && *location != oldObj) {SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);goto retry;}// Prevent a deadlock between the weak reference machinery// and the +initialize machinery by ensuring that no// weakly-referenced object has an un-+initialized isa.if (HaveNew && newObj) {Class cls = newObj->getIsa();if (cls != previouslyInitializedClass &&!((objc_class *)cls)->isInitialized()){SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);_class_initialize(_class_getNonMetaClass(cls, (id)newObj));// If this class is finished with +initialize then we're good.// If this class is still running +initialize on this thread// (i.e. +initialize called storeWeak on an instance of itself)// then we may proceed but it will appear initializing and// not yet initialized to the check above.// Instead set previouslyInitializedClass to recognize it on retry.previouslyInitializedClass = cls;goto retry;}}// Clean up old value, if any.if (HaveOld) {weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);}// Assign new value, if any.if (HaveNew) {newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,(id)newObj, location,CrashIfDeallocating);// weak_register_no_lock returns nil if weak store should be rejected// Set is-weakly-referenced bit in refcount table.if (newObj && !newObj->isTaggedPointer()) {newObj->setWeaklyReferenced_nolock();}// Do not set *location anywhere else. That would introduce a race.*location = (id)newObj;}else {// No new value. The storage is not changed.}SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);return (id)newObj;}
我们可以看到里面有一个比较重要的类SideTable,我们再来看看SideTable的定义:
struct SideTable {spinlock_t slock;RefcountMap refcnts;weak_table_t weak_table;SideTable() {memset(&weak_table, 0, sizeof(weak_table));}~SideTable() {_objc_fatal("Do not delete SideTable.");}void lock() { slock.lock(); }void unlock() { slock.unlock(); }bool trylock() { return slock.trylock(); }// Address-ordered lock discipline for a pair of side tables.template<bool HaveOld, bool HaveNew>static void lockTwo(SideTable *lock1, SideTable *lock2);template<bool HaveOld, bool HaveNew>static void unlockTwo(SideTable *lock1, SideTable *lock2);};
SideTable里有一个RefcountMap refcnts;和weak_table_t weak_table;
RefcountMap里存储了一个对象的引用计数信息。而weak_table_t weak_table;维护和存储了一个对象的所有弱引用的信息。具体的结构定义可以看runtime源码中的objc-weak.h文件。
知道了这些定义,我们现在就来看看整个storeWeak函数的作用:
HaveOld和HaveNew来判断是否有新老对象。然后找到其新老对象,根据其新老对象获取与其相关的SideTable对象。
if (HaveOld) {oldObj = *location;oldTable = &SideTables()[oldObj];} else {oldTable = nil;}if (HaveNew) {newTable = &SideTables()[newObj];} else {newTable = nil;}
然后就是一些锁的操作
最后就是在老对象的weak表中移除指向信息,而在新对象的weak表中建立关联信息,让弱引用指针指向新对象,然后返回这个新对象:
//移除老对象weak表中的信息if (HaveOld) {weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);}// 在新对象的weak表中建立关联if (HaveNew) {newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,(id)newObj, location,CrashIfDeallocating);// weak_register_no_lock returns nil if weak store should be rejected// Set is-weakly-referenced bit in refcount table.if (newObj && !newObj->isTaggedPointer()) {newObj->setWeaklyReferenced_nolock();}// 弱引用指针指向新对象*location = (id)newObj;}else {// No new value. The storage is not changed.}SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);//返回新对象return (id)newObj;
当weak引用指向的对象被释放时,其基本流程如下:
其中objc_release和_objc_rootDealloc在NSObject.mm中定义。
object_dispose、objc_destructInstance和clearDeallocating在objc-runtime-new.mm文件中定义。
我们看源码中的定义其实就能知道最后clearDeallocating最关键的部分的是执行objc-weak.mm文件中的weak_clear_no_lock函数:
voidweak_clear_no_lock(weak_table_t *weak_table, id referent_id){objc_object *referent = (objc_object *)referent_id;weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);if (entry == nil) {/// XXX shouldn't happen, but does with mismatched CF/objc//printf("XXX no entry for clear deallocating %p\n", referent);return;}// zero out referencesweak_referrer_t *referrers;size_t count;if (entry->out_of_line) {referrers = entry->referrers;count = TABLE_SIZE(entry);}else {referrers = entry->inline_referrers;count = WEAK_INLINE_COUNT;}for (size_t i = 0; i < count; ++i) {objc_object **referrer = referrers[i];if (referrer) {if (*referrer == referent) {*referrer = nil;}else if (*referrer) {_objc_inform("__weak variable at %p holds %p instead of %p. ""This is probably incorrect use of ""objc_storeWeak() and objc_loadWeak(). ""Break on objc_weak_error to debug.\n",referrer, (void*)*referrer, (void*)referent);objc_weak_error();}}}weak_entry_remove(weak_table, entry);}
这个函数的首要任务就是找出对象对应的weak_entry_t链表,然后挨个将弱引用置为nil。最后清理对象的记录。
struct SideTable {spinlock_t slock;RefcountMap refcnts;weak_table_t weak_table;}
- spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable。
- RefcountMap refcnts :用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
- weak_table_t weak_table : 存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。
struct weak_table_t {weak_entry_t *weak_entries;size_t num_entries;uintptr_t mask;uintptr_t max_hash_displacement;};
- 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的结构也是一个hash结构,其存储的元素是弱引用对象指针的指针, 通过操作指针的指针,就可以使得weak 引用的指针在对象析构后,指向nil。
#define WEAK_INLINE_COUNT 4#define REFERRERS_OUT_OF_LINE 2struct weak_entry_t {DisguisedPtr<objc_object> referent; // 被弱引用的对象// 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrersunion {struct {weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)};struct {// out_of_line_ness field is low bits of inline_referrers[1]weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];};};bool out_of_line() {return (out_of_line_ness == REFERRERS_OUT_OF_LINE);}weak_entry_t& operator=(const weak_entry_t& other) {memcpy(this, &other, sizeof(other));return *this;}weak_entry_t(objc_object *newReferent, objc_object **newReferrer): referent(newReferent) // 构造方法,里面初始化了静态数组{inline_referrers[0] = newReferrer;for (int i = 1; i < WEAK_INLINE_COUNT; i++) {inline_referrers[i] = nil;}}};
可以看到在weak_entry_t的结构定义中有联合体,在联合体的内部有定长数组inline_referrers[WEAK_INLINE_COUNT]和动态数组weak_referrer_t *referrers两种方式来存储弱引用对象的指针地址。通过out_of_line()这样一个函数方法来判断采用哪种存储方式。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT时,使用定长数组。当超过WEAK_INLINE_COUNT时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。
我们知道weak和unsafe_unretained的不同之处是,设置weak属性后,系统会在对象被释放后自动将这个指向对象的指针设为nil。而unsafe_unretained修饰的属性会在对象释放后产生悬空指针。
这里我们解释一下什么是空指针、悬空指针、野指针。
空指针:也就是weak修饰的指针,在对象释放后指针设为nil,也就成了空指针,在OC里给nil发送消息是不会崩溃的。
悬空指针:对象被释放后那块对象的内存已经失效了,此内存就是垃圾内存,但是还有指针指向这块垃圾内存,这个指针就是悬空指针。也就是unsafe_unretained修饰的指针在对象释放后会成为悬空指针,给悬空指针发送消息是会崩溃的。
野指针:某些编程语言允许未初始化的指针的存在,而这类指针即为野指针。
1: 首先,创建一个ClassA类
2: 创建一个ClassB类,并在里面添加一个测试方法print,用于等下向ClassB实例对象发送消息,确认对象是否仍在使用
- (void)print {NSLog(@"Object is %@", self);}
3: 给ClassA添加一个ClassB对象属性,并设置为unsafe_unretained
@property (nonatomic, unsafe_unretained) ClassB *objectB;
4: 在main函数中添加如下代码:
@autoreleasepool {ClassA *instanceA = [ClassA new];ClassB *instanceB = [ClassB new];instanceA.objectB = instanceB;[instanceA.objectB print];// release instanceBinstanceB = nil;[instanceA.objectB print];}return 0;
5: 运行一下代码,不出意外的话,程序会挂掉,因为instanceA.objectB所指向的内存已经在instanceB = nil;时候被释放掉,instanceA.objectB仅指向一个悬空指针:
2016-03-09 15:25:10.427 WeakDemo[98402:1613532] Object is 0x7fa080d0f1002016-03-09 15:25:10.432 WeakDemo[98402:1613532] Object is 0x7fa080d0f100Process finished with exit code 139
当然,也有可能不会挂,这取决于执行print函数时,instanceB所在的内存是否完全释放掉。
6: 如果将ClassA中的属性改为@property (nonatomic, weak) ClassB *objectB;则不会出现crash,这就是weak的作用了,objectB对象如果被释放掉,则该指针变为nil,而向nil发送消息是不会出现问题的。
demo代码这里给出了具体的实现代码。具体的原理我简单说一下:
就是创建了一个
NSObject的category,category里有一个方法传入一个block并使用runtime Associate方法关联一个delloc对象,这个对象在delloc的时候,会调用block。所以当NSObjec对象释放的时候,这个delloc对象也就会执行block,我们直接在block里面设置指针为nil就不会产生悬空指针了。
这里有一个问题,我们给NSObject对象关联的delloc对象是在什么时候delloc的呢?
我们看到对象销毁时流程是如下这样的:
在runtime源码的objc-runtime-new.mm文件中我们看到objc_destructInstance的定义如下,可以看到里面有个_object_remove_assocations执行了移除关联对象的操作:
void *objc_destructInstance(id obj){if (obj) {// Read all of the flags at once for performance.bool cxx = obj->hasCxxDtor();bool assoc = !UseGC && obj->hasAssociatedObjects();bool dealloc = !UseGC;// This order is important.if (cxx) object_cxxDestruct(obj);if (assoc) _object_remove_assocations(obj);if (dealloc) obj->clearDeallocating();}return obj;}
NSObject的category:NSObject+deallocBlock.h
@interface NSObject (deallocBlock)- (void)runBlockOnDealloc:(voidBlock)block;@end@implementation NSObject (deallocBlock)- (void)runBlockOnDealloc:(voidBlock)block {if (block) {DeallocBlock *deallocBlock = [[DeallocBlock alloc] initWithBlock:block];objc_setAssociatedObject(self, _cmd, deallocBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}}@end
NSObjec的category中要关联的对象的类:DeallocBlock.h
typedef void (^voidBlock)();@interface DeallocBlock : NSObject@property(nonatomic, copy) voidBlock block;- (instancetype)initWithBlock:(voidBlock)block1;@end@implementation DeallocBlock- (instancetype)initWithBlock:(voidBlock)block1 {self = [super init];if (self) {_block = [block1 copy];}return self;}- (void)dealloc {if (_block) {NSLog(@"DeallocBlock dealloc!");_block();}}@end
使用就是如下面代码这样,重写classA的setObjectB方法:
- (void)setObjectB:(ClassB *)objectB {_objectB = objectB;// 仅对objectB != nil case做处理if (_objectB) {[_objectB runBlockOnDealloc:^{NSLog(@"_objectB dealloc");_objectB = nil;}];}}