学会使用Objective-C中的block

Apple从OS X 10.4和iOS 4以后开始支持block,相对于delegate,block有很多便捷之处,使得代码更简洁,可读性更强。但是如果使用不当,则会造成很多问题。本文结合自己的经验和《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》书中的知识点,介绍block的相关知识点。

block语法

我们通过以下图来了解block的语法,图片来自这里

block语法结构图

我们来看看上面的图,代码如下

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

根据图中的解释,我们从左向右来看,该block返回值为int类型,'^'符号声明一个名为myblock的block,该block有一个int类型的入参,等号右边则为block的定义,block有一个名为num的int类型的入参,{return num * multiplier;};则为该block的block实现部分。

该block的调用方法如下,看起来像C的函数调用。

int result =  myBlock(2); //reslut = 14;

我们来看看复杂一点的情况:

- (void)startWithBlock:(void(^)())block {
    block();
}

- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

控制台输出

NSStackBlock

以上代码,新手看起来可能是会有些费劲的。我们一步一步来,首先,我们调用testBlock函数,在该函数中,

^{
     NSLog(@"%@",strBlock);
 }];

该代码块实际上是传给了startWithBlock函数的参数block,当执行startWithBlock函数时,调用block(),实际上就是执行了以上代码块。

使用typedef定义block类型
以上代码可以通过typedef来定义block,以便阅读,如下

typedef int (^myBlock)(int num);

在定义某个block类型时,可以使用

myBlock aBlock = ^(int num) {
    //Implemention
};   

这样看起来,要比之前简单得多。

block捕获外部变量

block内可以访问block之前定义的变量:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};
int result =  myBlock(2); //reslut = 14;

但是,如果想在block内部改变multiplier的值,编辑器则会报错

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

编辑器会提示: 变量不能被赋值,需要加上__block修饰符

error: variable is not assignable (missing __block type specifier)

此时,需要将该变量使用__block修饰:

__block int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

如果multiplier变量是static、static global或者global变量,则不需要添加__block,该值也是可以在block内部修改的。

static int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

因为static、static global或者global变量都是存储在内存中的全局区(静态区),对于这三种类型变量,block内部是捕获了其指针,则可以直接访问修改;而对于之前的临时变量,block则只是捕获了该变量的值,无法修改到外部的变量。

block内部还可以访问类的实例变量和self变量

@interface EOCClass : NSObject 
@property (nonatomic, copy) NSString *anInstanceVariable;
@end

@implementation EOCClass

- (void)anInstanceMethod {
    
    void (^someBlock)() = ^ {
        self.anInstanceVariable = @"Something";
    };
    someBlock();
    NSLog(@"self.aninstanceVaraible = %@", self.anInstanceVariable);
    //self.aninstanceVaraible = Something
}

@end

block的内部结构

block 的数据结构定义如下(图片来自 这里):

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 实例实际上由 6 部分构成:

  • isa指针:指向该block类型的类的指针,
    每个Objective-C对象,都有一个isa指针,指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身。如下图

    图片来自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》

    对于block,isa指针可以指向
    _NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock这三种类型

  • flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。

  • reserved:保留变量,我的理解是表示block内部的变量数。

  • invoke:函数指针,指向block的实现代码地址。

  • descriptor:指向结构体的指针,block的附加描述信息,比如保留变量数、block的大小、copy和dispose辅助函数的函数指针指针
    copy函数为当block执行copy操作或者当block从栈上拷贝到堆上时调用,dispose函数则是block在堆上释放时调用

  • variables:block内部捕获的对象,如

void (^blk)(void) = ^{print(fmt,val)};

此时,variables中则为fmt和val这两个变量

block的类型

block有_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock这三种类型。
三种block在内存中存储位置如下图

图片来自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》
block类型的区分

以下情况,block为_NSConcreteGlobalBlock类型

  • block内部只使用了全局变量
  • block内部没有使用任何外部的局部变量

除了以上两种情况,其他的block为_NSConcreteStackBlock类型。
而对于_NSConcreteMallocBlock,只有当_NSConcreteStackBlock类型的block执行copy操作(手动或者系统执行)时,该block才会是_NSConcreteMallocBlock类型

我们来看看代码,直观的看看这三种类型的block

  1. _NSConcreteGlobalBlock
    由类型名字可以得知,该block是存储在在内存中全局区的。
void (^block)() = ^{
    NSLog(@"This is a global block");
};
NSLog(@"%@",block);

控制台输出

<__NSGlobalBlock__: 0x104fd52d0>

或者

int globalVal = 1; //此处为全局变量
int (^myBlock)(int) = ^(int num) {
    return num * globalVal;
};
NSLog(@"%@",block);

控制台输出

<__NSGlobalBlock__: 0x104fd5310>

该block所需要的全部信息都能在编译期确定。该block是全局存在的,相当于单例了。

  1. _NSConcreteStackBlock
    由该类型的名字可以看出,该block所占的内存区域是分配在栈(stack)中的。也就是说,块只在定义它的那个范围内(作用域)内有效。如下面代码:
int multiplier = 7;
NSLog(@"%@",^(int num) {
    return num * multiplier;
};);

控制台输出

<__NSStackBlock__: 0x7fff59615a18>

