Objective-C中的Block

Posted __Sunshine_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Objective-C中的Block相关的知识,希望对你有一定的参考价值。

主要从下面几方面介绍下Block:

1、Block概念

Xcode文档对Block的概念是这么介绍的:

Block对象是一个C级别的语法和运行机制。它允许你写一些函数语句,这些函数语句可以传到API中,可以有选择性地存储,可以用于多线程中,而且还可以引用局部变量和保存对局部变量的存取。……Block可以同时用在C, C++,或者Objective-C,使得程序更有效率和更好的维护。

2、Block的基本用法

Block的用法也是:声明 ——> 创建 ——> 使用

1)声明

先回顾下C的函数指针,比如想用一函数指针指向sum函数:

int sum(int x, int y)
    return x + y;

则方法为:

int (*p) (int, int);            //定义一函数指针p
p = sum;                        //指针变量p指向函数sum
printf("sum = %d \\n", p(2, 3)); //p(2, 3)等同于 sum(2, 3)

而Block的声明和C的函数指针声明很类似,只需把指针变量前的“*”改为“^”。如:

int (^sumBlock)(int, int);

即: 返回类型 (^block名称)(参数列表)= ^ 返回类型(参数列表) // 代码实现; ;
声明了一个Block对象sumBlock,sumBlock指向了一个代码块,代码块需要两个int型参数(int , int ),返回值类型是int 。

2)创建和使用

Block表达式以“^”开始,以“;”结束,使用Block方式也与调用函数类似,如:

sumBlock = ^(int a, int b)         //创建变量sumBlock
     return a + b;                   //代码实现
;                                  
NSLog(@"sum = %d", sumBlock(2, 3)); //使用变量sumBlock,实参为 2 和 3
//输出结果:sum = 5

当声明和创建放在任何函数外,则是创建了全局Block。如:

//  main.m
#import <Foundation/Foundation.h>
//声明和创建全局Block对象sumBlock
int (^sumBlock)(int, int) = ^(int a, int b)
    return a + b;
;
int main(int argc, const char * argv[]) ……

当Block无参数、无返回值时,则是Block 的最简单形式:

格式: Void (^block名) () = ^ 代码块; ;
使用: block名();

无参数时,参数列表可省略。也可以有参数无返回值,或者无参数有返回值。

3)block的typedef

也是先回顾函数指针的typedef:

typedef int (*FUN) (int, int); //此时FUN代表一个类型,这个类型的变量是一个函数指针
FUN f;                         // 等同于 int (*p) (int, int);
f = sum;
printf("sum = %d \\n", f(2, 3));

而用typedef定义block类型的格式是:

Typedef 返回值类型 (^类型别名)(参数类型列表);

如:

typedef int(^BLOCK)(int, int);             //给一类型起别名BLOCK,这个类型是一个有参有返回值的BLOCK类型
int main(int argc, const char * argv[]) 
    @autoreleasepool 
        BLOCK sumBlock2;                   //用BLOCK类型定义一个变量sumBlock2
        sumBlock2 = ^(int a, int b)
                return a + b;
            ;
        NSLog(@"sum = %d", sumBlock2(2, 3));//使用sumBlock2变量
    
    return 0;

3、Block 与变量

