IOS之block和内存那些事

这篇文章主要介绍block和内存的一些问题,希望对大家有所帮助,同时也方便自己记忆。


block的一些基本使用,我已经写了一篇,有兴趣的话也可以去看看,block可以降低代码松散度,使代码结构更加轻便灵巧,然而使用block也有一些隐患,例如容易出现循环引用导致内存泄露,这篇文章会剖析为什么会出现这种问题,如何去解决,各个对象在内存中释放的时机等一些问题。

为什么会出现循环引用

解释这个问题我会用一个小例子和图示分析,请看事例:

@implementation RequestUtil
- (void)getRequestData{
    [self.requester startWithCompletionHandler:^(NSData *data){
        _fetchData = data;
    }];
}
@end

假设requester和fetchData是RequestUtil这个类的两个属性,这段代码片段貌似完全没问题,然而它已经出现循环应用了,那么上面的这段代码内存关系图会是下面图示的关系:
这里写图片描述
self对象强引用着requester对象,执行startWithCompletionHandler方法的时候,requester会对block进行强引用,在block中要访问self对象中的属性,首先得对self对象强引用,只有引用了self才能访问self对象中的属性,这样一来就出现一个闭环,谁也无法释放,从而导致内存泄露,这块内存一直被占用着,除非杀死整个app,一个app中如果出现过多的这种情况,会导致app占用的内存过高,内存过高会导致app crash,就算app运行正常,在进入后台的时候,如果用户设备开启了很多app,一旦内存吃紧,IOS系统会首先把你这个占用内存高的app给杀死,所以,要想写出一个性能好的app,内存方面是一定要注意的。

如何解决循环引用

解决循环引用的思路是打破上图所示的闭环,下面我介绍两种方法解决这个问题。
解决方法一:
这种方法是大部分程序哥哥们使用的方法,可以说百分之九十以上都是采用这种方法的,请看这种方法的事例代码:

@implementation RequestUtil
- (void)getRequestData{
    __weak typeof(self) weakSelf = self;
    [self.requester startWithCompletionHandler:^(NSData *data){
        weakSelf.fetchData = data;
    }];
}
@end

上面这段代码就完美的解决了循环引用的问题,这段代码在内存中的关系如图:
这里写图片描述
通过创建一个weak类型的指针指向self对象,block去强引用这个weakSelf就不会出现循环引用了。在内存中释放的顺序是,先self释放,self释放由于没有任何对象强引用requester,requester随着释放,之后block释放,weakSelf也没有任何对象引用,也从内存中释放。

解决方法二:
在合适的地方,手动将block或者requester释放掉。

@implementation RequestUtil
- (void)getRequestData{
    [self.requester startWithCompletionHandler:^(NSData *data){
        _fetchData = data;
        _requester = nil; 
    }];
}
@end
----------------------------------------------------------------------
@implementation RequestUtil
- (void)getRequestData{
    [self.requester startWithCompletionHandler:^(NSData *data){
        _fetchData = data;
    }];
}
@end

@implementation XXXXXXX
- (void)startWithCompletionHandler:(void (^)(NSData *data)) block{
    // do something
    NSData *pngData = UIImagePNGRepresentation([UIImage imageNamed:@"pngName"]);
    block(pngData);
    block = nil; // 一定要确保block执行完毕才可这么做,如果block中又有多线程,那么这么做就不合适了,总之block要在合适的时间释放
}
@end

这两种方法都是可以打破闭环的,如图所示:
这里写图片描述
当requester指向nil的时候,它所引用的block由于没有任何对象引用它,block会在内存中被释放掉。同理,将block指向nil,self会在需要释放的时候释放,requester也随着释放,从而都从内存中释放了。

方法一和方法二的实现思路是一样的,他们不同的地方就是内存消失的时机不同而已,设想如果self是ViewController,如果是用方法一,那么必须得等VC pop掉内存才能被释,而方法二是通过手动来控制,选择合适的时机手动去将requester或block释放掉,而block释放的时候,VC依然可以存在。这样就做到了VC都存在,第一种方法block要等VC释放后一起释放,其实这个时候block已经没有在内存中存在的必要了;第二种方法就可以在VC存在的时候,手动去释放掉block,即:VC存在,block使用过后手动在内存中被释放掉。具体选择哪种方案,看大家喜欢了,不过我还是推荐方法一,因为方法二的操作成本要大一些,并且requester释放后如果还需要使用的话又得重新创建。方法一唯一的不足就是得等VC释放,block才能释放,然而这对app的影响微乎其微。当然你有足够的信心使用第二种方式,将内存控制的更精细,这也是非常值得点赞的。

栈块和堆块

定义块的时候,其所占的内存是分配在栈区的,也就是说,块只在定义它的那个范围有效。请看事例:

void (^block)();
if(flag){
    block = ^{
        // do something
    };
} else {
    block = ^{
        // do something
    };
}
block();

这段代码看上去似乎很OK,其实是有风险的,定义在if和else中的两个block都分配在栈内存中,等离开了相应的if语句块或else语句块范围之后,编译器有可能把分配给block的内存覆写掉。于是,这两个块只能保证在对应的if或else语句范围内有效。这样的代码编译完全没问题的,运行起来就看人品了,若编译器未覆写block在栈区所占用的内存,则程序可以正常运行,若覆写或回收了程序会崩溃。
解决办法是给块发送copy消息,使之拷贝到堆内存中,这样块就成了带引用计数的对象了。

void (^block)();
if(flag){
    block = [^{
        // do something
    } copy];
} else {
    block = [^{
        // do something
    } copy];
}
block();

这样代码就安全了,如果是采用非ARC的,那么最后记得手动释放块在堆区所分配的内存。


最后,综合上面所了解的这么多知识点,我们看一个例子,这个例子是采用的闭包的写法:

@implementation XXX
- (void)funcAAA {

    void (^aaa)() = ^{
        self.a = 10;
    }
    {
        aaa();
    } // 代码块1.
}
@end

看完这个代码片段,请先思考下这段代码有没有出现循环引用,这里其实是没有出现循环引用的,block对self进行了强引用,而block是分配在栈区的,执行aaa()的时候,self对象并不会对aaa这个block强引用,并且aaa这个block再出(代码块1)的时候,内存就会被回收,所以这里是没有出现循环引用的。当然你在这个aaa block中不用self.a,定义一个weakSelf,用weakSelf.a,也是完全没有问题的。

这篇文章就介绍到这了,最后希望大家多多给我提建议,大家共同学习进步

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值