iOS开发之浅谈Block

目录

  • block概要
  • 自动变量的截获
  • block的调用本质
  • block的内存管理
  • block的循环引用

1.block概要

在刚接触iOS的时候,block真是一个让人头疼的东西,基本上所有的第三方框架都用了block,然后在我们阅读这些大神框架的时候,如果没有学好block,不了解block的用法,那基本上无法把这些第三方框架给吃透。

那么什么是block呢,我从一些书上得到的答案是:带有自动变量的匿名函数,顾名思义,匿名函数就是不带有名称的函数。带有自动变量这个意思我们在后面再具体讲。下面我们看看block的语法:

block语法:

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

例如

^ int ( int  a){
    return a+1;
}
  • 一般在开发过程中,我们可以省略返回值类型
 ^  参数列表 表达式

例如

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

注意: 当我们省略返回值时,如果表达式中有return语句,则返回值类型为该返回值的类型,如果没有return语句,则返回值类型为void类型。如果表达式中有多个return语句,所有的返回值必须是相同类型,不然会报错。 报错信息为:

Return type '类型1 *' must match previous return type '类型2' when block literal has unspecified explicit return type
  • 其次在没有参数的时候,我们还可以省略其参数
    例如
^{
    NSLog(@"省略参数");
}

Block类型变量

声明BLock属性的时候,需要定义一个属性类型:

@property (nonatomic,copy) void (^cpBlock)(void);

 _cpBlock =   ^{
      NSLog(@"111");
    };

 _cpBlock();
  • 当block作为参数的时候:
- (void)add:(void (^)(void))block{ 
    block(); 
}

[self add:^{
        NSLog(@"111");
    }];
  • 当block作为返回值的时候:
- (void (^)(void))sum{
    
    return  ^{
        NSLog(@"111");
    };
    
}

[self sum]();

但是 我们一般在开发中并不会直接这么用,因为这样写也太繁琐了。我们一般使用typedef来解决问题:

typedef void (^Blk)(void);

@property (nonatomic,copy) Blk block;

- (void)add:(Blk)block{
    
    block();
    
}

- (Blk)sum{
    
    return  ^{
        NSLog(@"111");
    };
    
}

2.自动变量的截获

上面我们讲了block是带有自动变量的匿名函数,我们已经解释了匿名函数,那么什么是带有自动变量呢,我们先来看看下面的代码:

typedef int (^Blk)(void);
int age = 10;
Blk block = ^{
    NSLog(@"age = %d", age);
};
age = 20;
block();

相信有点开发经验的同学,应该马上知道结果:

结果是:age = 10

block在使用过程中,会截获表达式中所使用的自动变量的值,即保存该自动变量的瞬间值。因为block在内部保存了这个自动变量的值,所以在执行block语法后,即使在修改block中使用的自动变量的值也不会影响block执行时自动变量的值。如果block表达式中不使用自动变量,则不会截获,因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

截获变量的类型

  1. 局部变量
对于基本数据类型的局部变量截获其值
对于对象类型的局部变量连同所有权修饰符一起截获
  1. 静态局部变量
以指针形式截获局部静态变量
  1. 全局变量
不截获
  1. 静态全局变量
不截获

__block修饰符

默认情况下,block只能访问不能修改局部变量的值,而且在表达式后再修改block语法外声明的自动变量,无法影响block内部的自动变量。那么如果我们想在block语法的表达式将值赋给block语法外声明的自动变量,则需要在该自动变量上附加__block修饰符。

typedef void (^Blk)(void);
__block  int age = 10;
Blk block = ^{
    NSLog(@"age = %d", age);
    age = 5;
   NSLog(@"age = %d", age);
};
age = 20;
block();

结果是:
age = 20
age = 5

那么这是为什么呢,我们可以在命令行输入代码 clang -rewrite-objc 需要编译的OC文件.m,看一下block底层是怎么实现的。
__block修饰的时候

__block  int age = 10;
        Blk block = ^{
           age;
        };
        age = 20;
        block();

转化成cpp后是

Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
struct __main_block_impl_0 {
  struct __block_impl impl;//封装了函数实现的结构体
  struct __main_block_desc_0* Desc;// 里面有内存管理函数,Block_size表示block的大小
  __Block_byref_age_0 *age; // by ref

  // 这个结构体的初始化函数 , 入参 : fp,函数实现的函数指针, __main_block_desc_0,占用大小的描述,age传入的是一个地址
    // 返回一个__main_block_impl_0类型的结构体,赋值给了block
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;//栈block
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

无__block修饰的时候

 int age = 10;
        Blk block = ^{
           age;
        };
        age = 20;
        block();

转化成cpp后是

Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

struct __main_block_impl_0 {
  struct __block_impl impl;//封装了函数实现的结构体
  struct __main_block_desc_0* Desc;// 里面有内存管理函数,Block_size表示block的大小
  int age;// 捕获到的普通局部变量

  // 这个结构体的初始化函数 , 入参 : fp,函数实现的函数指针, __main_block_desc_0,占用大小的描述
    // 返回一个__main_block_impl_0类型的结构体,赋值给了block
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

相比之下,__block修饰后,把变量变成来__Block_byref_age_0结构体对象,而没有__block修饰时,捕获的是变量的瞬间值,所以结果显而易见。

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

这个结构体有一个__forwarding指针,这个指针指向这个结构体,通过这个__forwarding可以访问结构体的成员变量age

下面我们来看下下面的代码:

{
    NSMutableArray *array =[NSMutableArray array];
    void (^Block)(void) = ^{
      [array addObject:@123];  
    };
    Block()
}

这段代码需要给array加__block吗

不需要,因为这个只是使用,并没有对array赋值

注意 静态局部变量、全局变量、静态全局变量不需要添加__block

3.block的调用本质,

block的调用其实就是函数的调用,我们通过源码去看

(1)block定义的时候,把表达式传给__main_block_func_0

 Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

看一下__main_block_impl_0的结构体实现:

struct __main_block_impl_0 {
  struct __block_impl impl;//封装了函数实现的结构体
  struct __main_block_desc_0* Desc;/ 里面有内存管理函数,Block_size表示block的大小
  __Block_byref_age_0 *age; // by ref
//构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
//把__main_block_func_0函数赋值给__block_impl结构体的FuncPtr
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

再看下这个函数实现结构体__block_impl的代码:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

(2)然后我们再看__main_block_func_0的函数定义

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

