Block
使用会存在循环引用的问题,多个Block
嵌套使用的情况更复杂,还会出现对象nil
的问题。
为什么会循环引用?
现在iOS开发都是在ARC引用计数
管理模式下的,参考另一篇文章《Block底层原理》,我们知道Block
访问外部变量对临时变量是值拷贝(深拷贝),对__block
修饰的变量是指针拷贝(浅拷贝),这两种情况都不在这次的讨论范围内,因为变量的作用域有限,会在block
执行完毕后释放掉;如果Block
访问的是对象类型(比如:Dog类
),Block
访问外部变量对__strong
修饰的对象(NSObject
默认是__strong
修饰),是执行的对象自身的copy
函数(浅拷贝)。
- 问:Block访问实例对象会造成循环引用吗?
- (void)testBlock {
void(^block1)(void) = ^{
self.name = @"张三";
};
block1();
}
答:不会。通过打印self.name
,证明name
已经修改为@"张三"
,再次证明block
捕获对象是浅拷贝,引用计数+1,当testBlock
函数调用完毕,block1
就被释放掉了,对self
的引用计数-1,所以不会存在循环引用。
- 造成循环引用的情况
循环引用其实也就是说,对象会被一直持有,对象无法释放,dealloc
就不调用了,导致内存溢出,这才是我们担心的问题所在。造成这一问题所在的根源就是对象被持有没有释放,引用计数一直大于0。
比如:A持有B,B也持有A,谁也无法释放内存;这个是最常见的。
- (void)testBlock {
self.block1 = ^{
self.name = @"张三";
};
block1();
}
//self持有了block1,block1又访问了self,
还有A持有B,B持有C,C持有D…D持有A,最后肯定形成了闭环
- (void)testBlock {
self.block1 = ^{
void(^block2)(void) = ^{
self.name = @"张三";
};
block2();
};
block1();
}
//self持有了block1,block1持有了block2访问了self,block2访问了self。
我们要做的就是把强制引用形成的链条打断。
__weak原理
解决循环引用,我们最多的就是用__weak
来修饰变量,从而打破强引用的链条。那么__weak
为什么可以做到呢?
Runtime维护了一个 weak_table_t
hash(哈希) 表,用于存储指向某个对象的所有weak
指针。Key
是所指对象的地址,Value
是weak
指针的地址(这个地址的值是所指对象的指针地址)数组。
__weak typeof(self) weakSelf = self;
- 当我们初始化一个weak变量时,
runtime
会调用NSObject.mm
中的objc_initWeak
函数
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
- *location :
__weak
weakSelf 的指针地址 - newObj :所引用的对象,即例子中的
self
objc_initWeak
函数会调用storeWeak
函数,该函数主要是更新指针指向,创建对应的弱引用表
template <HaveOld haveOld, HaveNew haveNew,
enum CrashIfDeallocating crashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
retry:
// 如果weak指针变量 之前弱引用过一个对象 讲这个对象对应的SideTable从SideTables中取出来,赋值给oldTable
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
// 如果weak指针变量 之前没有弱引用过一个obj,则oldTable = nil
oldTable = nil;
}
// 如果weak指针变量要weak引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTable
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
// 如果weak指针变量不需要引用一个新obj,则newTable = nil
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()) // 如果cls还没有初始化,先初始化,再尝试设置weak
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(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; // 重新获取一遍newObj,这时的newObj应该已经初始化过了
}
}
// 如果weak指针变量之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// 如果weak指针变量需要弱引用新的对象newObj
if (haveNew) {
// 1 调用weak_register_no_lock方法,weak_register_no_lock会将weak指针变量的地址 记录到newObj对应的weak_entry_t中
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// 2 更新newObj的isa的weakly_referenced bit标志位
if (!newObj->isTaggedPointerOrNil()) {
newObj->setWeaklyReferenced_nolock();
}
// 3 *location 赋值,也就是将weak指针变量直接指向了newObj 这里并没有将newObj的引用计数+1 , 所以weak引用不会让newObj引用计数+1
*location = (id)newObj; // 也就是例子中 将weakSelf 指向self
}
else {
// No new value. The storage is not changed.
}
// 解锁,其他线程可以访问oldTable, newTable了
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
.
// 返回newObj,此时的newObj与刚传入时相比,weakly-referenced bit位置1
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
- 当对象的引用计数为0时,底层会调用
_objc_rootDealloc
方法对对象进行释放 _objc_rootDealloc
->object_dispose
->objc_destructInstance
->clearDeallocating
->clearDeallocating_slow
->weak_clear_no_lock
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
// 找到referent在weak_table中对应的weak_entry_t
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 references
weak_referrer_t *referrers;
size_t count;
// 找出weak引用referent的weak指针地址数组以及数组长度
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]; // 取出每个weak 指针变量的地址
if (referrer) {
if (*referrer == referent) { // 如果weak 指针变量确实weak引用了referent,则将weak指针变量设置为nil,这也就是为什么weak 指针会自动设置为nil的原因
*referrer = nil;
}
else if (*referrer) { // 如果所存储的weak 指针变量没有weak 引用referent,这可能是由于runtime代码的逻辑错误引起的,报错
_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); // 由于referent要被释放了,因此referent的weak_entry_t也要移除出weak_table
}
强弱共舞(weak-strong-dance)
我们在Block里面嵌套一个延迟函数,通过输出打印,对象内存已经被置空。
- (void)checkBlock {
Dog *dog = [Dog new];
__weak Dog *dog_weak = dog;
dog.eatBlock = ^(NSString * _Nonnull name) {
dog_weak.name = name;
dispatch_async(dispatch_get_main_queue(), ^{
dog_weak.name = @"牛肉";
NSLog(@"wuwuFQ:%@", dog_weak);//输出 wuwuFQ:(null)
});
};
dog.eatBlock(@"鸡肉");
NSLog(@"wuwuFQ:%@", dog.name);//输出 wuwuFQ:鸡肉
}
dispatch_async
是主线程异步执行,可能会在checkBlock
函数执行完后才执行,对象dog
就会在之前被释放掉,导致弱引用dog_weak
指针为nil
。
我们用__strong
来解决这个问题:
- (void)checkBlock {
Dog *dog = [Dog new];
__weak Dog *dog_weak = dog;
dog.eatBlock = ^(NSString * _Nonnull name) {
dog_weak.name = name;
__strong Dog *dog_strong = dog_weak;
dispatch_async(dispatch_get_main_queue(), ^{
dog_strong.name = @"牛肉";
NSLog(@"wuwuFQ:%@", dog_strong);//输出 wuwuFQ:<Dog: 0x6000027ddce0>
});
};
dog.eatBlock(@"鸡肉");
NSLog(@"wuwuFQ:%@", dog.name);//输出 wuwuFQ:鸡肉
}
我们再来理解下这段代码,dispatch_async
在这里是主线程异步执行任务,会添加到 main_queue()
的末端,我在调用checkBlock
函数之后又尝试调用了其他同步函数,dispatch_async
总是最后一个被调用,也就是说肯定在checkBlock
函数之后了,也就导致了对象dog
被释放掉。
我们用 __strong
强引用了dog_weak
指针,可以让dispatch_async
内部捕获到变量dog_strong
,dispatch_async
执行完毕,变量dog_strong
释放,也不会产生循环引用。