美文网首页iosiOS进阶iOS面试知识点收集
Objective-C高级编程读书笔记之blocks

Objective-C高级编程读书笔记之blocks

作者: Jerry4me | 来源:发表于2016-05-11 16:23 被阅读1066次
    Objective-C高级编程 iOS与OS X多线程和内存管理

    Objective-C高级编程读书笔记三部曲已经写完, 另外两篇如下 :
    Objective-C高级编程读书笔记之内存管理
    Objective-C高级编程读书笔记之GCD


    Blocks

    这里有五道关于block的测试题, 大家可以去做做测试看看自己对block了解多少.

    目录

    1. Block的定义
    2. Block有哪几种类型
    3. Block特性
    4. __block修饰符
    5. block调用copy方法的内部实现
    6. block的循环引用问题
    7. 总结

    1. block的定义

    block是Objective-C对于闭包的实现(闭包是一个函数<或者指向函数的指针>加上函数有关的自由变量).

    block的数据结构

    block也是对象, 以下对block结构体的成员作简单解释

    • isa : 指向Class对象的指针, 所有对象都有该指针
    • flags : 用于按 bit 位表示一些 block 的附加信息
    • reserved : 保留变量
    • invoke : 指向函数的指针, 指向block的实现代码
    • descriptor : 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
    • 捕捉到的变量 : 捕捉过来的变量,block 之所以能够访问它外部的局部变量,就是因为将这些变量(或对象的地址)拷贝到了结构体中。
    • copy : 用于保留捕获的对象
    • dispose : 用于释放捕获的对象

    2. block的类型

    • 全局块(_NSConcreteGlobalBlock)
    • 栈块(_NSConcreteStackBlock)
    • 堆块(_NSConcreteMallocBlock)

    这三种block各自的存储域如下表

    设置对象的存储域
    _NSConcreteStackBlock
    _NSConcreteGlobalBlock 程序的数据区域(.data区)
    _NSConcreteMallocBlock

    说明 :

    • 全局块存在于全局内存中, 相当于单例.
    • 栈块存在于栈内存中, 超出其作用域则马上被销毁
    • 堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存

    全局块(_NSConcreteGlobalBlock)

    • block定义在全局变量的地方
    • block没有截获任何自动变量

    以上两个情况满足任意一个则该block为全局块, 全局块的生命周期贯穿整个程序, 相当于单例.

    栈块(_NSConcreteStackBlock)

    只要不是全局块, 且block没有被copy, 就是栈块.栈块的生命周期很短, 当前作用域结束, 该block就被废弃. 要想在当前作用域以外的地方使用该block, 应该把该block从栈copy到堆上

    从栈复制到堆上的Block与__block变量

    堆块(_NSConcreteMallocBlock)

    简单来说, 栈块copy之后就变成堆块, 这简单吧~

    ARC下的block类型

    因为ARC下默认变量修饰符为__strong, 所以我们接触到的block几乎全是堆block和全局block.
    <pre>
    ARC下, blk = block; 相当于 blk = [block copy];
    </pre>


    3. block特性

    1. 截获自动变量值

    1> 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的. 也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大.

    拷贝

    <pre>
    int age = 10;
    myBlock block = ^{
    NSLog(@"age = %d", age);
    };
    age = 18;
    block();
    </pre>

    输出为
    age = 10

    2> 对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的.

    引用地址

    <pre>
    __block int age = 10;
    myBlock block = ^{
    NSLog(@"age = %d", age);
    };
    age = 18;
    block();
    </pre>

    输出为
    age = 18

    意味着对于第一种情况, 在block外部修改变量的值并不会应该block内部变量的值.而第二种情况则反之.
    并且第一种情况block内部不允许修改变量的值, 第二种情况下可以. (有例外, 静态变量, 静态全局变量, 全局变量即使不使用__block修饰符也可以在block内部修改其值)

    2. 截获对象

    对象不同于自动变量, 就算对象不加上__block修饰符, 在block内部能够修改对象的属性.
    block截获对象与截获自动变量有所不同.
    堆块会持有对象, 而不会持有__block修饰的对象, 而栈块永远不会持有对象, 为什么呢?

    1. 堆块作用域不同于栈块, 堆块可以超出其作用域地方使用, 所以堆块结构体内部会保留对象的强指针, 保证堆块在生命周期结束之前都能访问对象. 而对于__block对象为什么不会持有呢? 原因很简单, 因为__block对象会跟随block被复制到堆中, block再去引用堆中的__对象(后面会讲这个过程)..
    1. 栈块只能在当前作用域下使用, 所以其内部不会持有对象. 因为不存在在作用域之外访问对象的可能(栈离开当前作用域立马被销毁)

    4. __block修饰符

    为什么__block修饰符修饰的变量就能够在block内部修改呢?? 原因在此
    利用<pre> clang -rewrite-objc 源代码文件名 </pre>便可揭开其神秘的面纱.

    <pre>
    __block int val = 10;
    转换成
    __Block_byref_val_0 val = {
    0,
    &val,
    0,
    sizeof(__Block_byref_val_0),
    10
    };
    </pre>

    天哪! 一个局部变量加上__block修饰符后竟然跟block一样变成了一个__Block_byref_val_0结构体类型的自动变量实例.

    此时我们在block内部访问val变量则需要通过一个叫__forwarding的成员变量来间接访问val变量(下面会对__forwarding进行详解)


    5. copy

    block的copy操作究竟做了什么呢?

    这里不得不提及__block变量的存储域

    __block变量的配置存储域| block从栈复制到堆时的影响
    --- | --- | ---
    栈 | 从栈复制到堆并被Block持有
    堆 | 被Block持有

    Block中使用__block变量

    由上图可知, 对一个栈块进行copy操作会连同block与__block变量(不管有没有使用)在内一同copy到堆上, 并且block会持有__block变量(使用).
    ps : 堆上的block及__block变量均为对象, 都有各自的引用计数

    当然, 当block被销毁时, block持有的__block也会被释放

    Block废弃和__block变量的释放

    到这里我们能知道, 此思考方式与Objective-C的引用计数内存管理完全相同.

    那么有人就会问了, 既然__block变量也被复制到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?? __forwarding 终于要闪亮登场了

    复制__block变量

    通过__forwarding, 无论实在block中, block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量.


    什么时候我们需要手动对block调用copy方法

    前面我们说到 : 要想在当前作用域以外的地方使用该block, 应该把该block从栈copy到堆上. 实际上, 在ARC下, 以下几种情况下, 编译器会帮我们把栈上的block复制到堆中

    • block作为函数返回值返回时
    • 将block赋值给__strong修饰符id类型或block类型成员变量时
    • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递block时

    理论上我们只有把block作为函数/方法的参数传入时才需要对block进行copy操作.

    我们对不同地方的block调用copy会产生什么效果呢?

    Block的类 副本源的配置存储域 拷贝效果
    _NSConcreteStackBlock 从栈拷贝到堆
    _NSConcreteGlobalBlock 程序的数据区域 什么也不做
    _NSConcreteMallocBlock 引用计数增加

    所以, 不管block是什么类型, 在什么地方, 用copy方法都不会引起任何问题.如下表格所示. 就算是反复多次调用copy方法, 如

    <pre> blk = [[[[blk copy] copy] copy] copy]; </pre>

    该源码可解释如下 :
    <pre>
    {
    block tmp = [blk copy]; // block被tmp持有
    blk = tmp; // block被tmp和blk持有
    }
    // tmp超出作用域, 其指向的block也被释放, block被blk持有
    {
    block tmp = [blk copy]; // block被tmp和blk持有
    blk = tmp; // blk指向的旧block释放, 并强引用新block, 最终block被tmp和blk持有
    }
    // tmp超出作用域, 其指向的block也被释放, block被blk持有
    ...下面不断重复该过程
    </pre>

    我们知道, 这只是一个循环的过程, block被tmp持有 -> block被tmp和blk持有 -> block被blk持有 -> block被tmp和blk持有 -> ......

    由此可得知, 在ARC下该代码也没有任何问题.

    总结 : 如果block需要给作用域外的地方使用, 但是你不知道需不需要copy, 那就copy吧. 反正不会错


    6. block的循环引用

    这部分相信大家都清楚怎样做能破环, 所以我在这就只简单说两句

    • MRC下用__block可以避免循环引用(原因见上面block特性之截获自动变量值)
    • ARC下用__weak来避免循环引用

    这里需要提醒大家的是, 只有堆块(_NSConcreteMallocBlock)才可能会造成循环引用, 其他两种block不会


    7. Block总结 :

    • block相比函数更加方便, 高效, 苹果强烈推荐使用
    • ARC下编译器会帮助我们更好地管理block的生命周期
    • ARC下block属性声明为strong或copy其实都一样, 因为编译器内部会帮我们实现copy方法
    • 善用__weak(ARC)或__block(MRC)来避免循环引用

    推荐几篇有关block的文章
    谈Objective-C block的实现
    让我们来深入简出block吧


    欢迎大家关注@Jerry4me, 关注菜鸟成长_. 我会不定时更新一些学习心得与文章.

    相关文章

      网友评论

      • Joy___:哇 写得蛮认真的
        Jerry4me:@Martin_Joy 只是一些小总结,不写生怕白读了:joy::joy:
      • 思也007:流程图是用什么做的?
        Jerry4me:@鸣镝 用PPT临时做的

      本文标题:Objective-C高级编程读书笔记之blocks

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