Block 到底啥时候崩溃?

作者: 梦想编程家小枫 | 来源:发表于2018-04-27 22:33 被阅读243次

    文章目录

    block 的数据结构

    全局block

    栈block

    堆block

    ARC下的 block

    全局block

    堆block和栈block

    结论

    block可以说是OC一项非常好用的功能。block的本质,实际上是『带有自动变量值的匿名函数』。但是在block的使用上,有各种江湖传说,说在某某情况下,block的使用是不安全的,会造成崩溃。于是也有很多面试题喜欢考察block。但是,实际的block的不安全使用,貌似除了循环引用,也没遇到过什么情况啊?我敢说,block在现如今的iOS开发中,99%的崩溃都是因为你没有给block判空。而其他问题,都是因为循环引用。那么block到底啥时候不安全呢?

    其实关于block,我们不用那么害怕。

    block 的数据结构

    首先,block的数据结构其实可以通过查看源码来获得。关于block的数据结构和runtime是开源的,可以在llvm项目看到,或者下载苹果的libclosure库的源码来看。苹果也提供了在线的代码查看方式,其中包含了很多示例和文档说明。

    所以,block真正的结构,就是这个样子:

    在objc中,根据对象的定义,凡是首地址是*isa的结构体指针,都可以认为是对象(id)。这样在objc中,block实际上就算是对象。

    那么既然block是个对象,那么block就应该有Class,那么block的Class是什么呢?

    在block runtime中,定义了6种类:

    _NSConcreteStackBlock 栈上创建的block

    _NSConcreteMallocBlock 堆上创建的block

    _NSConcreteGlobalBlock 作为全局变量的block

    _NSConcreteWeakBlockVariable

    _NSConcreteAutoBlock

    _NSConcreteFinalizingBlock

    其中我们能接触到的主要是前3种,后三种用于GC不做讨论。

    全局block

    其实,这三种block类型的情况非常好理解。

    首先我们要明确,在编译完成后,block内部的代码将会提取出来,成为一个单独的C函数。创建block时,实际就是在方法中声明一个struct,并且初始化该struct的成员。而执行block时,就是调用那个单独的C函数,并把该struct指针传递过去。block的的实际作用效果,相当于C语言中的匿名函

    于是,就可以理解_NSConcreteGlobalBlock的使用了。因为全局block是当一个block内部没有捕获任何外部变量时,就会是一个全局block类型。此时,这个block与一个函数无异。所以,那么它就应该有和函数一样的静态特性。而且,我们在调用block的时候,其实和普通C函数的调用很相似,都是名称加括号:block()。

    那么有函数一样静态特性的block,显然不需要再取考虑他的生命周期。

    栈block

    这个类型的block,是在编译器发现block内部引用了外部变量后,会生成的block类型。

    在block内部有引用外部变量时,当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。

    当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。详细代码可以直接点这里查看。

    之所以这样设计,实际上,可以认为成,当block有了外部变量的捕获,那么它就需要持有这个外部变量,就是赋值到结构体成员上。这种捕获,造成了block对应struct结构体大小的动态变化,所以,在设计上适合放在栈上更合理。

    堆block

    在栈block中,说到过,当函数的栈帧的销毁,那么栈block也会被随之清楚。但是我们一般都需要在函数结束后仍然能使用这个block,所以,需要把栈block拷贝到堆上。在copy时,就把栈block的类型转换成了堆block。

    所以在MRC时代,block属性的关键字必须是copy。这样就可以保证在给block属性赋值的时候,能把在栈上的block给copy到堆区。

    而讲得再细一点,为什么非要把block放到堆区才安全。

    因为你可以这么理解,block就是个匿名函数,只不过我们给了一个变量来引用这个匿名函数,在需要的时候调用。但是,栈block会随着函数栈帧的销毁而销毁,这样一来,我们用之前做引用的变量再去调用这么一块被销毁的内存,就会出现内存崩溃

    所以,只有把block放到由我们来控制生命周期的堆区中,才能安全地使用block。

    我们知道,在OC中,对象都会在堆区存储。实际上,此时的堆block,它的确就是一个对象。而且,你还需要对它手动release。

    当然,当ARC时代来临,这就又有所不同了。

    ARC下的 block

    首先先看我写的一段测试代码:

    这是在ARC下的Command line tools工程。

    这段代码中的str是做常量区地址参考的,最后的obj是做堆区地址参考的。

    可以看到str的地址是0x1000021f0,obj的地址是0x100406b40。的确符合预期,常量区地址非常小,堆区地址稍微大一些。

    全局block

    先看gBlock。这个block没有捕获任何外部变量,仅仅是打印一句文字。所以,理所当然,它是一个全局block。通过地址的观察,的确如此,比str常量的地址还小。

    堆block和栈block

    再看mBlock。这个block是捕获了一个外部变量,打印一个外部声明的字符串。这种情况下,在MRC小中应该是属于栈block。但是这里的执行结果显示,它实际是一个堆block。

    实际上,这是因为在ARC下,对block做了大量处理。现在的情况是,只要一个block被赋值给一个strong变量,会自动copy。所以,我们看到mBlock这个地址,和参考对象obj的地址非常接近。

    这样一来,实际上在ARC下,很难在写出一个栈block的情况,因为一旦有赋值给strong变量,那么就得到的是堆block。所以,为了写出一个栈block,我使用一个函数,入参是block类型,但是这个block参数没有经过任何一次赋值操作,直接放在了函数参数里。所以,这样就可以再函数体内得到这个栈block类型的参数。

    通过地址,可以看到,这个栈block的确地址很大。在给这个栈block进行一次手动copy后,block也变成了堆block。

    不过,这里虽然倒弄出来一个栈block,不过这种情况是不会有栈block被提前释放的问题。因为这个栈block作为入参,他的生命周期本身也是跟随这个函数的函数体。函数体栈帧释放,block也被释放,由于函数外并没有对block有引用,所以,这个block也可以被安全的释放。

    结论

    网上很多关于block的资料,说使用block会造成崩溃,实际都是因为文章太老。现阶段的block是非常安全的。而且LLVM编译器的检查也十分完善,可以提前发现仅有的一些block被提前释放的情况。

    所以,在ARC下,你可以大胆地使用block,而不太需要在意block本身的生命周期。因为他实际和我们平常用的其他NSObject对象的表现,并无二致。

    (其实作为一名开发者,拥有一个良好的交流圈是非常重要,这个小编的一个qq交流群659170228,群文件也分享了很多资料供大家学习,非常期待各位程序员加入,同时也欢迎招聘的、找工作的,提供一个更大的平台)

    相关文章

      网友评论

      本文标题:Block 到底啥时候崩溃?

      本文链接:https://www.haomeiwen.com/subject/gfzilftx.html