iOS开发:__weak __strong解决Block嵌套

news/2024/11/8 0:36:38/

Block使用会存在循环引用的问题,多个Block嵌套使用的情况更复杂,还会出现对象nil的问题。

为什么会循环引用?

现在iOS开发都是在ARC引用计数管理模式下的,参考另一篇文章《Block底层原理》,我们知道Block访问外部变量对临时变量是值拷贝(深拷贝),对__block修饰的变量是指针拷贝(浅拷贝),这两种情况都不在这次的讨论范围内,因为变量的作用域有限,会在block执行完毕后释放掉;如果Block访问的是对象类型(比如:Dog类),Block访问外部变量对__strong修饰的对象(NSObject默认是__strong修饰),是执行的对象自身的copy函数(浅拷贝)。

  1. 问:Block访问实例对象会造成循环引用吗?
- (void)testBlock {void(^block1)(void) = ^{self.name = @"张三";};block1();
}

答:不会。通过打印self.name,证明name已经修改为@"张三",再次证明block捕获对象是浅拷贝,引用计数+1,当testBlock函数调用完毕,block1就被释放掉了,对self的引用计数-1,所以不会存在循环引用。

  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是所指对象的地址,Valueweak指针的地址(这个地址的值是所指对象的指针地址)数组。

 __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中取出来,赋值给oldTableif (haveOld) {oldObj = *location;oldTable = &SideTables()[oldObj];} else {// 如果weak指针变量 之前没有弱引用过一个obj,则oldTable = niloldTable = nil;}//  如果weak指针变量要weak引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTableif (haveNew) {newTable = &SideTables()[newObj];} else {// 如果weak指针变量不需要引用一个新obj,则newTable = nilnewTable = 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指针变量需要弱引用新的对象newObjif (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位置1callSetWeaklyReferenced((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_tweak_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;// 找出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_strongdispatch_async执行完毕,变量dog_strong释放,也不会产生循环引用。


http://www.ppmy.cn/news/172556.html

相关文章

ES查询 too_many_clauses,maxClauseCount is set to 5000

一&#xff1a;语法执行背景 ES boo查询中过多的拼接bool导致报maxClauseCount is set to 5000 { "caused_by": { "type": "too_many_clauses","reason": "maxClauseCount is set to 5000" } } 查询DSL语句&#xff1a; { …

【备战秋招】每日一题:4月1日美团春招(二批)第五题:题面+题目思路 + C++/python/js/Go/java带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…

联想微型计算机boot,联想电脑boot设置图解

联想电脑boot设置图解 不管是台式机还是笔记本如果安装的主板不同&#xff0c;其主板BISO程序也略有不同。联想笔记本的里面的主板BIOS设置就跟别的笔记本的BIOS设置有少许的差异&#xff0c;下面就小编在联想笔记本维修过程中吸取一些关于主板BIOS设置经验&#xff0c;来向各位…

联想电脑 linux BIOS,联想电脑bios怎么设置

BIOS是英文“Basic Input Output System”的缩略语,直译过来就是“基本输入输出系统”。其实,它是一组固化到计算机内主板上一个ROM芯片上的程序,那么联想电脑bios怎么设置?下面大家跟着学习啦小编一起来学习一下吧。 联想电脑bios设置方法 1、开机时&#xff0c;按F2&#xf…

联想台式计算机编号怎么查,联想电脑怎么查看主机编号_联想电脑编号在哪里...

联想电脑怎么查看主机编号呢&#xff1f;下面介绍几种方法供你使用 1、主机背面的黑白标识牌 主机编号由主机型号TYPE xxxx-xxx和序列号 S/N xx-xxxxx 14位数字和字母组合而成。 2、电池槽位查看主机编号 如果机器处于开机状态&#xff0c;建议关机后拿掉电池&#xff0c;查看主…

联想电脑 linux bios设置,韩博士分享关于联想电脑bios的基本设置

BIOS设置是很多用户在重装系统时都需要用到的&#xff0c;而关于BIOS品牌的主板也有很多&#xff0c;所以在设置上也会存在一定的差异。一般电脑的开机启动快捷键为F12&#xff0c;有些电脑开机的时候在电脑屏幕下方会显示哪个键可以用来设置启动选项&#xff0c;有些电脑不显示…

联想如何打开计算机配置,联想电脑如何进入bios设置

联想电脑进入BIOS的快捷键有“F2、F1、Del/Delete、NOVO开机” 部分机型按F2、F1时需要FN键配合 注:使用Win8/8.1操作系统的电脑,需要在系统下选择重启,在“开机自检界面”连续点击对应快捷键进入BIOS界面,详细方法见如下解决方案 联想笔记本产品进入BIOS的操作方法 适用范…

联想的锋行计算机,联想电脑锋行系列都有哪些型号

尊敬用户您好&#xff0c;根据您的提出问题&#xff0c;应该是联想锋行 X5520这款电脑 &#xff0c;以下是它的详细介绍 联想锋行 X5520CPU规格 CPU类型 Intel 奔腾双核 CPU频率 2200MHz 二级缓存 1MB 前端总线 800MHz CPU说明 Intel 奔腾双核 E2200 2.2GHz 联想锋行 X5520显示…