以上代码,block内部捕获了multiplier这个外部的局部变量,所以是_NSConcreteStackBlock类型。
因为该block存在在栈上,在超过block的作用域时,该block就会被系统释放,就有可能会出现block内部的代码还没有走完,就被释放掉的情况。对于这种情况,应该对block执行copy操作,将block复制到堆上。
注:在ARC下,系统在大部分情况下,会将block从栈上复制到堆上,这个后面会细说

  1. _NSConcreteMallocBlock
    对以上代码中的block执行copy操作,block就变成了_NSConcreteMallocBlock类型,如下
int multiplier = 7;
NSLog(@"mallocBlock:%@",[^(int num) {
    return num * multiplier;
} copy]);

控制台输出

<__NSMallocBlock__: 0x6000000486a0>

拷贝到堆后,block的生命周期就与一般的OC对象一样了。

ARC 下 block 的自动拷贝和手动拷贝

ARC下,以下几种情况,系统会将block从栈上自动复制到堆上

  • 当 block 作为函数返回值返回时;
  • 当 block 被赋值给__strong修饰的 id 类型的对象或 block 对象时;
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时(比如使用NSArray的enumerateObjectsUsingBlock和GCD的dispatch_async方法时,其block不需要我们手动执行copy操作)
    注:系统方法内部对block进行了copy操作

因为在ARC下,对象默认是用__strong修饰的,所以大部分情况下编译器都会将 block从栈自动复制到堆上,除了以下情况

  • block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;
  • block 作为临时变量,没有赋值给其他block
看看代码
block作为函数的返回值,如下
- (void(^)())blockReturn {
    NSString *strBlock = @"NSMallocBlock";
    return ^(){
        NSLog(@"%@",strBlock);
    };
}
NSLog(@"%@",[self blockReturn]);

控制台输出

<__NSMallocBlock__: 0x7fa161f081f0>
block赋值给强引用block
typedef void(^block)();

NSString *strBlock = @"NSMallocBlock";
block mallocBlock = ^(){
    NSLog(@"%@",strBlock);
};
NSLog(@"%@",mallocBlock);

控制台输出

<__NSMallocBlock__: 0x7fedd0d26110>
将block作为临时变量
NSString *strBlock = @"NSStackBlock";
NSLog(@"%@",^(){
    NSLog(@"%@",strBlock);
});    

控制台输出

<__NSStackBlock__: 0x7fff563aa9b0>
block作为函数参数
- (void)startWithBlock:(void(^)())block {
    NSLog(@"%@",block);
}

- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

执行testBlock方法,控制台输出

<__NSStackBlock__: 0x7fff563aa988>

此处可能会有疑问:既然当block作为函数参数时为_NSConcreteStackBlock类型,超出其作用域时,block会被释放掉,那会不会出现函数先退出了,block还是没有执行完毕的?

经过我测试,我发现,其实在函数中,在block执行完毕前,函数是不会退出的。因为函数中按顺序执行的,函数中block后的代码会等待block执行完毕,所以在block块代码未执行完毕时,该函数不会退出,从而没有超过block的作用域,block不会被释放。看下面的例子就可以明白了。

- (void)startWithBlock:(void(^)())block {
    block();
    NSLog(@"%@",block);
}
- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

控制台输出

NSStackBlock
<__NSStackBlock__: 0x7fff54ba4a20>

从打印结果可以看出,当我们执行startWithBlock函数时,先是执行了block内的代码,再是执行函数中block后的代码,所以可以保证block执行完毕。

可能,还有人会问,如果把block()放在子线程中执行呢,这样就不是按顺序执行了,在block块代码执行之前,函数就退出了,这样是不是block就不能执行完毕呢?

其实,把block放子线程中,无非是通过GCD和performSelectorInBackground方法,系统会自动GCD的block进copy操作,而performSelectorInBackground需要传一个selector,又相当于走进了函数里,还是按顺序执行了,函数还是会等待block执行完毕。

针对不同block类型的copy、retain、release操作

  • 对block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;
  • 针对NSConcreteGlobalBlock:retain、copy、release操作都无效;
  • 针对NSConcreteStackBlock:retain、release操作无效
    注意的是,NSConcreteStackBlock离开其作用域后,该block内存将被回收,即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在stackBlock离开其作用域失效后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[stackBlock copy]]。
  • NSConcreteMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain;

注:尽量不要对block使用retain操作。因为从上可以看出,retain操作对)_NSConcreteStackBlock并没有效果,这样会误以为retain生效了,在后续调用block的时候,其实block早就被释放了,从而导致crash

block循环引用问题

可以使用__weak__unsafe_unretained__block修饰词修饰被block持有的对象来打破循环,还有就是在block执行完毕的时候,将block置nil的方法。具体细节这里就不讲了,有兴趣的童鞋可以看看我简书上写了另一篇文章

推荐阅读更多精彩内容

  • 一、Objective-C发展史 Objective-C从1983年诞生,已经走过了30多年的历程。随着时间的推移...
    没事蹦蹦阅读 4,955评论 12 34
  • 《Objective-C高级编程》这本书就讲了三个东西:自动引用计数、block、GCD,偏向于从原理上对这些内容...
    WeiHing阅读 4,408评论 5 50
  • 要了解Block需要先了解什么是“闭包性” 2.3 闭包性 上文说过,block实际是Objc对闭包的实现。 我们...
    随风飘荡的小逗逼阅读 72评论 0 0
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 2,461评论 0 22
  • .相关概念 在这篇笔记开始之前,我们需要对以下概念有所了解。 1.1 操作系统中的栈和堆 注:这里所说的堆和栈与数...
    狼凤皇阅读 81评论 0 0