iOS晋级知识汇总(九)Block

block

  • Block介绍
  • 截获变量
  • __block修饰符
  • Block的内存管理
  • Block的循环引用

什么是Block?

Block是将函数及其执行上下文封装起来的对象。

//想要更彻底的理解block需要clang它的中间码
clang -rewrite-objc file.m  查看编译之后的文件内容 
  • Block是一个对象
  • 对象封装了函数和执行上下文

block的调用

Block调用就是函数的调用

截获变量

下述代码的结果是16,因为block对局部变量multiplier进行了截获

int multiplier = 8;
    
int(^MuBlock)(int) = ^int(int num){
    
    return num * multiplier;
    
};
    
multiplier = 6
    
MuBlock(2);
  • 局部变量
    • 基本数据类型
    • 对象类型
  • 静态局部变量
  • 全局变量
  • 静态全局变量

什么是block截获变量?

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

如何理解我们需要编译中间码来帮我理解

//因为在ARC中的block跟MRC不一样
//因为MRC中没有block循环引用问题,下面会有解释
clang -rewrite-objc -fobjc-arc file.m

编译代码,以及OC代码如下:

struct __MyBlock__MyMethod_block_impl_0 {
  struct __block_impl impl;
  struct __MyBlock__MyMethod_block_desc_0* Desc;
  //截获局部变量的值(基本数据类型)
  int var;
  //连同所有权修饰符一起截获
  __unsafe_unretained id unsafe_obj;
  __strong id strong_obj;
  
  //以指针形式截获静态局部变量
  int *static_var;
  
  //对全局变量、静态全局变量不截获
  
  __MyBlock__MyMethod_block_impl_0(void *fp, struct __MyBlock__MyMethod_block_desc_0 *desc, int _var, __unsafe_unretained id _unsafe_obj, __strong id _strong_obj, int *_static_var, int flags=0) : var(_var), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj), static_var(_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//全局变量
int global_var = 6;
//静态全局变量
static int static_global_var = 8;

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

}

截获变量的总结

  • 局部变量
    • 基本数据类型
      • 截获其值
    • 对象类型
      • 指针连同所有权一起截获
  • 静态局部变量
    • 截获其指针
  • 全局变量
    • 不截获
  • 静态全局变量
    • 不截获

__block修饰符

什么场景需要使用__block?

一般情况下,对截获变量进行赋值(跟使用操作不一样)操作需添加__block修饰符。

例子:下面代码有什么问题吗?

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

答案:没有问题,因为array并没有去赋值。不需要使用__block。

再来看一个例子:下面代码有什么问题吗?

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

答案: array需要添加__block修饰符。

对变量进行赋值时,合适需要添加__block?

  • 需要添加__block修饰符的情况?
    • 局部变量
      • 基本数据类型
      • 对象类型
  • 不需要添加__block修饰符
    • 静态局部变量
    • 全局变量
    • 静态全局变量

__block 修饰的变量最终变成了什么?

还是通过clang 命令来编译出中间码

- (void)MyMethod{
    __block int multiplier = 8;
    int(^MuBlock)(int) = ^int(int num){
        return num * multiplier;
    };
    
    multiplier = 6;
    NSLog(@"%d",MuBlock(2));
}
// @implementation MyBlock
struct __Block_byref_multiplier_0 {
//因为有isa指针
  void *__isa;  
  //指向同类指针的__forwarding
__Block_byref_multiplier_0 *__forwarding;
 int __flags;
 int __size;
 //我们截获的变量
 int multiplier;
};

综合上述说明通过__block修饰的变量最终变成了对象,如果在OC代码中直接修改这个变量的值,就会走下面的步骤

static void _I_MyBlock_MyMethod(MyBlock * self, SEL _cmd) {

    __attribute__((__blocks__(byref))) __Block_byref_multiplier_0 multiplier = {(void*)0,(__Block_byref_multiplier_0 *)&multiplier, 0, sizeof(__Block_byref_multiplier_0), 8};

    int(*MuBlock)(int) = ((int (*)(int))&__MyBlock__MyMethod_block_impl_0((void *)__MyBlock__MyMethod_block_func_0, &__MyBlock__MyMethod_block_desc_0_DATA, (__Block_byref_multiplier_0 *)&multiplier, 570425344));
    //打印代码之前,我们给这个__block修饰的变量对象的值赋值到了6
    (multiplier.__forwarding->multiplier) = 6;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_8z_j2rlh3t16m9cdrljdz_r67380000gn_T_Myblock_5bd0fd_mi_0,((int (*)(__block_impl *, int))((__block_impl *)MuBlock)->FuncPtr)((__block_impl *)MuBlock, 2));

}
//block方法实现
static int __MyBlock__MyMethod_block_func_0(struct __MyBlock__MyMethod_block_impl_0 *__cself, int num) {
  __Block_byref_multiplier_0 *multiplier = __cself->multiplier; // bound by ref


        return num * (multiplier->__forwarding->multiplier);

    }

