前言:
最近都在折腾 Sagit 架框的内存释放的问题,所以对这一块有些心得.
对于新手,学到的文章都在教你用:typeof(self) __weak weakSelf = self.
对于老手,可能早习惯了到处了 WeakSelf 了.
这次,就来学学,如何不用 WeakSelf.
1:从引用计数器开始:
这里先设计一个 TableBlock 类:
先这么简单,一个 BlockTable 只有一个 block 属性,然后输出一段释放的日志.
@interface BlockTable : NSObject
typedef void (^AddCellBlock)();
@property (nonatomic,copy)AddCellBlock addCell;@end
接着,随意找一个地方写写代码:来 new 了一个 BlockTable,并打印一下信息:
-(void)dealloc
{
NSLog(@"Table relase");//relase为错误字,为了和下图保持一致的错别字,这里就不改了.
}
这时候它的引用数是 1,并且出了 Table relase .
接着给 addCell 属性赋一个值,并运行:
一个空的事件,里面并没有引用到 table,所以引用数还是 1.
2:开始循环引用
在 block 引用 table,让它产生循环引用,并运行:
我们看到:引用数变成了 3,没有输出对象释放信息了,为啥不是 2 呢?大大的问号!!
一个属性赋值,为什么增强两个引用计数?
3:猜解跳跃的计数器
接下来,把属性设置为 nil,运行看看:
设置为 nil,还有 2
也正常释放了?
为了证实自己对这个看起来就很明显的猜想:重写 addCell 的 setter 方法,不进行任何保存:
同时去掉置为 nil 的代码:再运行看看:
@implementation BlockTable
-(void)setAddCell:(AddCellBlock)addCell
{
}
计数器仍为 2,而且也释放了.
经过思考,出来了以下的结论:
:块的定义本身,就会造成1次引用,不过这次引用,在块离开所在的函数时,释放时,抵消掉引用数.
:存档块的时候,会造成1次引用,而这个引用,是内存无法释放的原因.
4:根据上述解释,得到一个疯狂的结论:
只要block的代码只执行1次的,都可以任性的self或其它强引用.
事实上,我们写的代码,很多block的确只执行一次,不管是传的时候就执行,还是传完之后过段时间回调再执行.
认定只要执行1次的,就不需要WeakSelf,除非第三方框架的设计者造孽留坑,忘了在存档block执行后补上block=nil这一刀.
5:消灭赋值的引用计数:
继续发挥想象力,既然存的时候,会增加一次引用,辣么,让它不增加引用不就好了:
我们先给这个 block 定义一个弱引用,然后再赋值给_addCell,运行看看:
@implementation BlockTable
-(void)setAddCell:(AddCellBlock)addCell
{
__weak AddCellBlock addCellWeak=addCell;
_addCell=addCellWeak;
}
哇草,成功了!计数器为 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 不在同一个函数,比如代码像这样:
给外面的类定义了一个 table 属性,然后调用完 start 后再调用 reflesh,运行,会怎样呢?
-(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];
}
出现了 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 可以重建呢?
比如:
:将block转成字符串存档,适当时机还原回来重新赋值
:将block序列化保存,适当时机还原回来?
: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();
}
}
:怎么把block存档?
:怎么将存档数据还原成block.
对搞 C# 的来说,这些都家常便饭,oc 这块还不熟,有路过的朋友可顺路给支支招!!
11:如果第 10 的方式解决不了,就只能,只能,引入时机第三者了
不过这个引入第三者,只是一个时机切入点,在这个时机触发的时候,将其中的一方的引用设置为 nil.
像 Sagit 框架的布局方面的时机,就选在导航回退等事件中处理.
不过这里需要一个小技巧:
在存档 block 时,不一定要存在当前对象,也可以用一个统一的全局 block 管理起来.
这样在业务处理时,根据业务情况,从全局 block 里来移除某些 block 即可.
具体取决于业务,所以这个就不展开了.
总结:
相信,一路看下,看懂了,后续的情况,基本上已经用不上 WeakSelf 这东西了,因为像一个 block,其生命周期必须和持有者保持一致的,还是挺少的.
而这种少的情况,如果第 10 步解决了,基本就全都解决了,解决不了,还有 11.
相信读完此文,如果能完全理解,你就再也看不到 block 前 WeakSelf 这种,WeakSelf 也没有存在必要了.
最后,欢迎大伙关注 IT 连创业,虽然最近我都在折腾 IOS,哈哈.
不过 IOS 基础还是要打劳,后续产品改进起来才有质的飞跃.
来源: https://www.cnblogs.com/cyq1162/p/8235768.html