iOS面试题(二十三)Block--Block的本质&截获变量特性&__block修饰

6.Block

  • Block的本质(什么是Block,你对Block的调用又是怎样理解的)
  • 截获变量特性(系统关于Block的截获变量特性又是怎样实现的呢)
  • __block修饰符的本质(在什么情况下使用)
  • Block的内存管理(说明时候需要对一个Block进行copy操作?栈Block和堆Block你又是否了解呢?)
  • 循环引用(Block在使用不当的时候,经常会产生的循环引用)

Block的本质

int multiplier = 6
int(^ Block)(int) = ^int(int num){
      return num* multiplier
};
Block(2);

从源码来分析一下,通过clang命令行工具中的-rewrite-objc参数[clang -rewrite-objc file.m],我们可以把OC代码转化为C++的实现

什么是Block?



Block是一个对象,封装了函数及其执行上下文
Block是将函数及其执行上下文封装起来的对象

内部有isa指针和FuncPtr函数指针

isa说明他是个对象,FuncPtr指针指向了函数实现

什么是Block调用:

Block调用既是函数的调用。

当我们调用block(2)时,内部实现是

通过block结构体里面的函数指针,取出对应的执行体.将参数传递进来(block本身,2),然后进行内部调用

 

截获变量特性

先看一个问题

// 全局变量
int global_var = 4;
// 静态全局变量
static int static_global_var = 5;

- (void)method
{
     int multiplier = 6;
    int(^Block)(int) = ^int(int num)
    {

        return num * multiplier;
    };
    multiplier = 4;
    NSLog(@"result is %d", Block(2));
}


输出是什么--->"result is 12"? 
 如果 将 int multiplier 改为静态变量 static  int multiplier = 6, 结果又是什么?

带着问题往下看,针对不同类型的对象,block对其截获的特点也是不一样的。

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

从这段代码来深入了解一下

int global_var = 4;
static int static_global_var = 5;

-(void)method1
{
    int var = 1;
    __unsafe_unretained id unsafe_obj = nil;
    __strong id strong_obj = nil;
    static int static_var = 3 ;
    void(^block)(void) = ^{
        
        NSLog(@"局部变量<基本数据类型> var %@",var);
        NSLog(@"局部变量<__unsafe_unretained 对象类型> var %@",unsafe_obj);
        NSLog(@"局部变量< __strong 对象类型> var %@",strong_obj);
        NSLog(@"静态变量 %d",static_var);
        NSLog(@"全局变量 %d",global_var);
        NSLog(@"静态全局变量 %d",global_var);
    }
    
}

使用 clang命令看一下编译后的源码 
看这一段

  • 可以看到,局部变量截获的就是它的值
  • 静态局部变量以指针形式截取的
  • 对象类型的类型连同其修饰符一起截获,理解这个就能更好的理解 Block 循环引用的问题,后续会说
  • 全局和静态全局变量不截获

然后回到问题

int multiplier  = 6 ,block(2)输出的是 12,

因为multiplier是基本数据类型的局部变量,在定义的时候就以值得方式传递到了block结构体当中,而具体的函数调用是使用对应结构体当中的multiplier,而不是我们在方法中定义的multiplier,block执行的时候截获的是 6。所以输出的是 12

static int multiplier  = 6 ,block(2) 输出的是8,
因为multiplier是静态局部变量的指针,截获也是以指针形式截获,所以获取到的  multiplier 是最新的值 4

__block修饰符

看一些笔试题


{
  NSMutableArray *array = [NSMutableArray array];
    void(^block)(void) = ^{
        [array addObject:@123];
    };
    Block();
}
这里对 array 只是一个使用,而不是赋值,所以不需要 __block 进行修饰

 

{
  NSMutableArray *array = nil;
    void(^block)(void) = ^{
            array = [NSMutableArray array];
    };
    Block();

}
这里是赋值,若在block内部调用的话,需要为外部的array声明添加__block修饰符,不然编译器会报错

什么情况下需要用到 __block修饰符呢?
一般情况下,对被截获变量进行赋值操作的时候添加__block (区分 赋值 和使用)
注意:赋值不等于使用