Block在OC中的实现是闭包(closure)。先理解什么是闭包(closure),闭包并不是Objective-C特有的,在C中表现为回调函数(Callbacks),在C++中表现为函数对象(Function objects),在维基百科上(https://en.wikipedia.org/wiki/Closure_(computer_programming))的介绍是:

“In programming languages, closures (also lexical closures or function closures) are a technique for implementing lexically scoped name binding in languages with first-class functions. Operationally, a closure is a record storing a function together with an environment:[1] a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or storage location to which the name was bound when the closure was created ”

即在计算机语言中,闭包(也叫词法闭包(lexical closures)或者函数闭包(function closures))是记录了一个函数和它所在的环境,并将函数的每个自由变量(在封闭域里定义,局部使用的变量)和其值或内存地址关联起来。即使这个函数的调用在变量的作用域之外,也可以通过闭包的引用来访问捕获(captured)到的变量。
如以下程序:

void (^wdd)();                      //声明一个无参数无返回值的全局block变量wdd
void callC()                       //定义一个普通函数,在函数中调用block变量
    wdd();

int main(int argc, const char * argv[]) 
    @autoreleasepool 
        int c = 8;                  //定义一个局部变量
        NSLog(@"c = %d, c addr: %p", c, &c);
        wdd = ^
            NSLog(@"c = %d, c addr: %p", c, &c);//在block中使用变量c
        ;
        callC();                    //调用一般函数
    
    return 0;

程序运行:

已经声明但未初始化的全局block变量wdd是一个结构体,内部指针都为NULL


虽然不执行wdd里面的代码块,但可以看到wdd内部的成员已经被赋予了值


调用普通函数callC(),虽然函数无参数,但在函数中可调用全局block变量wdd


调用wdd,则又回到main函数中创建wdd的代码块


虽然是在callC()函数中调用block变量wdd,而且wdd要用的变量c的作用域只是在main函数而部在callC()函数中(即原始的变量“c”已经脱离了它当初的变量环境),但仍可以用block变量wdd来输出c的值及其地址。而且可以看到在block的内外“c”的值一样而地址不一样,所以这种情况是在block内把变量“c”与它的值关联起来了。

由上面的调试截图也可以看到block变量wdd是有结构体的。
在Block_private.h中对block的结构体定义如下:

struct Block_descriptor 
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
;
struct Block_layout 
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
;

即block在内存的布局为:

(图片来自http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/
一个block实例包含有:isa指针(OC对象都有的指针,所以block实例也是个对象),flags(用于按bit位表示一些block的附加信息),reserved(保留变量),invoke(函数指针,指向具体的block实现的函数调用地址),descriptor(表示该block的附加描述信息),variables(获取过来的变量,使得block能够访问外部的局部变量)。

从block角度来看,有五种变量:全局变量(global variables,可访问)、参数(parameters,和传给函数的参数的用法一样)、栈变量(局部变量,以const变量出现在block内)、用__block修饰的局部变量(可修改)、在block内定义的局部变量(如同在函数里定义的局部变量)。
1)block具有封闭性(closure),所以它能访问非局部变量(即定义在包含block的作用域、但在block之外的变量)。效果相当于变量在block定义的地方被拍了个照。如:

        int m = 10;                         //在block外部定义的变量 m
        NSLog(@"before the block:");
        NSLog(@"m = %d, m addr: %p", m, &m);//使用block前,m的值和地址
        void (^myBlock)() = ^              //定义block
            NSLog(@"in the block:");
            NSLog(@"m = %d, m addr: %p", m, &m);
        ;
        m = 50;                             //在block定义后、使用前修改m的值
        myBlock();                          //使用block,注意此时输出的是 m = 10
        NSLog(@"after the block:");
        NSLog(@"m = %d, m addr: %p", m, &m);//使用block后,m的值和地址
//输出结果:
2015-11-14 16:02:25.269 BlockTest[1564:109816] before the block:
2015-11-14 16:02:25.270 BlockTest[1564:109816] m = 10, m addr: 0x7fff5fbff83c
2015-11-14 16:02:25.270 BlockTest[1564:109816] in the block:
2015-11-14 16:02:25.270 BlockTest[1564:109816] m = 10, m addr: 0x100300020
2015-11-14 16:02:25.271 BlockTest[1564:109816] after the block:
2015-11-14 16:02:25.271 BlockTest[1564:109816] m = 50, m addr: 0x7fff5fbff83c

由地址可知局部变量m存储在栈区。而在block内的m变量却存储在堆区,所以后者已经是一个新的内存空间变量。
在上例中使用block前后,m的值和地址不变。
如果在block内部有与block外部同名的变量,则前者会暂时屏蔽外部变量。
但要注意,在block内部是默认不可修改block外部的变量,因为是在block内部把外部变量以const的方式复制到堆区,这些复制的变量是只读的:

2)如果想要修改外部变量,则用__block关键字修饰外部变量,这样block就已引用(reference)的形式来获取外部变量,这样block内外的变量就有了直接的联系,这个变量在block内的值不再是在block定义的“快照”,而是会因变量在block外部的变化而变化,反之亦然。

        __block int m = 10;                 //用__block修饰外部变量 m
        int n = 20; 
        NSLog(@"before the block:");
        NSLog(@"m = %d, m addr: %p", m, &m);
        void (^myBlock)() = ^
            NSLog(@"in the block:");
            n = 100;     //error: ……Variable is not assignable (missing __block type specifier)
            NSLog(@"m = %d, m addr: %p", m, &m);
            m = 50;                         //在block内修改外部变量m            
            NSLog(@"m = %d, m addr: %p", m, &m);
        ;
        m = 100;                            //在block定义后、使用前修改m的值
        myBlock();                          //使用block
        NSLog(@"after the block:");
        NSLog(@"m = %d, m addr: %p", m, &m);
//输出结果:
2015-11-14 16:16:58.596 BlockTest[1592:113267] before the block:
2015-11-14 16:16:58.597 BlockTest[1592:113267] m = 10, m addr: 0x7fff5fbff838
2015-11-14 16:16:58.597 BlockTest[1592:113267] in the block:
2015-11-14 16:16:58.597 BlockTest[1592:113267] m = 100, m addr: 0x1002072b8
2015-11-14 16:16:58.597 BlockTest[1592:113267] m = 50, m addr: 0x1002072b8
2015-11-14 16:16:58.597 BlockTest[1592:113267] after the block:
2015-11-14 16:16:58.597 BlockTest[1592:113267] m = 50, m addr: 0x1002072b8

