全方位剖析iOS高级技术问题(五)之Block相关问题

本文主要内容

一.Block介绍
二.Block截获变量
三.__block修饰符
四.Block的内存管理
五.Block的循环引用

截屏2022-08-17 10.11.20.png

一.Block介绍

1、什么是Block

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

2、Block的本质及其调用

  • Block是一个对象,它封装了函数函数的执行上下文

MCBlock.m

- (void)method {


    int multiplier = 6;

    int(^Block)(int) = ^int(int num) {


        return num * multiplier;


    };


    

    Block(2);
}





源码解析

  • 使用【clang -rewrite-objc file.m】编译后,可查看编译之后的.cpp文件内容

MCBlock.cpp

// I表示当前类的实例方法,MCBlock当前类名,method当前方法名method
static void _I_MCBlock_method(MCBlock *self, SEL _cmd) {
    int multiplier = 6;
    
    /*
     int(^Block)(int) = ^int(int num) {
         return num * multiplier;
     };
    */
    
    int(*Block)(int) = (int((int (*)))&__MCBlock__method_block_impl_0((void *)__MCBlock__method_block_func_0, &__MCBlock__method_block_desc_0_DATA, multiplier));
    
    /*
    Block(2);
    */
    ((int (*)(__block_impl *, int))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block, 2);
}


// __MCBlock__method_block_impl_0
struct __MCBlock__method_block_impl_0 {
    struct __block_impl impl;
    struct __MCBlock__method_block_desc_0* Desc;
    int multiplier;
    // 结构体构造函数
    __MCBlock__method_block_impl_0(void *fp, struct __MCBlock__method_block_desc_0 *desc, int _mutiplier, int flags=0) : multiplier(_multiplier) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// __block_impl:Block结构体
struct __block_impl {
    void *isa; // isa指针,Block是对象的标志
    int Flags;
    int Reserved;
    void *FuncPtr; // 函数指针
};

Static int __MCBlock__method_block_func_0(struct __MCBlock__method_block_impl_0 *__cself, int num) {
    int multiplier = __cself->multiplier; // bound by copy
    return num * multiplier;
}

二.Block截获变量

  • 局部变量:对于基本数据类型截获其值,对于对象类型连同所有权修饰符一起截获。

  • 静态局部变量:以指针形式截获

  • 全局变量:不截获

  • 静态全局变量:不截获

1、截获变量源码解析

  • 使用【clang -rewrite-objc -fobjc-arc file.m】命令编译

MCBlock.m

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

- (void)method {
    // 基本数据类型的局部变量
    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 对象类型> var %d", unsafe_obj);
        NSLog(@"局部变量<__strong 对象类型> var %d", strong_obj;
        
        NSLog(@"静态变量 %d", static_var);
        
        NSLog(@"全局变量 %d", global_var);
        NSLog(@"静态全局变量 %d", static_global_var);
    };
    
    Block();
}

MCBlock.cpp

int global_var = 4;
static int static_global_var = 5;


struct __MCBlock__method_block_impl_0 {
    struct __block_impl impl;
    struct __MCBlock_method_block_desc_0* Desc;
    

    // 截获基本数据类型局部变量的值
    int var;
    // 连同所有权修饰符一起截获
    __unsafe_unretained id unsafe_obj;
    __strong id strong_obj;
    // 以指针形式截获局部变量
    int *static_var;
    
    // 对全局变量和静态全局变量不截获
}

2、截获变量实例

  • 基本数据类型变量
- (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
  • 静态局部变量
- (void)method {


    static int multiplier = 6;
    int(^Block)(int) = ^int(int num) {


        return num * multiplier;


    };


    multiplier = 4;

    NSLog(@"result is %d",Block(2));

}





结果:result is 8



三.__block修饰符

1、什么场景下需要使用__block修饰符?

一般情况下,对被截获变量进行`赋值`操作需要添加`__block修饰符`

如下情况是否需要添加__block修饰符

示例1

{






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

        [array addObject: @123];
    };



    Block();
}

答:不需要。当前是对array变量的使用,而非赋值操作。

示例2

{






   NSMutableArray *array = nil;
    void(^Block)(void) = ^{

        array = [NSMutableArray array];
    };



    Block(); 
}

答:当前是对array变量的赋值操作,所以需要在array的声明处添加__block修饰符,否则编译会报错。

2、对变量进行赋值

  • 需要__block修饰符:基本数据类型和对象类型的局部变量。
  • 不需要__block修饰符:静态局部变量、全局变量、静态全局变量。

3、__block机制与原理