以下变量的赋值操作,需要使用__block修饰的情况

添加__block之后,当外部变量值改了之后,block内部调用时也会更改

  1.  在block内部对局部变量基本数据类型进行赋值操作时
  2. 在block内部对局部变量 对象类型进行赋值操作时

以下变量的赋值操作,不需要__block修饰

  1. 在block内部对静态局部变量进行赋值操作时
  2. 在block内部对全局变量进行赋值操作时
  3. 在block内部对静态全局变量进行赋值操作时
  • 因为全局变量和静态全局变量都不涉及截获操作
  • 静态局部变量是通过指针来使用的,操作的是block外部的变量,所以不需要修饰
{
    __Block int multiplier = 6;
    int(^Block)(int) = ^int(int num)
    {

        return num * multiplier;
    };
    multiplier = 4;
    NSLog(@"result is %d", Block(2));
}
/*
  这里的结果就是 8 了
  加了 __block 修饰之后,这个变量就变成了一个对象
  在 multiplier 进行赋值的时候
  通过multiplier对象里面的__forwarding指针,找到指向原来的对象, 
  通过 __forwarding 指针进行赋值,修改掉 multiplier 的值
*/

理解:

 加了 __block 修饰之后,这个变量就变成了一个对象。


  通过multiplier对象里面的__forwarding指针,找到指向原来的对象, 
  通过 __forwarding 指针进行赋值,修改掉 multiplier 的值。
(栈上的__block变量的__forwarding指针,是指向__block自身。
当block外部的num改变时,__forwarding指针会去block结构体中找到里面的num对象进行赋值,
但要注意这是栈上的block才会这样)

  • 如果对__block变量进行copy操作后,会在堆上面产生完全一样的block变量
  • 栈上的__forwarding指针指向堆上的__block变量,而堆上的__forwarding指针指向自身的__block
  • 所以说,在经过了copy之后,只要对这个值进行了修改,栈和堆上面__forwarding指针改的都是堆上的值


_forwarding指针有什么作用?

_blk:当前对象的成员变量形式的block

  • 不论在任何内存位置,我们都可以通过_forwarding指针顺利的访问同一个__block变量
  • 若没有对__block进行copy,那么操作的是栈上的__block变量
  • 如果copy后,不论是在栈还是堆,我们对__block的修改活赋值,都是对堆上的__block进行的

Block的内存管理

block有哪几类

impl.isa = NSConcteteStackBlock, isa会标记block是哪种类型

  • 全局block = NSConcreteGlobalBlock        存放在内存的已初始化数据区域中
  • 栈block = NSConcreteStackBlock           存放在内存的栈上面
  • 堆上面的block = NSConcreteMallocBlock     存放在内存的堆上面

我们在何时对Block进行copy操作

对于不同类型的Block,copy的效果

  • NSConcreteGlobalBlock(全局block) - copy后什么也不做
  • NSConcreteStackBlock(栈block) - copy后会在堆上产生一个block
  • NSConcreteMallocBlock(堆block) - copy后会增加其引用计数

问题:

P类中有个assign修饰的block,假如在方法A中,我们P.block = ^(int){***}
因为方法A是在栈上,执行完在内存中就销毁了,假如在后面我们又调用了P.block
就会崩溃!!!

  • 比如现在声明一个成员变量Block,而在栈上创建这个Block,同时赋值给成员变量的Block。
    如果没有对成员变量的Block进行Copy操作的话,当我们通过成员变量去访问对应的Block的时候,
    可能会因为栈对应的函数退出之后在内存当中就销毁掉了,继续访问就会引起内存崩溃

 

栈上block的销毁

  • 栈block变量在作用域结束后就销毁了
  • 我们在栈中有个Block,它使用了__block变量(可能是说我们对这个变量进行复制了,所以声明为__block类型)。
    当我们对栈上block进行copy之后,会在堆上产生一模一样的blcok,和__block变量,
    但分占了栈和堆两块内存空间,当作用域结束后,栈上的block会销毁,但堆上的不销毁。
    (当我们对栈上的block进行copy操作之后,假如在MRC环境下,是否会有内存泄漏呢)
  • 在MRC环境下,对栈上block进行copy后,会内存泄露,因为如果堆上的block没有其他变量指向,和alloc一个对象,没有release效果是一样的,会产生内存泄露

