iOS

block知多少

像函数的block是什么东西

Posted by Renchao on August 2, 2017

block知多少

block对象就是一个结构体,里面有isa指针指向自己的类(global malloc stack),有desc结构体描述block的信息,__forwarding指向自己或堆上自己的地址,如果block对象截获变量,这些变量也会出现在block结构体中。最重要的block结构体有一个函数指针,指向block代码块。block结构体的构造函数的参数,包括函数指针,描述block的结构体,自动截获的变量(全局变量不用截获),引用到的__block变量。(__block对象也会转变成结构体)

block代码块在编译的时候会生成一个函数,函数第一个参数是前面说到的block对象结构体指针。执行block,相当于执行block里面__forwarding里面的函数指针。

一点一点解释:

定义

block作为C语言的扩展,并不是高新技术,和其他语言的闭包是一回事。iOS4.0系统已开始支持block,在编程过程中,block被Obj-C看成是对象,它封装了一段代码,这段代码可以在任何时候执行。block可以作为函数参数或者函数的返回值,而其本身又可以带输入参数或返回值。

Block的使用很像函数指针,(1)可以保存代码(2)有返回值(3)有形参(4)调用方式一样。不过与函数最大的不同是:block可以访问函数以外、词法作用域以内的外部变量的值。换句话说,block不仅实现函数的功能,还能携带函数的执行环境

// 声明和实现写在一起,就像变量的声明实现 int a = 10;
int (^aBlock)(int, int) = ^(int num1, int num2) {
    return num1 * num2;
};

// 声明和实现分开,就像变量先声明后实现 int a; a = 10;
int (^cBlock)(int,int);

cBlock = ^(int num1,int num2){
    return num1 * num2;
};

//之后我们就可以像使用函数那样使用block了

block重命名

我们可以使用typedef为block重命名

typedef int (^Sum) (int,  int);

这样我们就利用typedef定义了一个block,这个block的名字就是Sum,需要传入两个int类型参数:

Sum mySum = ^(int a, int b) {
  	n = 2;
	return (a + b)*n;
};

这样就完整的定义好了一个block了,接下来的使用如下:

#import <Foundation/Foundation.h> 
typedef int (^Sum) (int, int); 
int main(int argc, const char * argv[]) { 
	__block int n = 1; 
	@autoreleasepool { 
	Sum mySum = ^(int a, int b) { 
		n = 10; 
		return (a + b)*n; 
		};
        NSLog(@"(1 + 2) * %i = %d", n, mySum(1, 2)); 
    } 
    return 0; 
}

block类型

内存区域划分:

