• 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属性,然后输出一段释放的日志。

    -(void)dealloc
    {
        NSLog(@"Table relase");//relase为错误字,为了和下图保持一致的错别字,这里就不改了。
    }

    接着,随意找一个地方写写代码:来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基础还是要打劳,后续产品改进起来才有质的飞跃。

  • 相关阅读:
    日志工具——slf4j
    统一建模语言——UML
    Java基础——网络编程
    Java基础——语法基础
    Java基础——NIO(二)非阻塞式网络通信与NIO2新增类库
    Java基础——NIO(一)通道与缓冲区
    动态加载script文件的两种方法
    asp.net 通用的连接数据库实例代码
    Nginx用户认证配置方法详解(域名/目录)
    js冒泡法和数组转换成字符串示例代码
  • 原文地址:https://www.cnblogs.com/cyq1162/p/8235768.html
Copyright © 2020-2023  润新知