  • __block修饰的变量变成了对象;

截屏2022-08-17 15.06.55.png示例3

{






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



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





结果:result is 8

截屏2022-08-17 15.10.06.png

注意:block在栈上创建,__forwarding指针指向自己;堆上创建的block,__forwarding指针会指向其他地方。

四.Block的内存管理

  • 栈Block:放在栈上
  • 堆Block:放在堆上
  • 全局Block:放在已初始化数据中

截屏2022-08-17 15.18.56.png

1、Block的Copy操作

截屏2022-08-17 15.22.13.png

1.1 栈上的Block

栈上Block的Copy

  • 栈上有一个Block,Block中使用了__block变量,当对栈上的Block进行copy操作后,会在堆上产生和栈上一摸一样的堆Block和__block变量。随着变量作用域的结束,栈上的Block和__block变量都会随之销毁,而堆上的Block和__block变量仍然存在。
    截屏2022-08-17 15.24.50.png

问题:在MRC环境下,当对栈上的Block进行Copy操作之后,是否会引起内存泄漏?

答:会。对栈上的Block进行Copy操作后,堆上的Block没有其他成员指向它,相当于alloc对象后,没有调用release效果相同。

栈上Block的销毁

  • 栈上有一个__block变量和一个Block,在变量的作用域结束后或栈上的函数退出后,栈上的__block变量和Block都会被销毁。注意⚠️,由于__block修饰的变量已经成为对象,所以会被销毁。

截屏2022-08-17 15.23.53.png

1.2 栈上__block变量的Copy

  • 对栈上__block变量进行Copy操作后,会在堆上产生一摸一样的__block变量,由于栈上__forwarding指针也指向堆上的__block变量,所以对__block变量的任何修改,最终都是对堆上__block变量的修改。

截屏2022-08-17 15.48.00.png

__forwarding指针存在的意义?
对栈上的__block变量进行Copy操作后,栈上的__forwarding指针指向堆上的__block变量,堆上的__forwarding指针也指向自身。


1.3 __forwarding的总结

typedef int(^Block)(int num);


@property (nonatomic, copy, readwrite) Block blk;

{
    // 变成了对象multiplier
    __block int multiplier = 10;
    // Copy操作:则下面操作都是对堆上的multiplier进行操作
    _blk = ^int(int num) {
        return num * multiplier;
    };
    // multiplier对象的__forwarding指针对其成员变量multiplier赋值
    multiplier = 6;
    
    [self executeBlock];
}

- (void)executeBlock {
    // 堆上的blk 
    int result = _blk(4);
    NSLog(@"result is %d", result);
}
结果:result is 24

2、__forwarding存在的意义

  • 不论在任何内存位置
  • 都可以顺利的访问同一个__block变量
  • 没有对__block变量进行Copy,操作的就是栈上的__block变量;如果进行了Copy操作,无论是在栈上还是堆上,对变量的修改或赋值操作都是堆上的__block变量,

五.Block的循环引用

1、如下代码会不会引起循环引用

{






    _array = [NSMutableArray arrayWithObject: @"block"];

    _strBlk = ^NSString*(NSString *num) {
        return [NSString stringWithFormat:@"helloc_%@", array[0]];
    };



    

    _strBlk(@"hello");
}





  • 会产生自循环方式循环引用。由于当前对象是通过Copy属性关键字声明的strBlk,所以当前对象对Block存在强引用,而Block表达式中使用到当前对象的array成员变量,根据Block截获变量部分内容,Block对象中使用到对象类型的局部变量,会连同其属性关键字一起被截获,array的属性关键字为strong,所以在Block中有一个strong类型的指针指向当前对象,由此就产生了循环引用。

截屏2022-08-17 16.30.24.png

  • 解决方案:在当前栈上声明(创建)一个__weak修饰的weakArray指针(变量),来指向原对象的array成员变量,在Block中使用weakArray,由此避免循环引用。
  • 为什么通过使用__weak修饰符就可以避免循环引用?根据Block截获变量原理,对对象的截获是连同所有权修饰符一同截获,当被截获的对象是__weak修饰的对象时,在Block中产生的结构体中持有的截获对象也是__weak类型的。

截屏2022-08-17 16.58.15.png

{






    _array = [NSMutableArray arrayWithObject: @"block"];

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

    

    _strBlk(@"hello");
}

2、如下代码有何问题

{






    __block MCBlock *blockSelf = self;

    _blk = ^int(int num) {

        // var = 2

        return num * blockSelf.var;
    };

    _blk(3);
}





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

截屏2022-08-17 17.08.04.png

  • 解决方案:

截屏2022-08-17 17.11.13.png

{






    __block MCBlock *blockSelf = self;

    _blk = ^int(int num) {

        // var = 2

        int result = num * blockSelf.var;
        blockSelf = nil;
        return result;
    };
    _blk(3);
}
注意⚠️:如果没有调用此Block,则此循环引用环会一直存在!

本文总结

问题1:什么是Block?

Block是关于函数及其执行上下文封装的对象;

问题2:为什么Block会产生循环引用?

  • 当前Block对当前对象的某一成员变量进行截获,Block会对对应变量有前引用,而当前对象对Block有前引用,就产生自循环引用。声明成员变量为__weak变量来解决
  • 定义__block说明符也可能会引起循环引用。MRC下不会产生循环引用,ARC下会产生循环引用。解决方案:在Block中将__block修饰的变量置为nil,但是注意:如果没有调用此Block,则此循环引用环会一直存在!

问题3:怎样理解Block截获变量的特性?

  • 基本数据类型变量只截获其值
  • 对象类型局部变量连同所有权修饰符一同被截获
  • 对静态局部变量以指针形式截获
  • 对全局变量和静态全局变量不截获

问题4:开发中遇到过哪些循环引用?如何解决?

五.Block的循环引用

有任何问题,欢迎?各位评论指出!觉得博主写的还不错的麻烦点个赞喽?

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYQRgdRW' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片