[面试时]我是如何讲清楚Block的
阅读原文时间:2021年04月20日阅读:1

1、概述

blocks是OS X Snow Leopard和iOS4引入的C语言扩充语法,其优点在于代码简洁而且集中,而且还可以写匿名函数!

2、Blocks 模式

2.1、语法

2.1.1完整版本

声明

返回类型 (^块名称)(参数列表)

 int (^addBlock)(int a, int b);
定义

^ ( 返回值类型 ) (参数列表) (表达式)

^int (int count) {return count + 1;}

2.1.2返回值类型

^ (参数列表) (表达式)

^ (int count) {return count + 1;}

2.1.3返回值和参数列表

^ (表达式)

^ {printf("Block\n");}

2.2block类型变量

block也可以定义称为block类型变量,一般可以有以下用途:

  • 自动变量

  • 函数变量

  • 静态变量

  • 静态全局变量

  • 全局变量

    //定义block变量
    int (^blk) (int);
    //给block变量赋值
    int (^blk) (int) = ^ (int count) {return count + 1;};

    int (^blk1)(int) = blk;

    int (^blk2)(int);
    blk2 = blk1;
    //block变量当做参数传递
    void func(int (^blk) (int)){/处理/}
    //block变量当做返回值
    int (^func()(int))
    {
    return ^(int count) {return count + 1;};
    }
    //typedef定义能够让代码更加清晰易懂
    typedef int (^blk_t)(int);

    void func(blk_t blk){/处理/};

    blk_t func(){/处理/}

2.3、截获自动变量值

block在语法定义的时候,block表达式能够截获所使用的自动变量的值,而且在block语法后改变自动变量的值,block中的自动变量的值也不会受到影响。

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{printf(fmt, val);};

    val = 2; 
    fmt = "There values were changed, val = %d\n";

    blk();

    return 0;
}

//输出
val = 10;

2.4、__block说明符

在block中,你不能够修改截获的自动变量的值,否则回产生编译错误,如果真的想修改自动变量的值,那么需要用__block来修饰自动变量的值。

//错误使用
int val = 0;

void (^blk) (void) = ^{val = 1;};

blk();//编译出错

//正确使用
__block int val  = 0;

void (^blk) (void) = ^{val = 1;};

blk();

如果截获objective-c对象,操作对象时合法的,但是对对象赋值是非法的。

id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    id obj = [[NSObject alloc] init];
    [array addObject: obj];

    //array = [[NSMutableArray alloc] init]; //编译出错
}

使用C语言的字符串字面量数组,虽然没有像截获的自动变量赋值,但是也会出现编译错误

//错误的使用方法
const char text[] = "hello";
void (^blk)(void) = ^{
    printf("%c\n", text[2]);//编译出错
}

//正确的使用方法
const char *text = "hello";
void (^blk)(void) = ^{
    printf("%c\n", text[2]);//编译出错
}

3、block的实现

3.1 block的实质

3.1.1 block的存储域

其实block和__block变量的实质都是对象,如下:

名称

实质

block

栈上block的结构体实例

__block变量

栈上__block变量的结构体实例

block有三种类型的存储域,分别是:

  • _NSConcreteStackBlock

  • _NSConcreteGlobalBlock

  • _NSConcreteMallocBlock

    //栈Block,定义在函数之内,系统自动回收
    -(void)block{
    void (^stackBlock) () = ^{
    NSLog(@"this is a block");
    };
    }

    //堆Block,定义在函数之内,引用计数加1,非ARC环境由开发者释放
    -(void)block{
    void (^stackBlock) () = [^{
    NSLog(@"this is a block");
    } copy];
    }

    //全局Block,定义在函数之外,相当于一个单例
    void (^globalBlock) () = ^{
    NSLog(@"this is a block");
    };

对应的存储区域分别如下:

block在语法记述时,将block保存在栈上,当block语法记述的变量作用域结束,栈上的block也会因没有持有者而被释放掉,如下:

那么问题来了:

  • 为什么Block超出变量作用域可存在?
  • 为什么__block变量用结构体成员变量__forwarding?(这个看不懂不要紧)

原来block会在以下情况调用copy函数将栈上的block复制到堆上,使得即使超出了作用域,栈上的block被释放了,但是仍然可以访问堆上的block

  • 调用block的copy实例方法时
  • block作为函数返回值返回时
  • 将block赋值给附有__strong修饰符id类型的类或block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中传递Block时

那么对属于不同存储域的block进行copy,会产生什么效果呢?总结如下:

Block的类

副本源的配置存储域

复制效果

_NSConcreteStackBlock

从复制到堆

_NSConcreteGlobalBlock

程序的数据区域

什么也不做

_NSConcreteMallocBlock

引用计数增加

可见,不管block配置在何处,对其使用copy方法复制都不会引起任何问题,因此为了避免栈上的block被释放导致访问出错,应该使用copy方法将block复制到堆上,在不确定的时候调用copy方法即可,不过这个过程相当消耗CPU时间,因此谨慎对待。

