一篇文章让你看懂IOS中的block为何再也不需要WeakSelf弱引用

前言:

最近都在折腾Sagit架框的内存释放的问题,所以对这一块有些心得。

对于新手,学到的文章都在教你用:typeof(self) __weak weakSelf = self

对于老手,可能早习惯了到处了WeakSelf了。

这次,就来学学,如何不用WeakSelf。

1:从引用计数器开始:

这里先设计一个TableBlock类:

@interface BlockTable : NSObject

typedef void (^AddCellBlock)();
@property (nonatomic,copy)AddCellBlock addCell;@end

先这么简单,一个BlockTable只有一个block属性,然后输出一段释放的日志。

@interface BlockTable : NSObject
typedef void (^AddCellBlock)();
@property (nonatomic,copy)AddCellBlock addCell;@end

接着,随意找一个地方写写代码:来new了一个BlockTable,并打印一下信息:

这时候它的引用数是1,并且出了Table relase 。

接着给addCell属性赋一个值,并运行:

一个空的事件,里面并没有引用到table,所以引用数还是1。

2:开始循环引用

在block引用table,让它产生循环引用,并运行:

我们看到:引用数变成了3,没有输出对象释放信息了,为啥不是2呢?大大的问号!!

一个属性赋值,为什么增强两个引用计数?

3:猜解跳跃的计数器

接下来,把属性设置为nil,运行看看:

设置为nil,还有2?

也正常释放了?

为了证实自己对这个看起来就很明显的猜想:重写addCell的setter方法,不进行任何保存:

@implementation BlockTable
-(void)setAddCell:(AddCellBlock)addCell
{

}

同时去掉置为nil的代码:再运行看看:

计数器仍为2,而且也释放了。

经过思考,出来了以下的结论:

1:块的定义本身,就会造成1次引用,不过这次引用,在块离开所在的函数时,释放时,抵消掉引用数。

2:存档块的时候,会造成1次引用,而这个引用,是内存无法释放的原因。

4:根据上述解释,得到一个疯狂的结论:

只要block的代码只执行1次的,都可以任性的self或其它强引用。

事实上,我们写的代码,很多block的确只执行一次,不管是传的时候就执行,还是传完之后过段时间回调再执行。

认定只要执行1次的,就不需要WeakSelf,除非第三方框架的设计者造孽留坑,忘了在存档block执行后补上block=nil这一刀。

5:消灭赋值的引用计数:

继续发挥想象力,既然存的时候,会增加一次引用,辣么,让它不增加引用不就好了:

@implementation BlockTable
-(void)setAddCell:(AddCellBlock)addCell
{
 __weak AddCellBlock addCellWeak=addCell;
 _addCell=addCellWeak;
}

我们先给这个block定义一个弱引用,然后再赋值给_addCell,运行看看:

哇草,成功了!计数器为2,正常释放了,看来自己的想象力,还是可以的!!

接下来,我们补充完善一下代码,增加一个reloadData方法,方法里调用事件。

完整的代码如下:

@interface BlockTable : NSObject
typedef void (^AddCellBlock)();
@property (nonatomic,copy)AddCellBlock addCell;
-(void)reloadData;
@end
@implementation BlockTable
-(void)setAddCell:(AddCellBlock)addCell
{
 __weak AddCellBlock addCellWeak=addCell;
 _addCell=addCellWeak;
}
-(void)reloadData
{
 if(self.addCell)
 {
 self.addCell();     self.addCell();//没事来两次,模拟table多次循环清加cell
 }
}
-(void)dealloc
{
 NSLog(@"Table relase");
}
@end

修改一下增加日志输出,现在再执行一下看看:

一切看起来都相当完美,不需要引入第三,需要多次使用的,只是在存的时候,存个弱引用,就搞定了。

6:弱引用降低计数的缺陷:

块的定义,和使用的场景,必须在同一个函数。

说白了就是块离开函数体就会消亡,所以要用要赶紧,且用且珍惜。

正常一个Table写完代码reloadData后,数据出来了。

但如果后面还跟有一个刷新重新加载的功能?

而这个重新调用reloadData的地方,可能跟block不在同一个函数,比如代码像这样:

-(void)start
{
 BlockTable *table=[BlockTable new];
 self.table=table;//搞到全局变量中
 table.addCell = ^{
 __weak typeof(table) this=table;
 NSLog(@"addCell call");
 };
 [table reloadData];
 NSLog(@"table retain = %ld",CFGetRetainCount((__bridge CFTypeRef)(table)));
}
-(void)reflesh
{
 [self.table reloadData];
}