打印代码之前,给这个__block修饰的变量对象的值赋值到了6,并且在block中的实现是直接通过(multiplier->__forwarding->multiplier)来进行操作,所以打印结果应该会是修改过后的值。也就是12.

栈的__Block修饰的变量.png

__forwarding存在的意义?

  • 不论在任何内存位置,都可以访问同一个__block变量。

__block相关的笔试题

- (void)MyMethod{
    
    __block int multiplier = 8;
    int(^MuBlock)(int) = ^int(int num){
        return num * multiplier;
    };
    multiplier = 6;
    NSLog(@"%d",MuBlock(2));
}

上述调用结果是12.

Block的内存管理

  • _NSConcreteGlobalBlock 全局block
  • _NSConcreteStackBlock 栈block
  • _NSConcreteMallocBlock 堆block

Block的内粗分布:

全局、栈、堆Block的内存分布.png

Block的copy操作

Block的copy操作.png

声明一个成员变量是block,如果这个block使用的是assign,通过成员变量去访问block的话,可能栈所对应的函数退出之后,在内存中就销毁掉了,所以继续访问可能崩溃

栈上Block的copy

当我们在栈上面的block进行copy以后,那么在MRC环境下是否会引起内存泄漏?
产生内存泄漏。

栈上block的copy操作.png

栈上的Block的销毁

栈上,等作用域

栈block的销毁.png

栈上__block变量的copy

__block修饰的变量都会转换成一个对象,而这个对象被copy的时候会在堆上面开辟一块空间生成一个全新的__block修饰的变量对象,而这两个对象的__forwarding指针都指向了堆上面的对象。如果没有做copy操作那么就使用的是栈上面的__block对象。

栈的__Block修饰的变量.png

Block代码的解读

@property(nonatomic,copy)MuBlock blk;
- (void)MyMethod{
    
    //用__block修饰的变量会在栈上生成一个block对象,里面有__forwarding指针和成员变量multiplier。__forwarding指针指向这个block对象自己
    __block int multiplier = 8;
    
    //当我向_blk属性赋值的时候实际上是对这个block进行copy,所以这个block会在堆上开辟一个空间。在堆上有另外一个副本
    _blk = ^int(int num){
        return num * multiplier;
    };
    
    /这个时候修改的值是__forwarding指向的对象的成员变量multiplier,因为_blk实际上是堆上面的block,下面的代码实际上是修改堆上面的__block对象
    multiplier = 6;
    [self excMuBlock];
    
}

- (void)excMuBlock{
    int result = _blk(4);
    NSLog(@"%d",result);
    结果应该是24.
}

代码分析题

block循环引用的问题

下面代码有何问题?

//在ARC环境下
- (void)MyMethod{
    //默认是strong属性
    _array = [NSMutableArray arrayWithObjects:@"111", nil];
    //默认是copy属性
    //_array、 _strblk都是当前属性的_array是strong,_strblk是copy
    _strblk = ^NSString *(NSString * str){
        return [NSString stringWithFormat:@"%@",_array[0]];
    };
    _strblk(@"hello");
}

下面继续分析clang代码