除此之外,block中如果引用了objective-c对象变量,那么记录在block对象类(编译源码会有block类)中的对应对象会用__strong来修饰,也就是说block持有该对象。这也就是为什么会出现下面代码输出的原因:

blk_t blk;
{
    id array = [[NSMutableArray alloc] init];
    blk = [^(id obj){
        [array addObject: obj];

        NSLog(@"array count = %ld", [array count]);
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

//输出
array count = 1
array count = 2
array count = 3

但是假如block中如果引用了objective-c对象变量,但是不持有该对象会出现什么情况?我们用__weak来修饰block中对象变量,代码如下。__strong 修饰的变量array超出作用域后被释放、废弃,此时__weak修饰的变量array2被置nil。因此输出结果都是0。

blk_t blk;
{
    id array = [[NSMutableArray alloc] init];
    id __weak array2 = array;
    blk = [^(id obj){
        [array2 addObject: obj];

        NSLog(@"array2 count = %ld", [array2 count]);
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

//输出
array2 count = 0
array2 count = 0
array2 count = 0

3.1.2、__block变量存储域

当block从栈复制到堆时,使用的所有__block变量也必定配置栈上,而这些变量也全部被复制到堆上,并且被堆上的block持有。

__block 变量的配置存储域

Block从栈复制到堆时的影响

从栈复制到堆并被Block持有

被Block持有

__block变量的复制情况如下图:

3.1.3、block和__block变量的释放

当超出block语法作用域,栈上的block和__block变量因不被持有而被释放,当超出block变量的作用域,堆上的block和__block变量因不被持有而被释放,当释放时,会调用block的dispose函数,具体如下图:


3.1.4、__forwarding

__block变量在没有复制到堆上时,__block变量结构体中的__forwarding指针指向自己,当__block变量复制到堆上后,栈上的__block变量结构体中的__forwarding指针指向堆上__block变量结构体中的__forwarding指针,堆上的__block变量结构体中的__forwarding指针指向它自己,总的来说,使用__forwarding指针是为了无论在block语法中、block语法外使用__block变量,还是__block变量配置在栈上火堆上,都可以顺利的访问同一个__block变量,如下图:

3.1.5 block循环引用

如果在block中使用附有__strong修饰符的对象类型自动变量,那么当block从栈复制到堆时,该对象为block所持有。酱紫容易引起循环引用。

typedef void (^blk_t) (void);
@interface MyObject: NSObject
{
    blk_t blk_;
}
@implementation MyObject
-(id)init
{
    self = [super init];
    blk_ = ^{NSLog(@"self = %@", self);};
    return self;
}

-(void)dealloc
{
    NSLog(@"dealloc");
}
@end

int main()
{
    id o = [[MyObject alloc] init];
    NSLog(@"%@", o);
    return 0;
}

如上代码所示,MyObject类对象对Block类型成员变量blk_持有强引用,同时init实例方法中,Block类型成员blk_对id类型变量self持有强引用,并且由于Block语法赋值在成员变量blk_中,因此通过Block语法生成的栈上的block由栈复制到了堆上,此时堆上的self持有block,block持有self,导致循环引用,如下图。

那么要如何解决这种情况?两种方法
方法一:用__weak(iOS4,Snow Leopard的应用程序中需要使用__unsafe_unretained修饰符来代替)修饰符修饰变量,且都不必担心产生悬挂指针问题。

-(id)init
{
    self = [super init];
    id __weak tmp = self;

    //id __unsafe_unretained tmp = self;

    blk_ = ^{NSLog(@"self = %@", tmp);};
    return self;
}

方法二:也可以使用__block变量来避免循环引用,但是一定要调用execBlock实例方法,不然会引起循环应用导致内存泄露。

typedef void (^blk_t) (void);
@interface MyObject: NSObject
{
    blk_t blk_;
}
@implementation MyObject
-(id)init
{
    self = [super init];
    __block id tmp = self;
    blk_ = ^{
        NSLog(@"self = %@", tmp);
        tmp = nil;
    };

    return self;
}

-(void)execBlock
{
    blk_();
}

-(void)dealloc
{
    NSLog(@"dealloc");
}
@end

int main()
{
    id o = [[MyObject alloc] init];
    [o execBlock];
    return 0;
}

以上代码引起循环泄露的原因,如下图:

  • MyObject类对象持有_block
  • Block持有__block变量
  • __block变量持有MyObject类对象

执行了execBlock会使得nil被赋值在__block变量tmp中,使得__block变量tmp对Myobject类对象的强引用失效,从而避免了循环引用,如下图:

  • MyObject类对象持有_block
  • Block持有__block变量

总的来说,使用__block变量避免循环引用的方法有如下优点:

  • 通过__block变量可控制对象的持有期间

  • 在不能使用__weak修饰符的环境中不使用__unsafe_unretained修饰符即可(不用担心悬挂指针问题)

  • 在执行Block时可动态的决定是否将nil或其他对象赋值在__block变量中

    不足之处在于为了避免循环引用必须执行Block