要注意的是:一是在block中两次输出的 m 分别为 100,50,二是外部变量m在block内部经过修改之后,在block范围之外使用的m也只能是在block内复制到堆区中的m,原来在栈区的m找不到了。

而且被__block修饰的变量,可以像静态局部变量那样,在多次调用block中保存其值。如下:

        __block int i = 1;
        int (^count)(void) = ^
            return ++i;
        ;
        NSLog(@"i = %d", count());  //输出:i = 1
        NSLog(@"i = %d", count());  //输出:i = 2
        NSLog(@"i = %d", count());  //输出:i = 3

3)__block只能修饰局部变量,对于静态变量和全局变量,在block内都是引用变量地址的,也就是可以修改变量的值:

int a = 1;                                      //定义全局变量
static int b = 2;                               //定义静态变量
int main(int argc, const char * argv[]) 
    @autoreleasepool 
        NSLog(@"before the block:");
        NSLog(@"a = %d, a addr: %p", a, &a);
        NSLog(@"b = %d, b addr: %p", b, &b);
        void (^myBlock)() = ^        
            NSLog(@"in the block:");    
            NSLog(@"a = %d, a addr: %p", a, &a);//全局变量的地址在block内外一样
            a = 2;                              //可直接修改全局变量a          
            NSLog(@"b = %d, b addr: %p", b, &b);//静态变量的地址在block内外一样
            b = 3;                              //可直接修改静态变量b
        ;
        myBlock();                              //使用block
        NSLog(@"after the block:");
        NSLog(@"a = %d, a addr: %p", a, &a);    
        NSLog(@"b = %d, b addr: %p", b, &b);
    
    return 0;

输出结果:
2015-11-14 18:40:39.417 BlockTest[1740:130478] before the block:
2015-11-14 18:40:39.418 BlockTest[1740:130478] a = 1, a addr: 0x1000011c8
2015-11-14 18:40:39.418 BlockTest[1740:130478] b = 2, b addr: 0x1000011cc
2015-11-14 18:40:39.418 BlockTest[1740:130478] in the block:
2015-11-14 18:40:39.418 BlockTest[1740:130478] a = 1, a addr: 0x1000011c8
2015-11-14 18:40:39.418 BlockTest[1740:130478] b = 2, b addr: 0x1000011cc
2015-11-14 18:40:39.419 BlockTest[1740:130478] after the block:
2015-11-14 18:40:39.419 BlockTest[1740:130478] a = 2, a addr: 0x1000011c8
2015-11-14 18:40:39.419 BlockTest[1740:130478] b = 3, b addr: 0x1000011cc

4)在block内部定义的变量和其他局部变量一样,是存储在栈区:

        void (^myBlock)() = ^
            int n = 20;                     //在block内部定义的局部变量
            NSLog(@"n = %d, n addr: %p", n, &n);
        ;
        myBlock();                          //使用block
//输出结果:
2015-11-14 16:25:34.502 BlockTest[1624:115823] n = 20, n addr: 0x7fff5fbff7ac

4、Block与函数

当函数的参数不好设计或者返回值比较复杂时,block显得比较有用。
(1)Block作函数的返回值
使用:

typedef int (*BLOCK) (int, int);       //1、用typedef定义一个新的类型
BLOCK sum2()                          //2、定义一个返回值是block类型的函数
    BLOCK getSum = ^(int x, int y)    //3、在函数中定义一个BLOCK类型的block变量,创建,并作返回值
        return x + y;
    ;
    return getSum;

int main(int argc, const char * argv[]) 
    @autoreleasepool 
        BLOCK getSumInMain = sum2();             //4、定义一个BLOCK型的变量,用于接收函数的返回值
        NSLog(@"sum = %d", getSumInMain(10, 20));//5、使用block
    
    return 0;

注意:
BLOCK 类型的变量 getSumInMain 接收了函数 sum2() 的返回值,相当于以下定义了getSumInMain:

int(^getSumInMain)(int, int) = ^(int x, int y)
    return x + y;
;

(2)Block作函数或者方法的参数
使用:

//如输出星期一到星期三
typedef void (^WEEKDAY)();      //1、用typedef定义一个新的类型WEEKDAY
void week(WEEKDAY weeday)      //5、用新定义的block类型 WEEKDAY型变量作函数参数
    NSLog(@"today is :");
    weeday();                   //6、使用形参block变量weeday,此处无参数