static void _I_MyBlock_MyMethod(MyBlock * self, SEL _cmd) {

    (*(NSMutableArray *__strong *)((char *)self + OBJC_IVAR_$_MyBlock$_array)) = ((NSMutableArray * _Nonnull (*)(id, SEL, ObjectType  _Nonnull __strong, ...))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("arrayWithObjects:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_8z_j2rlh3t16m9cdrljdz_r67380000gn_T_Myblock_e9f218_mi_0, __null);

    (*(__strong xhBlock *)((char *)self + OBJC_IVAR_$_MyBlock$_strblk)) = ((NSString *(*)(NSString *__strong))&__MyBlock__MyMethod_block_impl_0((void *)__MyBlock__MyMethod_block_func_0, &__MyBlock__MyMethod_block_desc_0_DATA, self, 570425344));

    ((NSString *(*)(__block_impl *, NSString *__strong))((__block_impl *)(*(__strong xhBlock *)((char *)self + OBJC_IVAR_$_MyBlock$_strblk)))->FuncPtr)((__block_impl *)(*(__strong xhBlock *)((char *)self + OBJC_IVAR_$_MyBlock$_strblk)), (NSString *)&__NSConstantStringImpl__var_folders_8z_j2rlh3t16m9cdrljdz_r67380000gn_T_Myblock_e9f218_mi_2);

}

由中间码我们可以分析出来,当在block中使用_array的时候实际上是通过(*(NSMutableArray *__strong *)((char *)self + OBJC_IVAR_$_MyBlock$_array))这样的形式获取到_array,这样的话,相当于,在self中的block中又强引用了self这样就是循环引用。自循环引用

解决方案:

//第一种
- (void)MyMethod{
    
    _array = [NSMutableArray arrayWithObjects:@"111", nil];
    
    __weak NSArray *weakarray = _array;
    
    _strblk = ^NSString *(NSString * str){
        return [NSString stringWithFormat:@"%@",weakarray[0]];
    };
    
    _strblk(@"hello");
    
}
//第二种
- (void)MyMethod{
    
    _array = [NSMutableArray arrayWithObjects:@"111", nil];
    
     __weak typeof(self)weakself = self;
    
    _strblk = ^NSString *(NSString * str){
        return [NSString stringWithFormat:@"%@", weakself.array[0]];
    };
    
    _strblk(@"hello");
    
}


创建一个弱引用指针指向_array,这样在block中,使用的就是弱引用指针,当前对象释放后,弱引用也就会自动释放,当前当前对象的block也会被释放掉。

分析下面代码:

- (void)MyMethod{
    
    __block MyBlock *blockself = self;
    
    _blk = ^int(int num){

        return num * blockself.var;

    };
    
    _blk(3);
    
}

分析

  • MRC下,不会产生循环引用
  • 在ARC下会产生循环引用,引起内存泄漏

在self修饰为__block的时候,系统会自动生成一个对象__Block_byref_blockself_0
而这个对象强持有self。最终导致了,当前对象持有block,block持有__block对象,__block对象对象持有self造成大环引用。

ARC下的解决方案

大环循环引用.png

断环,当我block中的__block使用完成以后,需要直接赋值为nil这样就可以断环了。

- (void)MyMethod{
    
    __block MyBlock *blockself = self;
    
    _blk = ^int(int num){

         int = num * blockself.var;
         blockself = nil;
        return num * blockself.var;

    };
    
    _blk(3);
    
}

BLock面试题

什么是block?

block就是一个对象,这个对象封装了函数以及其上下文。

为什么block会产生循环引用?

  • 如果当前block对当前对象的成员变量有一个强引用,对象的成员变量归根究底的还是需要通过self去访问它,那么就是当前对象强引用了blcok,而block中又调用了self并将其强引用,这样就造成了自循环引用,解决方案使用__weak修饰你的成员变量,或者修饰当前对象。在block直接调用weak变量,

  • 大环引用,当使用__block修饰self,并且self强引用了block。这样就造成了大环引用。
    self强持有block,block强持有__block对象,__block对象强持有了self,这就是大环循环引用。解决方案就是使用断环的方式,但是有一个缺点就是,如果这个block不去执行的话永远不会解除这个循环引用。将__block修饰的self在block中使用完成置nil。

怎么样理解Block截获变量的特性?

  • 基本数据局部变量
  • 对象类型局部变量
    • 对其持有一个强引用,连同它的所有权
  • 对于静态局部变量
    • 对其指针进行截获
  • 静态全局变量、全局变量
    • 不产生截获的

你都遇到过哪些循环引用?你又是如何解决的?

block捕获的变量是当前的成员变量,block也是当前对象的成员变量,就会造成自循环引用,可以避免这种情况,对当前当前成员变量使用__weak所有权修饰符去解除
__block的情况也会造成循环引用,这种情况跟自循环引用不太一样,大环循环引用有个弊端就是当你在不调用block的情况下,这个循环引用会一直解不开。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容