            age;
        }

(3) 然后再看下block调用的代码

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

先是把block强转成__block_impl类型,然后取出FuncPtr属性,而这个属性就是__main_block_impl_0结构体里面构造函数的fp,也就是(1)里面的__main_block_func_0函数。

所以,block的调用其实就是__main_block_func_0函数的调用,也就是表达式的调用。

4.block的内存管理

block的存储域

block是一个对象,根据block对象的存储域,可以分为以下几种:

1._NSConcreteStackBlock    存储在栈上 
2._NSConcreteGlobalBlock   存储在程序的数据区域(.data区)
3._NSConcreteMallocBlock   存储在堆上

根据结构体__block_impl里面的的isa指针,可以判断block的存储域

Block的Copy操作

1.对于栈上的block,copy后会在堆上产生Block
2.对于已初始化数据取的全局block,copy后什么也不做
3.对于堆上的block,copy后会增加引用计数

那么什么时候block会被copy呢?

1.调用Block的copy实例方法时
2.Block作为函数返回值返回时
3.将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
4.在方法名中含有usingBlock的Cocoa框架方法或GCD的Api中传递Block时

__block变量的存储域

上面我们讲了block的复制,那么对__block变量又是怎么处理的呢?

__block变量的配置存储域 Block从栈复制到堆时的影响
从栈复制到堆并被Block持有
被Block持有

(1)若只有1个block中使用__block变量,则当该Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上,在复制block的时候,也会把__block变量从栈复制到堆。此时block持有__block变量。如果block已经在堆上,再进行copy,那么堆block所使用的__block变量没有任何影响。

(2) 若多个block中使用__block变量时,因为最先将所有的block配置在栈上,所以 __block变量也会配置在栈上。在任何一个block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该block持有。当剩下的block从栈复制到堆时,被复制的block持有__block变量,并增加__block变量的引用计数。

注意 当__block变量从栈复制到堆时,会同时把栈上的__block变量的__forwarding指针指向堆上的__block变量,通过这一操作,无论在Block表达、Block表达式外使用__block变量,不管__block变量配置在栈上还是堆上,都可以顺利地访问同一个__block变量。

总结:__forwarding的意义:不论在任何内存位置,都可以顺利的访问同一个__block变量。

5.block的循环引用

在开发过程中,block的循环引用应该是我们经常碰到也让人头疼的问题。比如我们看下面的代码块:

_array = [NSMutableArray arrayWithObject:@"block"];
_cpBlock = ^NSString *(NSString *str){
          return [NSString stringWithFormat:@"hello_%@",_array[0]];
}; 

_cokong(@"hello");

这个代码块有什么问题的,相信有开发经验的同学应该能立马看出来,这段代码有block循环引用的问题。

因为我们当前block是用copy修饰的,而array是用strong修饰的,而在截获变量中,我们知道,关于block中对象类型的局部变量会连同其属性关键字一起截获,所以在block中有一个strong指针指向self,所以造成了自循环引用。

那么我们怎么去解决呢?
(1)我们可以使用__weak 来解决循环引用,如下代码块

_array = [NSMutableArray arrayWithObject:@"block"];
  __weak NSArray *weakArray = _ayyay;
_cpBlock = ^NSString *(NSString *str){
          return [NSString stringWithFormat:@"hello_%@",weakArray[0]];
}; 

_cokong(@"hello");

这样,因为对象截获的时候会连同其属性关键字一起截获,这边block指向的是一个__weak修饰的self,所以不会造成循环引用。

(2)同理,我们可以使用__unsafe_unretained来解决循环引用。如下代码块

_array = [NSMutableArray arrayWithObject:@"block"];
 _unsafe_unretained NSArray *unsafe_unretainedArray = _ayyay;
_cpBlock = ^NSString *(NSString *str){
          return [NSString stringWithFormat:@"hello_%@",unsafe_unretainedArray[0]];
}; 

_cokong(@"hello");

因为持有cpBlock的self一定存在,所以使用__unsafe_unretainedArray修饰符,不必担心悬垂指针。

(3)我们还可以通过__block来解决循环引用。如下代码块:


 _block id blockSelf = self;
_cpBlock = ^int(NSString *str){
          return blockSelf.a;
}; 

_cokong(@"hello");

其实上面的代码在MRC下一点问题都没有,但是在ARC下存在问题。

用__block修饰self后,self持有cpBlock,cpBlock持有self,这样会造成循环引用。所以我们要在block内部做一个断环的操作,才能解决循环引用。如下代码块:


 _block id blockSelf = self;
_cpBlock = ^int(NSString *str){
           int a = blockSelf.a;
          blockSelf = nil;//断环
          return blockSelf.a;
}; 

_cokong(@"hello");

但是其实,这样做还是有点问题,因为如果我们只是定义了block,而一直不去调用block,那么就永远不能断环,循环引用就一直存在。

总的来说: __block相比较于上面两种方法,优点在于,__block变量可以控制对象的持有时间。缺点在于为了避免循环引用,必须执行block。

你可能感兴趣的:(iOS开发之浅谈Block)