给外面的类定义了一个table属性,然后调用完start后再调用reflesh,运行,会怎样呢?

出现了IOS上最可怕的EXC_BAD_ACCESS 野指针错误。

对于block离开函数后,消亡了容易理解,只是这里:

这什么是直接抛异常?哥不是作了判断了么?

让我们换种代码写法:

另外从上图看:_addCell还是有值的。

为什么if(self.addCell)判断就直接死,if(_addCell)却没死呢?

正常self.addCell正常不是也return _addCell么?

这个问题,留给让你们思考了。

最可怕的,还是下面的这段话:

7:避开野指针,仍是弱引用,功能不变

OK,继续发挥想象力,看看怎么避开野指针,同时还是实现上述的效果:

1:把block属性从copy改成weak

@property (nonatomic,weak)AddCellBlock addCell;

2:赋值代码手工copy:

-(void)setAddCell:(AddCellBlock)addCell
{
 addCell=[addCell copy];
 _addCell=addCell;
 //_addCell=[addCell copy];这样简写是不行的,不明白为虾米呢

 // 原来是这样写的:
 // __weak AddCellBlock addCellWeak=addCell;
 // _addCell=addCellWeak ;
}

再次运行,神奇的事情发生了:

流程还是很顺,不会有野批针异常,Table也释放了。

唯一的遗憾,就是跳出函数后,block不能再复用了:

8:block的copy方法:

对于默认传进来的block(有三种形态:全局、栈、堆)

全局 copy 还是全局

堆 copy 还是堆

栈 copy 变成堆

说白了,copy只对类型是栈是才有效。

这是因为:栈的block,在执行完后出括号后,直接是销毁对象。

如果有弱引用过去,会造成野指针。

而其它两种类型,销毁时,会将指针指向一个空指针。

addCell=[addCell copy] 和默认copy的属性 _addCell=addCell 也是执行了copy操作。

执行后,addCell的类型就变成堆形态,这样销毁的时候,是空指针。

9:空指针和野指针的区别:

空指针:指向一个:人为创造的一个指针,它的名字叫空,有座空房子,里面什么也没有。

野指针:就是指向的都不知哪去了,连空房子都木有。

10:扩展想象力,如何消灭引用数,还能长久保留?

弱引用的坏处,就是block出了函数,就不再可用这个block了。

那还能怎么办呢?没事,我还有想象力!!!!!

如果block可以重建呢?

比如:

1:将block转成字符串存档,适当时机还原回来重新赋值

2:将block序列化保存,适当时机还原回来?

3:runtime读取block的__FuncPtr,存档再动态创建?

伪代码大体如下:

-(void)setAddCell:(AddCellBlock)addCell
{
 addCell=[addCell copy];
 _addCell=addCell;

 //_addCell=[addCell copy];这样简写是不行的,不明白为虾米呢
 // 原来是这样写的:
 // __weak AddCellBlock addCellWeak=addCell;
 // _addCell=addCellWeak ;

 //存档block的字符串
}
-(void)reloadData
{
 if(!_addCell)
 {
 //从存档的block字符串还原block
 //_addCell=还原block
 }
 if(_addCell)
 {
 _addCell();
 _addCell();
 }
}

那么就剩下两个问题?

1:怎么把block存档?

2:怎么将存档数据还原成block。

对搞C#的来说,这些都家常便饭,oc这块还不熟,有路过的朋友可顺路给支支招!!

11:如果第10的方式解决不了,就只能,只能,引入时机第三者了

不过这个引入第三者,只是一个时机切入点,在这个时机触发的时候,将其中的一方的引用设置为nil。

像Sagit框架的布局方面的时机,就选在导航回退等事件中处理。

不过这里需要一个小技巧:

在存档block时,不一定要存在当前对象,也可以用一个统一的全局block管理起来。

这样在业务处理时,根据业务情况,从全局block里来移除某些block即可。

具体取决于业务,所以这个就不展开了。

总结:

相信,一路看下,看懂了,后续的情况,基本上已经用不上WeakSelf这东西了,因为像一个block,其生命周期必须和持有者保持一致的,还是挺少的。

而这种少的情况,如果第10步解决了,基本就全都解决了,解决不了,还有11。

相信读完此文,如果能完全理解,你就再也看不到block前WeakSelf这种,WeakSelf也没有存在必要了。

最后,欢迎大伙关注IT连创业,虽然最近我都在折腾IOS,哈哈。

不过IOS基础还是要打劳,后续产品改进起来才有质的飞跃。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