void weekdays (int n)
    WEEKDAY wd;                 //2、用新定义的block类型 WEEKDAY型定义一变量,在最后作函数实参
    switch (n) 
        case 1:
            wd = ^
                NSLog(@"Monday");//3、判断参数,并创建WEEKDAY型变量wd
            ;
            break;
        case 2:
            wd = ^
                NSLog(@"Tuesday");
            ;
            break;
        case 3:
            wd = ^
                NSLog(@"Wednesday");
            ;
            break;
        default:
            break;
    
    week(wd);                   //4、WEEKDAY型变量wd作函数实参传递,并调用函数week

int main(int argc, const char * argv[]) 
    @autoreleasepool 
        for (int n = 1; n <= 3; n++) 
            weekdays(n);            //调用函数,用n控制星期几
        
    
    return 0;

在Foundation框架中有很多方法用到了block,用block遍历集合中的元素的速度要比C中的for语句、Objective-C中for增强语句的遍历速度都要快,而且block可以在内部(inline)定义,这是函数所没有的。如下面,要在集合aSet中寻找是否有与字符串string相同的元素:

        __block BOOL found = NO;                                    //用__block修饰found
        NSSet *aSet = [NSSet setWithObjects:@"A", @"B", @"C", nil]; //定义一集合,有三元素
        NSString *string = @"A";                                    //要寻找的字符串
        [aSet enumerateObjectsUsingBlock:^(id obj, BOOL *stop)     //
            //用block来遍历集合aSet,将string与aSet里每个元素对比看是否相等
            if ([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame) 
                *stop = YES;                                        //相等则停止
                found = YES;                                        //标志找到了,在block内修改外部变量found
            
        ];
        NSLog(@"find the string? %@", found?@"yes":@"no");          //输出结果
//输出结果:find the string? yes

(5)Block的结构及实现原理

(1)将以下代码用clang改写:

//block.c
#include <stdio.h>
int main()

    ^ printf("Hello, World!\\n");  ();
    return 0;

得到block.cpp,其中有代码:

//block.cpp
struct __block_impl 
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
;
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;
  
;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
 printf("Hello, World!\\n"); 

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);
int main()

    (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
    return 0;

将以上代码画成图及注释如下:

(2)若将block.c代码修改为block2.c:

//block2.c
#include <stdio.h>
int main() 
    int a = 100;
    void (^block2)(void) = ^
        printf("%d\\n", a);
    ;
    block2();

    return 0;

转换之后得到 关键代码:

//block2.cpp
struct __main_block_impl_0 
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) 
    impl.isa = &_NSConcreteStackBlock;      //说明这是一个分配在栈上的实例
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  
;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
  int a = __cself->a; // bound by copy

        printf("%d\\n", a);
    

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);
int main() 
    int a = 100;
    void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
    ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);

    return 0;

画图并注释得:

因原代码要访问外部变量a,此时可以看到__main_block_impl_0结构体中增加了一个变量a,并在声明block的时候将外部变量a的值复制到结构体中的a。

(3)若要在block中修改变量,加__block修饰符,代码如下:

//block3.c
#include <stdio.h>
int main()

    __block int i = 1024;       //加__block修饰符
    void (^block1)(void) = ^
        printf("%d\\n", i);
        i = 1023;               //修改外部变量
    ;
    block1();
    return 0;

用clang转换以上代码后,关键代码如下:

struct __Block_byref_i_0 
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
;

struct __main_block_impl_0 
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref 注意该变量不是基本数据类型,而是一个结构体
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) 
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  
;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
  __Block_byref_i_0 *i = __cself->i; // bound by ref

        printf("%d\\n", (i->__forwarding->i));
        (i->__forwarding->i) = 1023; //修改外部变量
    
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) _Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);

static void __main_block_dispose_0(struct __main_block_impl_0*src) _Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);

static struct __main_block_desc_0 
  size_t reserved;
  size_t Block_size;
    //附加信息也增加了以下两项用于管理内存
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
 __main_block_desc_0_DATA =  0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0;
int main()

    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = (void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024;
    void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);
    ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
    return 0;

画图并注释:

注意这里的block不简单地用基本数据类型存储要修改的外部变量,而用了 __Block_byref_i_0 结构体,这个结构体的第一个元素也是isa,说明也是一个对象。而且我们还要用copy和dispose函数指针来调用函数来处理变量的引用计数。

以上是关于Objective-C中的Block的主要内容,如果未能解决你的问题,请参考以下文章

Objective-C中的Block

Objective-C语法之代码块(block)的使用 (转载)

浅谈Objective-C中的block那些事

Objective-C基础笔记Block

Objective-C Block与函数指针比较

Objective-C 基础之— Block本质+源码剖析