block

作者: QYCD | 来源:发表于2021-09-18 14:59 被阅读0次

    iOS中Block的用法,举例,解析与底层原理(这可能是最详细的Block解析)
    iOS Block用法与实现原理

    1 前言

    定义: 带有自动变量(局部变量)的匿名函数,它是C语言的扩充功能,之所以是扩展,是因为C语言并不允许这种函数存在

    1.1 匿名函数

    匿名函数是指不带函数名的函数,C语言中的函数是怎样的呢
    可使用Xcode来创建Command Line Tool项目来测试C函数

    int add(int a, int b);
    

    调用的时候:

    int result = add(10, 20);
    

    也可以通过指针调用函数,看起来没用到函数名:

    int res = (*ptr)(10, 2);
    

    实际上,在赋值给函数指针时,必须通过函数的名称才能获得该函数的地址:

    int (*ptr)(int, int) = &add;
    int res = (*ptr)(10, 2);
    printf("res = %d\n", res);
    
    输出:
    res = 12
    

    而通过block,就能够使用匿名函数,即不带函数名的函数

    1.2 带有自动变量

    关于"带有自动变量(局部变量)"的含义,是因为block拥有捕获外部变量的功能。在block中访问一个外部的局部变量,block会持有它的临时状态,自动捕获变量值,外部局部变量的变化不会影响它的状态。

    捕获外部变量:

    int val = 10;
        void (^blk)(void) = ^{
            NSLog(@"val = %d", val);
        };
        val = 20;
        blk();
    

    上面这段代码输出值是val = 10,而不是20

    block在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,然后在block块内使用该只读拷贝;换句话说block截获自动变量的瞬时值,或者block捕获的是自动变量的副本。

    由于block捕获了自动变量的瞬时值,所以在执行block语法后, 即使改写block中使用的自动变量的值也不会影响block执行时自动变量的值。

    自动变量值为一个变量情况

    解决block不能修改自动变量的值,使用__block修饰符

    __block int val = 10;
        void (^blk)(void) = ^{
            NSLog(@"val = %d", val);
        };
        val = 20;
        blk();
    
    输出:
    val = 20
    

    自动变量值为一个对象情况

    • 当自动变量为一个类的对象且没有__block修饰时,虽然不可以在block块内对该变量进行重新赋值,但可以修改该对象的属性。
      如果该对象是一个mutable对象,如NSMutableArray,则可以在block块内对NSMutableArray进行元素的增减
    NSMutableArray *array = [NSMutableArray arrayWithObjects:@"1", @"2", nil];
        NSLog(@"array: %@", array);
        void (^blk)(void) = ^{
            //array = [NSMutableArray array]; // 没有__block修饰 编译失败
            [array removeObjectAtIndex:0];
            [array addObject:@"3"];
        };
        blk();
        NSLog(@"array: %@", array);
    
    输出:
    array: (
        1,
        2
    )
    
    array: (
        2,
        3
    )
    

    2 block语法大全

    2.1 block声明、定义语法及其变形

    1> 标准声明与定义

    return_type (^blockName)(var_type) = ^return_type (var_type varName) { //... };
    blockName(var);
    
    • return_type: 表示返回值的类型
    • blockName: block的名称
    • var_type: 参数类型
    • varName: 参数名称

    2> 返回类型为void

    void (^blockName)(var_type) = ^void (var_type varName) { //... };
    blockName(var);
    

    可省略写成

    void (^blockName)(var_type) = ^(var_type varName) { //... };
    blockName(var);
    

    3> 参数类型为void

    return_type (^blockName)(void) = ^return_type (void) { //... };
    blockName(var);
    

    可省略写成

    return_type (^blockName)(void) = ^return_type { //... };
    blockName(var);
    

    4> 返回类型和参数类型都为void

    void (^blockName)(void) = ^void (void) { //... };
    blockName(var);
    

    可省略写成

    void(^blockName)(void) = ^{ //... };
    blockName(var);
    

    5> 匿名block
    block实现时,等号右边就是一个匿名函数,它没有函数名

    ^return_type (var_type varName) { //... };
    
    2.2 typedef简化block的声明
    typedef return_type(^blockName)(var_type varName);
    

    1> 作属性

    // 声明
    typedef void(^CTSelectBlock)(int index);
    // 属性
    @property (nonatomic, copy) CTSelectBlock selectBlock;
    

    2> 作参数

    // 声明
    typedef void(^CTSuccessBlock)(void);
    // 参数
    - (void)handleWithBlock:(CTSuccessBlock)block {
        //...
    }
    
    2.3 block的常见用法

    1> 局部位置声明一个block型的变量

    return_type (^blockName)(var_type varName) = ^return_type (var_type varName) { //... };
    
    void(^globalBlockInMemory)(int number) = ^(int number) {
            NSLog(@"value = %d", number);
        };
        globalBlockInMemory(10);
    
    输出:
    value = 10;
    

    2> @interface位置声明一个block型的属性

    @property(nonatomic, copy) return_type (^blockName)(var_type varName);
    
    // 按钮点击block
    @property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);
    
    - (void)btnClick:(UIButton *)btn {
        if (self.btnClickedBlock) {
            self.btnClickedBlock(btn);
        }
    }
    
    self.btnClickedBlock = ^(UIButton *sender) {
            // ...
        };
    

    3> 定义方法时,定义block型的形参

    - (void)yourmethod:(return_type(^)(var_type))blockName;
    
    - (void)addClickedBlock:(void(^)(id obj))clickAction;
    

    4> 调用如上方法时,block作为实参

    - (void)btnClick:(UIButton *)btn {
        if (self.btnClickedBlock) {
            self.btnClickedBlock(btn);
        }
    }
    
    2.4 block的少见用法

    1> block的内联用法

    ^return_type (var_type varName) { //... }(var);
    
    // 这种用法不常用,匿名block声明后立即被调用
    ^void (int val) {
            NSLog(@"val==%d", val);
        }(20);
    

    2> block的递归调用

    // block内部调用自身
    __block return_type (^blockName)(var_type varName) = [^return_type(var_type varName) {
        if (returnCondition) { blockName = nil; return;}
        // 递归调用
        blockName(varName);
    }  copy];
    
    // 初次调用
    blockName(varValue);
    

    3> block作为返回值

    方法的返回值是一个block
    - (return_type (^)(var_type))methodName {
        return ^return_type(var_type param) { //... };
    }
    

    3. block应用场景

    1> 响应事件

    vc为tableView的代理,cell中按钮点击事件,vc作出响应
    
    cell.h
    @interface TestCell : UITableViewCell
    
    @property (nonatomic, copy) void (^clickBtnBlock)(UIButton *sender);
    
    @end
    
    cell.m
    - (void)btnClick {
        if (self.clickBtnBlock) {
            self.clickBtnBlock(self.btn);
        }
    }
    
    vc
    cell.clickBtnBlock = ^(UIButton * _Nonnull sender) {
            // ...
        };
    

    2> 传递数据,如例子1 传递的是UIButton,然后常见的我们利用AFN封装网络框架时经常将response回调

    3> 链式语法

    链式编程思想: 核心思想为将block作为方法的返回值,返回值的类型为调用者本身,并将该方法以setter的形式返回,这样就实现了连续调用,即为连式编程。

    Masonry:

    [self.resultLabel mas_makeConstraints:^(MASConstraintMaker *make) {
            make.leading.equalTo(self.nameLabel);
            make.trailing.mas_equalTo(-10);
            make.top.equalTo(self.resultDesL);
        }];
    

    链式编程思想实现一个计算器:

    CTCalculator.h:
    @interface CTCalculator : NSObject
    
    @property (nonatomic, assign) int result;
    
    - (CTCalculator * (^)(int num))add;
    
    @end
    
    CTCalculator.m:
    @implementation CTCalculator
    
    - (CTCalculator * _Nonnull (^)(int num))add {
        return ^CTCalculator * (int num) {
            self.result += num;
            return self;
        };
    }
    
    @end
    
    调用:
    CTCalculator *maker = [[CTCalculator alloc] init];
        int result = maker.add(2).add(10).result;
        NSLog(@"result = %d", result);
    

    4 block使用注意

    4.1 截获自动变量与__block

    block所在函数中会捕获自动变量(局部变量), 但是不能修改它,但是可以改变全局变量、静态变量,全局静态变量

    • 不能修改自动变量的值: block捕获的是自动变量的const值,名字一样,不能修改
    • 可以修改静态变量的值: 静态变量属于类的,不是某一个变量
    int val = 10;
        void (^blk)(void) = ^{
            NSLog(@"val = %d", val);
        };
        val = 20;
        blk();
    结果:
    val = 10
    
    @property (nonatomic, assign) int value;
    
    self.value = 10;
        void (^blk1)(void) = ^{
            NSLog(@"val = %d", self.value);
        };
        self.value = 21;
        blk1();
    
    结果:
    val = 21
    
    static int kkk = 10;
    
    void (^blk3)(void) = ^{
            NSLog(@"val = %d", kkk);
        };
        kkk = 22;
        blk3();
    
    结果:
    val = 22
    
    解决block不能修改自动变量(局部变量)的另外一个方法是使用__block修饰符
    
    void (^blk3)(void) = ^{
            NSLog(@"val = %d", kkk);
        };
        kkk = 22;
        blk3();
    
    结果:
    val = 20
    
    4.2 截获对象

    对于捕获Objc对象,不同于基本数据类型,block会引起对象的引用计数变化

    4.3 block引起的循环引用

    一般我们总会在设置block后,在合适的时间回调block,但不希望回调时block已经被释放,所以需要对block进行copy,copy到堆中以便后用。

    block可能会导致循环引用问题,因为block在拷贝到堆上时,会retain其引用的外部变量,如果block中引用了他的宿主对象,就很有可能引起循环引用。

    • MRC下,__block可以消除循环引用
    • ARC下,必须用弱引用才可以解决循环引用问题。iOS5之后可以直接使用__weak,之前则只能使用__unsafe_unretained,__unsafe_unretained的缺点是指针释放后自己不会置空

    使用__weak修饰时,由于不知道self何时会被释放,为了保证在block内不会被释放,结合__strong。

    __weak typeof(self) weakSelf = self;
        self.testBlock = ^(int index) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            [strongSelf test];
        };
    
    4.4 不是所有的block内的self都必须weak一下

    有些情况可以直接使用self,比如系统方法

    block对self强引用着,但是self却不持有这个静态方法
    [UIView animateWithDuration:2 animations:^{
            self.
        }];
    

    是不是循环引用,判断条件就是是不是相互强持有引用,block对self强持有,但self并没有直接或间接持有block,就不会造成循环引用


    根据block对象创建所属数据区的不同,区分三种block:

    • _NSConcreteStackBlock: 在栈上创建的block对象
    • _NSConcreteMallocBlock: 在堆上创建的block对象
    • _NSConcreteGlobalBlock: 全局数据区的block对象
    创建一个全局block
    void (^blk)(void) = ^{
        NSLog(@"global block");
    };
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        NSLog(@"%@", [blk class]);
    }
    
    NSLog(@"%@", [blk class]);  // 打印 __NSGlobalBlock__
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        void (^blk1)(void) = ^{
            NSLog(@"blk1");
        };
        NSLog(@"%@", [blk1 class]); // 打印 __NSGlobalBlock__
        
        int a = 10;
        void (^blk2)(void) = ^{
            NSLog(@"a = %d", a);
        };
        NSLog(@"%@", [blk2 class]); // 打印 __NSMallocBlock__
    }
    

    上述代码块中,全局blk自然是存储在全局数据区,但注意在函数栈上创建的blk,如果没有捕获自动变量,block的结构实例还是会被设置在程序的全局数据区,而不是栈上。

    而捕获了自动变量的block打印的类是设置在堆上的NSMallocBlock。

    配置在栈上的block,如果其所属的栈作用域结束,该block就会被废弃,对于超出block作用域仍需使用block的情况,block提供了将block从栈上复制到堆上的方法来解决这种问题,即使block栈作用域结束,但被拷贝到堆上的block还可以继续存在。

    ARC下,大多数情况下编译器会进行判断,自动生成将block从栈上复制到堆上的代码,以下几种情况栈上的block会自动复制到堆上:

    调用block的copy方法
    将block作为函数返回时
    将block赋值给__strong修饰的变量时
    向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
    

    前面在栈上捕获了自动变量的block之所以在栈上创建,却是NSMallocBlock类,就是因为这个block对象赋值给了__strong修饰的变量blk2(ARC下,对象的默认修饰符是__strong)

    ARC下很少见到_NSConcreteStackBlock类的block,大多数情况下编译器都保证了block是在堆上创建的,如下仅最后一行代码直接使用一个不赋值给变量的block,它的类才是NSStackBlock

    int count = 10;
        
        NSLog(@"global block:%@", [^{
            NSLog(@"global block");
        } class]); // global block:__NSGlobalBlock__
        
        NSLog(@"copy block:%@", [[^{
            NSLog(@"copy block: %d", count);
        } copy]class]); // copy block:__NSMallocBlock__
        
        NSLog(@"stack block:%@", [^{
            NSLog(@"stack block: %d", count);
        } class]); // stack block:__NSStackBlock__
    
    使用__block发生了什么

    block捕获的自动变量使用__block修饰后,就可以在block块内读写该变量,也可以在原来的栈上读写该变量。

    自动变量的截获保证了栈上的自动变量被销毁后,block内仍可使用该变量。

    __block保证了栈上和block内(通常在堆上)可以访问和修改"同一个变量",__block是如何实现这一功能的?

    __block发挥作用的原理: 将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。

    相关文章

      网友评论

          本文标题:block

          本文链接:https://www.haomeiwen.com/subject/yqdahltx.html