iOS block,你要看的这都有

1,187 阅读13分钟
原文链接: www.jianshu.com

Blocks are a non-standard extension added by Apple Inc. to Clang's implementations of the C, C++, and Objective-C programming languages that uses a lambda expression-like syntax to create closures within these languages. Blocks are supported for programs developed for Mac OS X 10.6+ and iOS 4.0+,although third-party runtimes allow use on Mac OS X 10.5 and iOS 2.2+ and non-Apple systems.

参照wikipedia,block,其实就是C、C++、OC中闭包的表达。

一、block结构剖析

如果非要想一句话来形容block的话,我会将其比作能截获变量的函数。本节中,我会用一个特别简单的block来进行结构剖析。

分析:

// 首先贴一段最基本的block使用,写在main.m里面
typedef int (^Block)(void);
int main(int argc, char * argv[]) {
    // block实现
    Block block = ^{
        return 0;
    };
    // block调用
    block();
    return 0;
}

// 命令行在main.m文件夹执行命令:clang -rewrite-objc main.m
// 得到main.cpp,我们看下main.cpp中与上述代码直接相关的代码(里面代码很多)
// block内部结构体
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
typedef int (*Block)(void);
// block结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// block方法实现
static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
        return 0;
 }
// block内部结构体
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
// main函数
int main(int argc, char * argv[]) {
    // block实现
    Block block = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    // block调用
    ((int (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

那么,我们从最下面的main函数看起,分为两个步骤,分别对应上面的代码:

  • block实现:调用block结构体的构造函数_main_block_impl_0来实现block,可见,传入的两个参数,分别是函数_main_block_func_0的指针和_main_block_desc_0结构体。
  • block调用:将block作为参数传入block中的FuncPtr,也即_main_block_func_0方法,进行调用。

两个步骤相比较而言,第二步比较简单,也符合一开始的描述,将第一步中的block作为参数传入FucPtr,就能访问block实现位置的上下文。那么,第一步中,我们需要看的就是block的结构体。看下面的注释即可。

// block结构体
struct __main_block_impl_0 {
  // impl结构体,即block的主结构体
  struct __block_impl impl;
  // Desc结构体,即block的描述结构体
  struct __main_block_desc_0* Desc;
  // block结构体的构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// impl结构体
struct __block_impl {
  void *isa;  // 存储位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;  // 按位承载 block 的附加信息
  int Reserved;  // 保留变量
  void *FuncPtr;  // 函数指针,指向 Block 要执行的函数,即__main_block_func_0
};

// Desc结构体
static struct __main_block_desc_0 {
  size_t reserved;  // 结构体信息保留字段
  size_t Block_size;  // 结构体大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

小结:

由上文可以看出,block使用比函数多来两个步骤,即使用构造函数实现和使用FuncPtr的时候需要传入block结构体。可以得出结论,block比函数多了一个功能,就是可以在实现的地方访问上文。

疑问:

1、好像并没有提到block如何截获变量。
2、isa表示的三种存储位置_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock之间有什么关系。

二、截获变量

在第一部分,留下了两个疑问,在本节,我们首先看下变量的截获问题,这里有两个关键点,即截获变量的block的结构体分析和block截获变量的时机。

1、截获变量的block结构体实现

// block截获变量样例代码
typedef int (^Block)(void);
int main(int argc, char * argv[]) {
    int i = 0;
    Block block = ^{
        return i;
    };
    block();
    return 0;
}

// 命令行在main.m文件夹执行命令:clang -rewrite-objc main.m
// 得到main.cpp,我们看下main.cpp中block的结构体和main函数
// block结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// main函数
int main(int argc, char * argv[]) {
    int i = 0;
    Block block = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
    ((int (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

可见,与无变量截获的样例,有两个区别:

  • block结构体中多了一个变量i,这也是为什么说block会截获变量?至于,i的值如何初始化继续看下面。
  • block的构造函数__main_block_impl_0中多了一个参数i和 : i(_i),参数i好理解,为了给i赋值,而 : i(_i)则是c++语法结构体中const类型变量的初始化方式,即将参数_i赋值给block结构体中的变量i。

2、block截获变量的时机

参见二.1中,main函数实现:((int ()())&__main_block_impl_0((void )__main_block_func_0, &__main_block_desc_0_DATA, i))。可以发现block在构造的时刻,会将参数i传入结构体中进行初始化,所以,block会在实现的地方截获变量,而截获的变量的值也是实现时刻的变量值。

三、截获变量的类型

引用官方文档:

1、Global variables are accessible, including static variables that exist within the enclosing lexical scope.
2、Parameters passed to the block are accessible (just like parameters to a function).
3、Stack (non-static) variables local to the enclosing lexical scope are captured as const variables.Their values are taken at the point of the block expression within the program. In nested blocks, the value is captured from the nearest enclosing scope.
4、Variables local to the enclosing lexical scope declared with the _block storage modifier are provided by reference and so are mutable.Any changes are reflected in the enclosing lexical scope, including any other blocks defined within the same enclosing lexical scope. These are discussed in more detail in The _block Storage Type.
5、Local variables declared within the lexical scope of the block, which behave exactly like local variables in a function.Each invocation of the block provides a new copy of that variable. These variables can in turn be used as const
or by-reference variables in blocks enclosed within the block.

大致可以看出block可以中的外部变量一共有五种,依次是全局变量、全局静态变量、局部静态变量、_block修饰的变量和一个局部变量(截获后为const类型)。这里,先把_block剔除,看一下另外四种:

// block截获变量样例代码
#import 
typedef int (^Block)(void);
int a = 0;
static int b = 0;
int main(int argc, char * argv[]) {
    static int c = 0;
    int i = 0;
    NSMutableArray *arr = [NSMutableArray array];
    Block block = ^{
        a = 1;
        b = 1;
        c = 1;
        [arr addObject:@"1"];
        return i;
    };
    block();
    return 0;
}

这里有个问题,如果继续使用clang -rewrite-objc main.m会报错,因为包含了OC库UIKit,所以使用xcrun -sdk iphonesimulator9.3 clang -rewrite-objc main.m,如果想深入了解请看clang -rewrite-objc的使用点滴

// block结构体和构造函数
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *c;
  NSMutableArray *arr;
  int i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, NSMutableArray *_arr, int _i, int flags=0) : c(_c), arr(_arr), i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// block实现的方法
static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *c = __cself->c; // bound by copy
  NSMutableArray *arr = __cself->arr; // bound by copy
  int i = __cself->i; // bound by copy
  a = 1;
  b = 1;
  (*c) = 1;
  ((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id)(NSString *)&__NSConstantStringImpl__var_folders_k__6b9p9yt96y9dq8ds8_kvf3kh0000gn_T_main_3c9752_mi_0);
  return i;
}
// mian函数
int main(int argc, char * argv[]) {
    static int c = 0;
    int i = 0;
    NSMutableArray *arr = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
    Block block = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &c, arr, i, 570425344));
    ((int (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

上述样例中一共有五种外部变量,这里分为三类。
1、全局变量a、和静态全局变量b为第一类,block可以直接修改,没什么要说的。
2、局部静态变量c,从block构造函数可以看出此处传入的是c的指针,所以可以更改c。
3、局部基本变量i,局部对象arr,可以发现i不能修改,而arr因为是对象,可以对对象进行操作(但不能对给arr重新指向其他地址),这时需要注意的是,block内部的i和arr均为const类型,所以均不能对其进行修改。
本节过后,大家对截获变量大概只有两个难点了,即为_block修饰的变量是什么意思和对上述第三种类型的变量,我们如何在block中修改。聪明的童鞋应该想到了,两个难点其实是一个问题:_block就是为了解决block中修改const变量服务的。且看下部分。

四、_block作用

三中留下了截获变量的一个疑点,即如何修改const类型的变量,其实,我们可以从局部静态变量和局部对象[arr addObject:@"1"]中找到一些思路,即不直接操作变量,而是通过操纵其指针,虽然我们不能将指针指向新地址,但是,我们可以对指针空间进行操作。

// 使用_block的样例
#import 
typedef int (^Block)(void);
int main(int argc, char * argv[]) {
    __block int i = 2;
    __block NSMutableArray *arr = [NSMutableArray array];
    Block block = ^{
        i = 1;
        arr = [NSMutableArray array];
        return i;
    };
    block();
    return 0;
}

// 执行xcrun -sdk iphonesimulator9.3 clang -rewrite-objc main.m后代码
// __block为变量i创建的结构体,其中成员i为i的值,forwarding为指向自己的指针
struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};
// __block为变量arr创建的结构体,其中成员arr为arr的值,forwarding为指向自己的指针
struct __Block_byref_arr_1 {
  void *__isa;
  __Block_byref_arr_1 *__forwarding;
  int __flags;
  int __size;
  void (*__Block_byref_id_object_copy)(void*, void*);
  void (*__Block_byref_id_object_dispose)(void*);
  NSMutableArray *arr;
};
// block结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __Block_byref_arr_1 *arr; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,                __Block_byref_i_0 *_i, __Block_byref_arr_1 *_arr, int flags=0) : i(_i->__forwarding), arr(_arr->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// block函数实现
static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref
  __Block_byref_arr_1 *arr = __cself->arr; // bound by ref
  (i->__forwarding->i) = 1;
  (arr->__forwarding->arr) = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
  return (i->__forwarding->i); 
}
// main函数
int main(int argc, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 2};
    __attribute__((__blocks__(byref))) __Block_byref_arr_1 arr = {(void*)0,(__Block_byref_arr_1 *)&arr, 33554432, sizeof(__Block_byref_arr_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"))};
    Block block = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, (__Block_byref_arr_1 *)&arr, 570425344));
    ((int (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}


文/天口三水羊(简书作者)
原文链接:http://www.jianshu.com/p/9a59de31a660
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

从上述代码,可以看出,无论是基本变量i还是对象arr都被_block变成了一个结构体,而其值为结构体里面的成员变量,那么我们也就能明白为什么能在block中对其进行修改了。当然,这里有一个很大的疑问,即简单的结构体就能完成在block里面修改外部变量的要求,而这里为什么会多一个指向结构体自己的_forwarding指针呢?且看下一部分。

// 有兴趣可以看下下面这个样例,也能完成对i的修改,那么问题来了,__block的__forwarding指针到底有什么用处(i输出为3)
#import 
typedef int (^Block)(void);
int main(int argc, char * argv[]) {
    int i = 1;
    int *a = &i;
    Block block = ^{
        (*a)++;
        return 0;
    };
    i ++;
    block();
    NSLog(@"%d", i);
}

五、block的存储类型

在二、三、四节中基本解答了一中提出的截获变量的问题(留下了一个_forwarding指针问题,本节解答),那么从本节开始将解答一中留下的另外一个问题,即isa表示的三种存储位置_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock之间有什么关系。

1、_NSConcreteGlobalBlock

// a、全局声明实现的block
// clang编译后可发现isa指针为_NSConcreteGlobalBlock
#import 
typedef int (^Block)(void);
Block globalBlock = ^{
    return 0;
};
int main(int argc, char * argv[]) {
    globalBlock();
    return 0;
}

// b、局部block未截取任何变量
// clang编译后可发现isa指针为_NSConcreteStackBlock,这就看出了clang的不准确的地方,从此以后,我们通过NSLog打印。此处NSLog打印出的为_NSConcreteGlobalBlock。
#import 
typedef int (^Block)(void);
int main(int argc, char * argv[]) {
    Block globalBlock = ^ {
        return 0;
    };
    globalBlock();
    NSLog(@"%@", globalBlock);
    return 0;
}

2、_NSConcreteStackBlock

ARC环境下,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当block 作为方法或函数的参数传递时,编译器可能不会自动调用 copy 方法。

当 block 作为参数被传入方法名带有 usingBlock的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy或 _Block_copy进行拷贝。

// 下述是狮子书上非常经典的样例,会在Block block = [arr objectAtIndex:1];处crash,因为getBlockArray中的block存储在栈上,已经被回收。
// 这里需要注意的是一定是在参数上定义的block,若在其他地方定义,传入参数中,则不会。
// 这里用的NSLog打印的block显示_NSConcreteStackBlock。
#import 
typedef int (^Block)(void);
id getBlockArray()
{
    int val = 10;
    NSLog(@"%@", ^{NSLog(@"blklog:%d", val);});
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d", val);},
            ^{NSLog(@"blk1:%d", val);}, nil];
}
int main(int argc, char * argv[]) {
    id arr = getBlockArray();
    Block block = [arr objectAtIndex:1];
    block();
    return 0;
}
// 解决方式,不多解释了
#import 
typedef int (^Block)(void);
id getBlockArray()
{
    int val = 10;
    NSLog(@"%@", ^{NSLog(@"blklog:%d", val);});
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"blk0:%d", val) ;} copy],
            [^{NSLog(@"blk1:%d", val);} copy], nil];
}
int main(int argc, char * argv[]) {
    id arr = getBlockArray();
    Block block = [arr objectAtIndex:1];
    block();
    return 0;
}

3、_NSConcreteMallocBlock

这里装个逼:除了上述几类block,其余的block在ARC环境下都是_NSConcreteMallocBlock类型。欢迎打脸,那我就能学到更多知识了。哈哈,开个玩笑。仔细看的人可能会发现,从一到五的clang信息中,样例中的block基本都是_NSConcreteStackBlock,其实不然,应该都是_NSConcreteMallocBlock,你可以通过NSLog方式验证。

三种存储类型不要在clang代码中查看,而是通过NSLog打印。

4、_block、_forwarding与堆

已知arc环境下,栈上的block会自动copy到堆上,而block截取的_block变量也会同时copy到堆上(这块就不细说了,可以去看狮子书)。这就保证了,即使截获的变量出了作用域,block也能访问到它,而在四中最后提到的方法则无法保证。如下样例。

#import 
typedef int (^Block)(void);
// 声明一个全局block
Block globalBlock;
void test() {
    int i = 1;
    int *a = &i;
    Block block = ^{
        (*a)++;
        NSLog(@"%d", *a);
        return 0;
    };
    block();
    // 为全局block初始化
    globalBlock = block;
}
int main(int argc, char * argv[]) {
    test();
    globalBlock();
}
// 大家心目中的输出应该为
**2016-12-15 17:30:21.188 block[8683:10811473] 3**
**2016-12-15 17:30:21.189 block[8683:10811473] 4**
// 实际输出为
**2016-12-15 17:30:21.188 block[8683:10811473] 3**
**2016-12-15 17:30:21.189 block[8683:10811473] 24705**

为什么会出现,这种结果呢,因为*a指向局部变量i,而i在出了test方法作用域后回收内存,所以出现一个不知道哪来的数。那么__block能处理这种情况吗?

#import 
typedef int (^Block)(void);
Block globalBlock;
void test() {
    __block int i = 1;
    Block block = ^{
        i ++;
        NSLog(@"%d", i);
        return 0;
    };
    i ++;
    block();
    globalBlock = block;
}
int main(int argc, char * argv[]) {
    test();
    globalBlock();
}
// 输出
**2016-12-15 17:34:07.092 block[9046:10815940] 3**
**2016-12-15 17:34:07.094 block[9046:10815940] 4**

完美。这里看一下四中_block结构体的构造函数

// 由下可以看出,结构体内的i和arr都是由结构体内的_forwarding指针初始化的,这保证了block外部栈变量的独立性,也保证了block截获变量可以copy到堆上,因为是_block类型。
// 由四中的clang代码还可以看出,无论block内外,访问i或者arr的方式均是i->__forwarding->i或arr->__forwarding->arr。这保证了block内外访问的都是同一个内存(这里指的堆上的内存)。
__Block_byref_i_0 *_i, __Block_byref_arr_1 *_arr, int flags=0) : i(_i->__forwarding), arr(_arr->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}

六、循环引用

文章到这也就收尾了,大家应该也差不多知道了block会截获变量,即会持有变量,所以可能引起循环引用,关于循环引用我曾经写过一篇文章,讲的比较全面,参见循环引用,看我就对了

七、参考文献

1、狮子书:Objective-C高级编程 iOS与OS X多线程与内存管理
2、www.zybuluo.com/MicroCai/no…
3、blog.tingyun.com/web/article…
4、en.wikipedia.org/wiki/Blocks…)

文中部分__block均写成了_block,不要介意。如对您有所帮助,喜欢一下,关注一下,谢谢~~