Block的循环引用

下面这段代码就会造成循环引用

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

/*
_array和_strBlk作为当前对象的两个成员变量。_array一般用strong, _strBlk一般用copy。
在截获变量中,若对象P强引用block,block内部又截获了strong类型的数组对象,就会循环引用。
ps:截获变量的时候有提到过。
局部变量对象类型(不知道为啥成员变量也截获) - 在block结构体中,连同对象的修饰符一起截获,赋值给block内部使用  
注意:block的循环引用,就是因为局部对象是联通修饰符一起截获的
*/

self 持有 Block,而 Block 里有成员变量 array, 持有 self,所以就造成了循环引用,怎么解决呢?

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

为什么用_ _weak 修饰符解决循环引用?
这个其实在截获变量里有讲过,截获对象的时候会连同修饰符一起截获,
在外部定义的如果是 _ _weak 修饰符,在 Block 里所产生的结构体里面所持有的成员变量也是 _ _weak 类型。

再看一段代码,这样写有什么问题?

__block MCBlock*blockSelf = self;

    _blk = ^int(int num){
          //var = 2
              return num * blockSelf.var ;
    };
        _blk(3);

这样在 ARC 模式下是会产生循环引用,引起内存泄漏的,大环引用。

__block修饰后的指向是原来的对象,会造成循环引用
怎么解决呢,首先想到的当然是断开其中一个环

 

在调用完 blockSelf 后将它置为nil,断开其中的一个环,就可以让内存得到释放和销毁
但是这样会有一个弊端,如果长期不调用这个block,这个循环引用的环就会一直存在

 

为什么 weakSelf 需要配合 strong self 使用

一般解决循环引用问题会这么写

__weak typeof(self) weakSelf = self;
[self doSomeBackgroundJob:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        ...
    }
}];

为什么 weakSelf 需要配合 strongSelf 使用呢?
在 block 中先写一个 strongSelf,其实是为了避免在 block 的执行过程中,突然出现 self 被释放的尴尬情况。通常情况下,如果不这么做的话,还是很容易出现一些奇怪的逻辑,甚至闪退。

比如下面这样

__weak __typeof__(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [weakSelf doSomething];
    [weakSelf doOtherThing];

});

在 doSomething 内,weakSelf 不会被释放.可是在执行完第一个方法后 ,weakSelf可能就已经释放掉,再去执行 doOtherThing,会引起 一些奇怪的逻辑,甚至闪退。
所以需要这么写

__weak __typeof__(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    __strong __typeof(self) strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doOtherThing];
});

Block 面试总结

  1. 什么是 Block?  
    是将函数及其执行上下文封装起来的对象。
  2. 为什么 Block会产生循环引用?
     

    如果当前block对当前某一成员变量进行截获的时候呢,block会对对应对象有一个强引用block,而当前block由于当前对象对其有一个强引用,就产生了一个自循环引用的循环问题。我们可以通过声明其为_ _weak 变量来进行循环引用的消除。

    如果我们定义了一个__block修饰的话,也会分场景的产生循环引用。ARC下面通过断环的方式解除循环引用。但是也有一个弊端,如果这个block一直没得到调用,这个循环引用是没办法解除的。

  3. 如何理解 Block 截获变量的特性?
    一定要从被截获变量类型的分类来回答这个问题。
    基本数据类型的局部变量,直接对其值进行截获。
    对象类型的局部变量,连同对象的修饰符一起截获(进行强引用)。
    静态局部变量:对其指针进行截获。   (static int a;  )
    全局变量和静态全局变量,是不进行截获的。
  4. 你都遇到过哪些循环引用?怎么解决的?
    内存管理中NSTimer引起的循环引用。
    block角度:
       block当中所捕获的变量是当前对象的一个成员变量,block也是当前对象的一个成员变量,就会造成自循环引用。加__weak所有权修饰符避免这种循环引用。
     __block也会产生循环引用。问题2有明确答案

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值