zoukankan      html  css  js  c++  java
  • iOS中block实现的探究


    [0. Brief introduction of block]

    Block是iOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性。

    用维基百科的话来说,Block是Apple Inc.为C、C++以及Objective-C加入的特性,使得这些语言能够用类lambda表达式的语法来创建闭包

    用Apple文档的话来说,A block is an anonymous inline collection of code, and sometimes also called a "closure".

    关于闭包,我认为阮一峰的一句话解释简洁明了:闭包就是可以读取其他函数内部变量的函数

    这个解释用到block来也非常恰当:一个函数里定义了个block,这个block能够訪问该函数的内部变量。

    一个简单的Block示比例如以下:

    int (^maxBlock)(int, int) = ^(int x, int y) { return x > y ? x : y; };

    假设用Python的lambda表达式来写,能够写成例如以下形式:

    f = lambda x, y : x if x > y else y

    只是由于Python自身的语言特性,在def定义的函数体中,能够非常自然地再用def语句定义内嵌函数,由于这些函数本质上都是对象。

    假设用BNF来表示block的上下文无关文法,大致例如以下:

    block_expression  ::=  ^  block_declare  block_statement
    block_declare  ::=  block_return_type  block_argument_list
    block_return_type ::=  return_type  |  空
    block_argument_list  ::=  argument_list  |  空


    [1. Why block]

    Block除了可以定义參数列表、返回类型外,还可以获取被定义时的词法范围内的状态(比方局部变量),而且在一定条件下(比方使用__block变量)可以改动这些状态。此外,这些可改动的状态在同样词法范围内的多个block之间是共享的,即便出了该词法范围(比方栈展开,出了作用域),仍可以继续共享或者改动这些状态。

    通常来说,block都是一些简短代码片段的封装,适用作工作单元,通经常使用来做并发任务、遍历、以及回调。

    比方我们能够在遍历NSArray时做一些事情:

    - (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;

    当中将stop设为YES,就跳出循环,不继续遍历了。

    而在非常多框架中,block越来越常常被用作回调函数,代替传统的回调方式。

    • 用block作为回调函数,能够使得程序猿在写代码更顺畅,不用中途跑到还有一个地方写一个回调函数,有时还要考虑这个回调函数放在哪里比較合适。採用block,能够在调用函数时直接写兴许处理代码,将其作为參数传递过去,供其任务运行结束时回调。
    • 还有一个优点,就是採用block作为回调,能够直接訪问局部变量。比方我要在一批用户中改动一个用户的name,改动完毕后通过回调更新相应用户的单元格UI。这时候我须要知道相应用户单元格的index,假设採用传统回调方式,要嘛须要将index带过去,回调时再回传过来;要嘛通过外部作用域记录当前操作单元格的index(这限制了一次仅仅能改动一个用户的name);要嘛遍历找到相应用户。而使用block,则能够直接訪问单元格的index。

    这份文档中提到block的几种适用场合:

    • 任务完毕时回调处理
    • 消息监听回调处理
    • 错误回调处理
    • 枚举回调
    • 视图动画、变换
    • 排序


    [2. About __block_impl]

    Clang提供了中间代码展示的选项供我们进一步了解block的原理。

    以一段非常easy的代码为例:


    使用-rewrite-objc选项编译:


    得到一份block0.cpp文件,在这份文件里能够看到例如以下代码片段:


    从命名能够看出这是block的实现,而且得知block在Clang编译器前端得到实现,能够生成C中间代码。非常多语言都能够仅仅实现编译器前端,生成C中间代码,然后利用现有的非常多C编译器后端。

    从结构体的成员能够看出,Flags、Reserved能够先略过,isa指针表明了block能够是一个NSObject,而FuncPtr指针显然是block相应的函数指针。

    由此,揭开了block的神奇面纱。

    只是,block相关的变量放哪里呢?上面提到block能够capture词法范围内(或者说是外层上下文、作用域)的状态,即便是出了该范围,仍然能够改动这些状态。这是怎样做到的呢?


    [3. Implementation of a simple block]

    先看一个仅仅输出一句话的block是怎么样的。


    生成中间代码,得到片段例如以下:


    首先出现的结构体就是__main_block_impl_0,能够看出是依据所在函数(main函数)以及出现序列(第0个)进行命名的。假设是全局block,就依据变量名和出现序列进行命名。__main_block_impl_0中包括了两个成员变量和一个构造函数,成员变量各自是__block_impl结构体和描写叙述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。

    接着出现的是__main_block_func_0函数,即block相应的函数体。该函数接受一个__cself參数,即相应的block自身。

    再以下是__main_block_desc_0结构体,当中比較有价值的信息是block大小。

    最后就是main函数中对block的创建和调用,能够看出运行block就是调用一个以block自身作为參数的函数,这个函数相应着block的运行体

    这里,block的类型用_NSConcreteStackBlock来表示,表明这个block位于栈中。相同地,还有_NSConcreteMallocBlock_NSConcreteGlobalBlock

    因为block也是NSObject,我们能够对其进行retain操作。只是在将block作为回调函数传递给底层框架时,底层框架须要对其copy一份。例如说,假设将回调block作为属性,不能用retain,而要用copy。我们一般会将block写在栈中,而须要回调时,往往回调block已经不在栈中了,使用copy属性能够将block放到堆中。或者使用Block_copy()和Block_release()。


    [4. Capture local variable]

    再看一个訪问局部变量的block是如何的。


    生成中间代码,得到片段例如以下:


    能够看出这次的block结构体__main_block_impl_0多了个成员变量i,用来存储使用到的局部变量i(值为1024);而且此时能够看到__cself參数的作用,类似C++中的this和Objective-C的self。

    假设我们尝试改动局部变量i,则会得到例如以下错误:


    错误信息非常具体,既告诉我们变量不可赋值,也提醒我们要使用__block类型标识符。

    为什么不能给变量i赋值呢?

    由于main函数中的局部变量i和函数__main_block_func_0不在同一个作用域中,调用过程中仅仅是进行了值传递。当然,在上面代码中,我们能够通过指针来实现局部变量的改动。只是这是由于在调用__main_block_func_0时,main函数栈还没展开完毕,变量i还在栈中。可是在非常多情况下,block是作为參数传递以供兴许回调运行的。通常在这些情况下,block被运行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针訪问就⋯⋯。

    所以,对于auto类型的局部变量,不同意block进行改动是合理的。


    [5. Modify static local variable]

    于是我们也能够判断出,静态局部变量是怎样在block运行体中被改动的——通过指针。

    由于静态局部变量存在于数据段中,不存在栈展开后非法訪存的风险。


    上面中间代码片段与前一个片段的区别主要在于main函数里传递的是i的地址(&i,以及__main_block_impl_0结构体中成员i变成指针类型(int *)。

    然后在运行block时,通过指针改动值。

    当然,全局变量、静态全局变量都能够在block运行体内被改动。更准确地讲,block能够改动它被调用(这里是__main_block_func_0)时所处作用域内的变量。比方一个block作为成员变量时,它也能够訪问同一个对象里的其他成员变量。


    [6. Implementation of __block variable]

    那么,__block类型变量是怎样支持改动的呢?


    我们为int类型变量加上__block指示符,使得变量i能够在block函数体中被改动。

    此时再看中间代码,会多出非常多信息。首先是__block变量相应的结构体:


    由第一个成员__isa指针也能够知道__Block_byref_i_0也能够是NSObject。

    第二个成员__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,仅仅能说有时候须要指向还有一个__Block_byref_i_0结构。

    最后一个成员是目标存储变量i。

    此时,__main_block_impl_0结构例如以下:


    __main_block_impl_0的成员变量i变成了__Block_byref_i_0 *类型。

    相应的函数__main_block_func_0例如以下:


    亮点是__Block_byref_i_0指针类型变量i,通过其成员变量__forwarding指针来操作还有一个成员变量。 :-)

    而main函数例如以下:


    通过这样看起来有点复杂的改变,我们能够改动变量i的值。可是问题相同存在:__Block_byref_i_0类型变量i仍然处于栈上,当block被回调运行时,变量i所在的栈已经被展开,怎么办?

    在这样的关键时刻,__main_block_desc_0站出来了:


    此时,__main_block_desc_0多了两个成员函数:copy和dispose,分别指向__main_block_copy_0__main_block_dispose_0

    当block从栈上被copy到堆上时,会调用__main_block_copy_0将__block类型的成员变量i从栈上拷贝到堆上;而当block被释放时,对应地会调用__main_block_dispose_0来释放__block类型的成员变量i。

    一会在栈上,一会在堆上,那假设栈上和堆上同一时候对该变量进行操作,怎么办?

    这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被拷贝到堆上时,栈上的那个__Block_byref_i_0结构体中的__forwarding指针也会指向堆上的结构


    /* ---------------------------------------------------------------------------------------------------- */

    本来还想继续写下去,结果发现文章有点长了。先到此。

    原文链接:http://blog.csdn.net/jasonblog/article/details/7756763

    Jason Lee @ Hangzhou


  • 相关阅读:
    Leetcode 538. Convert BST to Greater Tree
    Leetcode 530. Minimum Absolute Difference in BST
    Leetcode 501. Find Mode in Binary Search Tree
    Leetcode 437. Path Sum III
    Leetcode 404. Sum of Left Leaves
    Leetcode 257. Binary Tree Paths
    Leetcode 235. Lowest Common Ancestor of a Binary Search Tree
    Leetcode 226. Invert Binary Tree
    Leetcode 112. Path Sum
    Leetcode 111. Minimum Depth of Binary Tree
  • 原文地址:https://www.cnblogs.com/gcczhongduan/p/4466726.html
Copyright © 2011-2022 走看看