学会使用Objective-C中的block

作者: HK_Hank | 来源:发表于2016-09-03 16:36 被阅读4785次

    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 的数据结构定义如下(图片来自 [这里](http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/)):
    ![block内存布局](http:https://img.haomeiwen.com/i809937/6d6f7c759832c3a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    对应的结构体定义如下:
    

    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》](http:https://img.haomeiwen.com/i809937/d400b23575e007d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    对于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内部捕获的对象,如
    ```objective-c
    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的方法。具体细节这里就不讲了,有兴趣的童鞋可以看看我简书上写了另一篇文章

    相关文章

      网友评论

      • 此晨:NSConcreteMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain;

        虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数. retainCount始终是1,这个是确实,什么叫内存管理器?既然retain release copy对NSConcreteMallocBlock的引用计数没有作用,为啥还需要管理NSConcreteMallocBlock的内存,为啥还会产生循环引用
      • 饼哥阿杜:1.【ARC 对 block 类型的影响
        在 ARC下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
        原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。】
        ARC下仍然存在NSConcreteStackBlock类型,比如:__block int temp = 10;
        NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);});
        输出结果<__NSStackBlock__: 0x7fff5fbff768>

        2. 三种Block类型的区分是根据Block体中引用到的变量的类型决定的。可参考http://www.cnblogs.com/jinfengboy/p/5844646.html
        HK_Hank:@饼哥阿杜 研究了一下,这地方说的不准确,我完善一下,多谢提醒。

      本文标题:学会使用Objective-C中的block

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