(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈

(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式类似于链表

全局区(静态区static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放

常量区:常量字符串放在这里, 程序结束后由系统释放

程序代码区:存放函数体的二进制代码

根据isa指针,block一共有3种类型的block:

  • NSConcreteGlobalBlock 全局静态,类似函数,位于text段(程序代码段),不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的block
  • NSConcreteStackBlock 保存在栈中,出函数作用域就销毁
  • NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁,该类型的block都是由_NSConcreteStackBlock类型的block从栈中复制到堆中形成的

而ARC和MRC中,还略有不同:

  1. 不管在ARC还是MRC环境下,block内部如果没有访问外部变量,这个block是全局blockNSGlobalBlock,形式类似函数,存储在内存中的代码区。
  2. 在MRC下,block内部如果访问外部变量,这个block是栈blockNSStackBlock,存储在内存中的栈上。block内部访问外部变量,同时对该block做一次copy操作,这个block是堆blockNSMallocBlock,存储在内存中的堆上。
  3. 在ARC下,block内部如果访问外部变量,这个block是堆blockNSMallocBlock,存储在内存中的堆上,因为在ARC下,默认对block做了一次copy操作。
Block对不同类型的变量的存取
基本类型
  • 局部自动变量,在Block中只读。Block定义时copy变量的值,在Block中作为常量使用,所以即使变量的值在Block外改变,也不影响他在Block中的值。

    int base = 100;
    BlkSum sum = ^ long (int a, int b) {
      // base++; 编译错误,只读
      return base + a + b;
    };
    base = 0;
    printf("%ld\n",sum(1,2)); // 这里输出是103,而不是3
    
  • static变量、全局变量。如果把上个例子的base改成全局的、或static。Block就可以对他进行读写了。因为全局变量或静态变量在内存中的地址是固定的,Block在读取该变量值的时候是直接从其所在内存读出,获取到的是最新值,而不是在定义时copy的常量。

    static int base = 100;
    BlkSum sum = ^ long (int a, int b) {
      base++;
      return base + a + b;
    };
    base = 0;
    printf("%d\n", base);
    printf("%ld\n",sum(1,2)); // 这里输出是3,而不是103
    printf("%d\n", base);
    

    输出结果是0 4 1,表明Block外部对base的更新会影响Block中的base的取值,同样Block对base的更新也会影响Block外部的base值。

  • Block变量,被__block修饰的变量称作Block变量。基本类型的Block变量等效于全局变量、或静态变量。

    Block被另一个Block使用时,另一个Block被copy到堆上时,被使用的Block也会被copy。但作为参数的Block是不会发生copy的。

用法

作为方法的参数

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
	[self blockAsPara:^{
		NSLog("block作为参数");
	}];
}
- (void)blockAsPara:(void(^)())block{
	block(); //执行block内部的代码
}

作为属性

@property (nonatomic,copy) void (^myBlock)();//block作为属性
- (void)viewDidLoad{
	[super viewDidLoad];
	[self setMyBlock:^{
		NSLog("block作为属性");
	}];
	NSLog("%@",self.myBlock);
	self.myBlock();//myBlock作为属性时的调用
}

链式编程思想

-(CaculatorViewController *(^)(int))add{
    return ^(int value){
        _result += value;
        return self;
    };
}

本质

Block的本质是可以截取自动变量的匿名函数 匿名函数:顾名思义就是不带名字的函数,在C语言中不允许这样的方法存在,至少要知道函数的名字来对函数进行引用,而在OC中的Block则可以用指针来直接调用一个函数,但虽说如此我们还是需要知道指针的名称。 截取自动变量

int b = 0;
void (^blo)() = ^{
	NSLog(@"Input:b=%d",b);
};
b = 3;
blo();
/* Output:b=0 */

虽然我们在调用blo之前改变了b的值,但是输出的还是Block编译时候b的值,所以截获瞬间自动变量就是:在Block中会保存变量的值,而不会随变量的值的改变而改变。

block注意事项

block访问外部变量

  1. block内部可以访问外部的变量,block默认是将其复制到其数据结构中来实现访问的

    block-capture-1.jpg

  2. 默认情况下,block内部不能修改外面的局部变量,因为通过block进行闭包的变量是const的

  3. 给局部变量加上__block关键字,这个局部变量就可以在block内部修改,block是复制其引用地址来实现访问的

    block-capture-2.jpg

block作为属性应该用copy修饰

  1. 如果用weak、assign修饰block属性时,block访问外部变量,此时block的类型是栈block。保存在栈中的block,当block所在函数\方法返回\结束,该block就会被销毁。在其他方法内部调用访问该block,就会引发野指针错误EXC_BAD_ACCESS
  2. 当用copy、strong修饰block属性时,block访问外部变量,此时block的类型是堆block。保存在堆中的block,当引用计数器为0时被销毁,该类型block是由栈block从栈中复制到堆上,因此可以在其他方法内部调用该block。在ARC下,strong和copy都可以用来修饰block,但是建议修饰block属性使用copy

block指定为copy后是否会拷贝一份呢?(或者说是浅拷贝还是深拷贝)

  1. copy可变变量:在赋值指针的同时也会复制指针指向的内存区域。深拷贝,例如NSMutableString对象
  2. copy不可变变量:等同于strong,还是浅拷贝,例如NSString对象

因为block是一段代码,即不可变的,所以并不会深拷贝。

循环引用

对于非ARC下, 为了防止循环引用, 我们使用__block来修饰在Block中使用的对象。

对于ARC下, 为了防止循环引用, 我们使用__weak来修饰在Block中使用的对象。原理就是:ARC中,Block中如果引用了__strong修饰符的自动变量,则相当于Block对该变量的引用计数+1。

@interface BlockViewController ()
@property (nonatomic, strong) void (^block)(void);
@property (nonatomic, copy) NSString *str;
@end
@implementation BlockViewController
- (void)viewDidLoad {
	[super viewDidLoad];
	self.block = ^{
		self.str = @"123";
	};
	}
@end

上面的代码,self.block强引用block,而block中又使用了self.str,所以block强引用self,造成强引用,解决方法:weakSelf和strongSelf

__weak __typeof(self)weakSelf = self;
[self.context performBlock:^{
	__strong __typeof(weakSelf)strongSelf = weakSelf;
	[strongSelf doSomething];
  	[strongSelf doMoreThing];
}];

那block中的StrongSelf又是做什么的呢?还是上面的例子,当你加了weakSelf后,block中的self随时都会有被释放的可能,所以会出现一种情况,在调用doSomething的时候self还存在,在doMoreThing的时候self就变成nil了,所以为了避免这种情况发生,我们会重新strongify self。

block的实现

block的数据结构定义

block-struct.jpg

从上图可以看出,Block_layout就是对block结构体的定义:

isa指针:指向表明该block类型的类。

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

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

invoke:函数指针,指向具体的block实现的函数调用地址。

descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。

variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。

block编译过后

通过clang编译器,可以看到block和其他Objective-c对象一样,都是被编译为C语言里的普通的struct结构体来实现的。我们来看一个最简单的block会被编译成什么样:

//这个是源代码
int main(){
    void (^blk)(void) = ^{
      printf("Block\n");
    };
    block();
    return 0;
}

编译后的代码如下:

struct __block_impl {      //结构体,block的基类
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {      //block变量
    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;
	}
};

struct void __main_block_func_0(struct __main_block_impl_0 *__cself){         //匿名函数
    printf("Block\n");
}

static struct __main_block_desc_0{        //block的描述
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

代码非常长,但是并不复杂,一共是四个结构体,显然一个block对象被编译为了一个__main_block_impl_0类型的结构体。这个结构体由两个成员结构体和一个构造函数组成。两个结构体分别是__block_impl__main_block_desc_0类型的。其中__block_impl结构体中有一个函数指针,指针将指向__main_block_func_0类型的结构体。总结了一副关系图:

这里写图片描述

block在定义的时候:

//调用__main_block_impl_0结构体的构造函数
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;

block在调用的时候:

*blk->impl.FuncPtr)(blk);

block有自己的数据和算法。显然算法(代码)是放在__main_block_func_0结构体里的。那么数据在哪里呢,这个问题比较复杂,我们来看一看下面的代码会编译成什么样,为了简化代码,这里只贴出需要修改的部分。

typedef void (^Block)(void);
Block block;
{
    int val = 0;
    block = ^(){
        NSLog(@"val = %d",val);
    };
}
block();
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    int val;

    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int _val, int flags=0) : val(_val){          
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}
};

struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
    int val = __cself->val;
    printf("val = %d",val);
}

可以看到,当block需要截获自动变量的时候,首先会在__main_block_mpl_0结构体中增加一个成员变量并且在结构体的构造函数中对变量赋值。以上这些对应着block对象的定义。

在block被执行的时候,把__main_block_impl_0结构体,也就是block对象作为参数传入__main_block_func_0结构体中,取出其中的val的值,进行接下来的操作。

img

为什么__block中不能修改变量值?

通过把block拆成这四个结构体,系统“完美”的实现了一个block,使得它可以截获自动变量,也可以像一个微型程序一样,在任意时刻都可以被调用。但是,block还存在这一个致命的不足:

注意到之前的__main_block_func_0结构体,里面有printf方法,用到了变量val,但是这个block,和最初block截获的block,除了数值一样,再也没有一样的地方了。参见这句代码:

int val = __cself->val;

当然这并没有什么影响,甚至还有好处,因为int val变量定义在栈上,在block调用时其实已经被销毁,但是我们还可以正常访问这个变量。但是试想一下,如果我希望在block中修改变量的值,那么受到影响的是int val而非__cself->val,事实上即使是 __cself->val,也只是截获的自动变量的副本,要想修改在block定义之外的自动变量,是不可能的事情。

既然无法实现修改截获的自动变量,那么编译器干脆就禁止程序员这么做了。

__block修饰符是如何做到修改变量值的?

如果把val变量加上__block修饰符,编译器会怎么做呢?

//int val = 0; 原代码
__block int val = 0;//修改后的代码

编译后的代码:

struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *forwarding;
    int __flags;
    int __size;
    int val;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    __Block_byref_val_0 *val;

    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,__Block_byref_val_0 *_val, int flags=0) : val(_val->__forwrding){
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
	}
};

struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
    __Block_byref_val_0 *val = __cself->val;
    printf("val = %d",val->__forwarding->val);
}

改动并不大,简单来说,只是把val封装在了一个结构体中而已。可以用下面这个图来表示五个结构体之间的关系:

这里写图片描述

但是关键在于__main_block_impl_0结构体中的这一行:

__Block_byref_val_0 *val;

由于__main_block_impl_0结构体中现在保存了一个指针变量,所以任何对这个指针的操作,是可以影响到原来的变量的。

进一步,我们考虑截获的自动变量是Objective-C的对象的情况。在开启ARC的情况下,将会强引用这个对象一次。这也保证了原对象不被销毁,但与此同时,也会导致循环引用问题。

需要注意的是,在未开启ARC的情况下,如果变量附有__block修饰符,将不会被retain,因此反而可以避免循环引用的问题。