您可能感兴趣的文章:

  • iOS中Block的回调使用和解析详解
  • iOS通过block在两个页面间传值的方法
  • iOS利用Block逆向传值的方式详解
  • iOS面试中如何优雅回答Block导致循环引用的问题
  • 浅谈iOS 对于block的一点理解
(0)

相关推荐

  • iOS面试中如何优雅回答Block导致循环引用的问题

    前言 说到循环引用问题,最最最常遇到的,不是在项目中,而是在面试中.如果面试官问你开发中是否遇到过retain cycle,你如果说没遇到过,估计已经很难跟面试官继续友好的沟通下去了. 但是这个问题怎么回答呢,网络上千篇一律的答案-->使用Block的时候遇到过,使用__weakSelf 代替 self 等等,可以说这个答案没啥错,但是所有人都回答的一样,并不能突出我们的逼格,无法让面试官知道我们在这方面有过研究,有闪光点. 对于开发者来说,喜欢探索,喜欢挖掘不懂的知识,在面试官眼里会加分不少.

  • 浅谈iOS 对于block的一点理解

    block是对象,它封装了一段代码,这段代码可以在任何时候执行.block可以作为函数参数或者函数的返回值,而其本身又可以带输入参数或返回值.它和传统的函数指针很类似,但是有区别:block是inline的,并且它对局部变量是只读的. Block的定义: int (^myBlock) (int a,int b) = ^(int a,int b){ return a+b; }; 定义了一个名为myBlock的blocks对象,它带有两个int参数,返回int.等式右边就是blocks的具体实现,是

  • iOS中Block的回调使用和解析详解

    Block 回调实现 先跟着我实现最简单的 Block 回调传参的使用,如果你能举一反三,基本上可以满足了 OC 中的开发需求.已经实现的同学可以跳到下一节. 首先解释一下我们例子要实现什么功能(其实是烂大街又最形象的例子): 有两个视图控制器 A 和 B,现在点击 A 上的按钮跳转到视图 B ,并在 B 中的textfield 输入字符串,点击 B 中的跳转按钮跳转回 A ,并将之前输入的字符串 显示在 A 中的 label 上.也就是说 A 视图中需要回调 B 视图中的数据. 想不明白的同学

  • iOS利用Block逆向传值的方式详解

    前言 在iOS通过代理逆向传值的方式详解一文中,分析了如何利用代理模式来逆向传值,其实还有一些其他的方式,如通知.Block等,相比较代理,我个人认为反而要简单些,但是需要处理好细节问题,如Block循环引用.还是用前文的案例,本次使用Block来实现,Block的基本知识本文不再赘述. 一.书写规范 Block传值,需要注意的是,谁传值就需要定义Block,捕获方仅仅需要传递Block给传值方,并处理捕获的值. 传值方 1.定义Block用于传值 2.声明一个上述Block属性,这个属性的具体

  • iOS通过block在两个页面间传值的方法

    一.功能需求 在第一个页面中有一个button和一个label,label上默认显示"哈哈",点击button进入第二个页面.在第二个页面有一个UITextField和一个button2,点击button2回到第一个页面,但同时第一个页面的label上显示的文字修改为刚刚在UITextField写进去的文字. 二.先定义block 在要传值得那个页面定义含有block参数的方法,即在第二个页面的.h文件中定义: 重新定义:typedef void (^ReturnTextBlock)(

  • 一篇文章让你看懂IOS中的block为何再也不需要WeakSelf弱引用

    前言: 最近都在折腾Sagit架框的内存释放的问题,所以对这一块有些心得. 对于新手,学到的文章都在教你用:typeof(self) __weak weakSelf = self. 对于老手,可能早习惯了到处了WeakSelf了. 这次,就来学学,如何不用WeakSelf. 1:从引用计数器开始: 这里先设计一个TableBlock类: @interface BlockTable : NSObject typedef void (^AddCellBlock)(); @property (nona

  • 一篇文章让你看懂Js继承与原型链

    目录 继承与原型链 继承属性 继承方法 在 JavaScript 中使用原型 性能 附:原型链是实现继承的主要方法 总结 继承与原型链 当谈到继承时,JavaScript 只有一种结构:对象.每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype).该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null.根据定义,null 没有原型,并作为这个原型链中的最后一个环节. 几乎所有 JavaScript

  • 一篇文章让你看懂封装Axios

    目录 前言 拦截器不要返回数据,依然返回 AxiosResponse 对象 不推荐的做法 推荐的做法 为你的请求添加拓展 支持请求重试 支持 jsonp 请求 支持 URI 版本控制 保持请求唯一 后语 总结 前言 看很多网上的人的封装 Axios 教程,但或多或少都有不太合适的点,这里为大家推荐我的最佳实践. 拦截器不要返回数据,依然返回 AxiosResponse 对象 网上的文章都让你用 拦截器 直接返回数据,这种作法其实是非常不妥的,这样会让你后续的功能很难进行拓展. 不推荐的做法 im

  • 一篇文章让你搞懂JavaScript 原型和原型链

    本文由葡萄城技术团队原创并首发 转载请注明出处:葡萄城官网 与多数面向对象的开发语言有所不同,虽然JavaScript没有引入类似类的概念(ES6已经引入了class语法糖),但它仍然能够大量的使用对象,那么如何将所有对象联系起来就成了问题.于是就有了本文中我们要讲到的原型和原型链的概念. 原型和原型链作为深入学习JavaScript最重要的概念之一,如果掌握它了后,弄清楚例如:JavaScript的继承,new关键字的原来.封装及优化等概念将变得不在话下,那么下面我们开始关于原型和原型链的介绍

  • 一篇文章带你搞懂Java线程池实现原理

    目录 1. 为什么要使用线程池 2. 线程池的使用 3. 线程池核心参数 4. 线程池工作原理 5. 线程池源码剖析 5.1 线程池的属性 5.2 线程池状态 5.3 execute源码 5.4 worker源码 5.5 runWorker源码 1. 为什么要使用线程池 使用线程池通常由以下两个原因: 频繁创建销毁线程需要消耗系统资源,使用线程池可以复用线程. 使用线程池可以更容易管理线程,线程池可以动态管理线程个数.具有阻塞队列.定时周期执行任务.环境隔离等. 2. 线程池的使用 /** *

  • 一篇文章带你搞懂Python类的相关知识

    一.什么是类 类(class),作为代码的父亲,可以说它包裹了很多有趣的函数和方法以及变量,下面我们试着简单创建一个吧. 这样就算创建了我们的第一个类了.大家可以看到这里面有一个self,其实它指的就是类aa的实例.每个类中的函数只要你不是类函数或者静态函数你都得加上这个self,当然你也可以用其他的代替这个self,只不过这是python中的写法,就好比Java 中的this. 二.类的方法 1.静态方法,类方法,普通方法 类一般常用有三种方法,即为static method(静态方法),cl

  • 一篇文章带你搞定SpringBoot中的热部署devtools方法

    一.前期配置 创建项目时,需要加入 DevTools 依赖 二.测试使用 (1)建立 HelloController @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello devtools"; } } 对其进行修改:然后不用重新运行,重新构建即可:只加载变化的类 三.热部署的原理 Spring Boot 中热部

  • 一篇文章带你搞定Ubuntu中打开Pycharm总是卡顿崩溃

    由于 Ubuntu 中的汉字输入实在是太不友好了,所以装了个 搜狗输入法,好不容易把 搜狗输入法装好,本以为可以开开心心的搞代码了,然而... pycharm 一打开,就崩溃,关不掉,进程杀死还是不行,只能关机重启. 本以为 pycharm 出现了问题,又重装了两遍,还是不行. 最终发现竟然是搜狗输入法以及 fcitx 输入法的锅 唉,只能老老实实的把 fctix 和搜狗输入法卸载了: (1)Ubuntu 软件里卸载 fctix,然后将键盘输入法系统改成 IBus (2)卸载搜狗输入法 先查找软

  • 一篇文章看懂C#中的协变、逆变

    1. 基本概念 官方:协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型.[MSDN] 公式: 协变:IFoo<父类> = IFoo<子类>: 逆变:IBar<子类> =  IBar<父类>: 暂时不理解没关系,您接着往下看. 2. 协变(Covariance) 1) out关键字 对于泛型类型参数,out 关键字可指定类型参数是协变的. 可以在泛型接口

  • 一篇文章看懂SQL中的开窗函数

    目录 OVER的定义 OVER的语法 OVER的用法 OVER在聚合函数中使用的示例 SUM后的开窗函数 COUNT后的开窗函数 OVER在排序函数中使用的示例 ROW_NUMBER() RANK() DENSE_RANK() NTILE() 总结 OVER的定义 OVER用于为行定义一个窗口,它对一组值进行操作,不需要使用GROUP BY子句对数据进行分组,能够在同一行中同时返回基础行的列和聚合列. OVER的语法 OVER ( [ PARTITION BY column ] [ ORDER

随机推荐