iOS Block

作者: 阿饼six | 来源:发表于2019-09-30 14:51 被阅读0次

    ​ 前言:Block在iOS开发中举足轻重,但对于初学者来说又比较抽象,使用注意点也比较多。本文先介绍Block的定义,然后介绍Block的大体四类用法:直接使用、作为属性、作为方法参数和作为方法返回值;通过Block作为返回值引出链式编程思想,并给出了代码例子;然后介绍了Block的变量捕获机制,Block在内存中的三种类型:NSGlobalBlock、NSStackBlock和NSMallocBlock;最后介绍了Block可能造成循环引用并如何解决循环引用问题。

    一、Block定义:

    1、Block定义:

    ​ 带有自动变量(局部变量)的匿名函数。Block本质上也是一个OC对象,它内部也有个ISA指针(后面会讲解block类型,会有详细的ISA指针准确的指向)。利用clang命令工具把OC代码转成c++代码,截取一部分源码:

    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
     
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; // isa指针指向这个类,准确的指向看下文
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    2、Block的声明:

    ​ return_type (^BlockName) (var_type var1, var_type var2),return_type是返回值类型,可以是void(无返回值),BlockName是block名称,var1、var2是block的参数。

    二、Block用法:

    ​ block在iOS开发中应用很广泛,可以很方便、很简单的进行各种传值、各种回调、异步线程处理等,使用大体分为四类:

    ​ 1)直接使用;

    ​ 2)作属性;

    ​ 3)作方法参数;

    ​ 4)作返回值;

    下面一一来讲解如何使用。

    1、Block直接使用:

    ​ 直接上代码,运行下面代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSInteger num1 = 0;
        NSLog(@"block 执行之前");
        void (^testBlock) (NSInteger) = ^(NSInteger num2) {
            NSLog(@"block 执行中...");
            NSLog(@"num1 = %ld, num2 = %ld", num1, num2);
            NSLog(@"sum = %ld", num1 + num2);
            NSLog(@"block 执行结束...");
        };
        num1 = 3;
        NSLog(@"block 被调用");
        testBlock(6);
        NSLog(@"程序结束");
    }
    

    打印结果:

    block 执行之前
    block 被调用
    block 执行中...
    num1 = 0, num2 = 6 //为啥num1还是0,先卖个关子
    sum = 6
    block 执行结束...
    程序结束
    

    从上面打印结果可以很直观的看到block执行前后的顺序,这个就是直接使用block的过程。

    2、Block作属性:

    ​ 假设一个常用场景,A控制器跳转到B控制器,B控制器有个点击事件需要回传给A,那么可以在B控制器定义一个block作为属性,在A控制器跳转的地方对B属性block进行赋值操作,代码如下:

    ​ A控制器中:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        UIButton *nextBtn = [UIButton new];
        nextBtn.frame = CGRectMake(100, 100, 100, 50);
        [nextBtn setTitle:@"B控制器" forState:UIControlStateNormal];
        [nextBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
        [self.view addSubview:nextBtn];
        [nextBtn addTarget:self action:@selector(pushToNextVC) forControlEvents:UIControlEventTouchUpInside];
    }
    
    - (void)pushToNextVC {
        BViewController *vc = [BViewController new];
        vc.moneyBlock = ^(float money) {
            NSLog(@"money = %.2f", money);
        }; // 对B控制器的属性block进行赋值操作
        [self.navigationController pushViewController:vc animated:YES];
    }
    

    ​ B控制器:

    // .h文件
    typedef void(^ClickPayBtn)(float money); //使用typedef 
    
    @interface BViewController : UIViewController
    @property(nonatomic, copy) ClickPayBtn moneyBlock;
    @end
    
    // .m文件
    @implementation BViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        UIButton *payBtn = [UIButton new];
        payBtn.frame = CGRectMake(100, 100, 100, 50);
        [payBtn setTitle:@"付钱" forState:UIControlStateNormal];
        [payBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
        [self.view addSubview:payBtn];
        [payBtn addTarget:self action:@selector(didClickPayBtn) forControlEvents:UIControlEventTouchUpInside];
    }
    
    - (void)didClickPayBtn {
        if (self.moneyBlock) {
            self.moneyBlock(100);
        }
    }
    @end
    

    点击付钱按钮,打印结果:

    money = 100.00
    
    3、Block作方法参数:

    ​ 很多时候涉及到线程操作之后的回调都是用block处理,比如常用的AFNetworking网络请求框架,请求成功或者失败都是使用block进行回调的,例如:

    - (nullable NSURLSessionDataTask *)POST:(NSString *)URLString parameters:(nullable id)parameters success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
    

    其中的success和failure都是block作为参数,当然我们也可以用typedef来定义一个success类型和failure类型的block,更方便使用。

    typedef void (^SuccessBlock)(NSURLSessionDataTask *task, id responseObject);
    typedef void (^FailureBlock)(NSURLSessionDataTask * _Nullable task, NSError *error);
    
    - (nullable NSURLSessionDataTask *)POST:(NSString *)URLString parameters:(nullable id)parameters success:(SuccessBlock)success failure:(FailureBlock)failure;
    

    当然,没有涉及到线程操作也可以用block进行操作回调,使用方法类似。

    4、Block作返回值:

    ​ iOS开发常用约束布局框架Masonry,就有block作为返回值,Masonry代码:

    - (MASConstraint * (^)(id))equalTo {
        return ^id(id attribute) {
            return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
        };
    }
    

    其实格式就是:

    - (return_type (^)(var_type))methodName {
        return ^return_type(var_type var) {
            return xxxx;
        }
    }
    

    下面使用一个例子加深印象:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSString* (^block)(NSInteger); //声明一个block
        block = [self testBlock]; //赋值
        NSString *returnStr = block(10); //调用block
        NSLog(@"block = %@", block);
        NSLog(@"%@", returnStr);
    }
    
    // block作为返回值
    - (NSString* (^)(NSInteger))testBlock {
        return ^NSString* (NSInteger age) {
            return [NSString stringWithFormat:@"Tom.age = %ld", age];
        };
    };
    

    打印结果:

    block = <__NSGlobalBlock__: 0x109054468>
    Tom.age = 10
    

    注意:这里可以引出链式编程思想,该思想核心是将block作为方法的返回值,且block的返回值类型是调用者本身,且将该方法以setter的形式返回,这样就可以实现连续调用,即链式编程。直接撸代码如下:

    // 创建一个Tom类
    // .h文件
    @interface Tom : NSObject
    @property(nonatomic, copy) NSString *name;
    @property(nonatomic, copy) NSString *address;
    @property(nonatomic, assign) NSInteger age;
    - (Tom* (^)(NSString *))nameSet;
    - (Tom* (^)(NSString *))addressSet;
    - (Tom* (^)(NSInteger))ageSet;
    @end
    
    // .m文件
    @implementation Tom
    - (Tom* (^)(NSString *))nameSet {
        return ^Tom *(NSString *name) {
            self.name = name;
            return self;
        };
    }
    
    - (Tom* (^)(NSString *))addressSet {
        return ^Tom *(NSString *address) {
            self.address = address;
            return self;
        };
    }
    
    - (Tom* (^)(NSInteger))ageSet {
        return ^Tom *(NSInteger age) {
            self.age = age;
            return self;
        };
    }
    @end
    

    运行代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Tom *tom = [Tom new];
        tom.nameSet(@"Tom").addressSet(@"唐人街").ageSet(10);
        NSLog(@"name = %@, address = %@, age = %ld", tom.name, tom.address, tom.age);
    }
    

    代码结果:

    name = Tom, address = 唐人街, age = 10
    

    就可以使用点语法连续调用所有的属性,使用更加方便。

    当然,在项目中一般使用宏定义来使用更加简洁,Tom代码如下:

    // .h文件
    // propertyModifier:属性修饰类型,className:类名,propertyPointerType:属性类型,propertyName:属性名称
    #define PropertyAndMethodStatement(propertyModifier, className, propertyPointerType, propertyName) \
    @property(nonatomic,propertyModifier)propertyPointerType propertyName;                            \
    - (className * (^) (propertyPointerType propertyName)) propertyName##Set;
    
    #define MethodImpl(className, propertyPointerType, propertyName)                                  \
    - (className * (^) (propertyPointerType propertyName))propertyName##Set{                          \
    return ^(propertyPointerType propertyName) {                                                      \
    self.propertyName = propertyName;                                                                 \
    return self;                                                                                      \
    };                                                                                                \
    }
    
    @interface Tom : NSObject
    PropertyAndMethodStatement(copy, Tom, NSString*, name)
    PropertyAndMethodStatement(copy, Tom, NSString*, address)
    PropertyAndMethodStatement(assign, Tom, NSInteger, age)
    @end
    
    // .m文件
    @implementation Tom
    MethodImpl(Tom, NSString*, name)
    MethodImpl(Tom, NSString*, address)
    MethodImpl(Tom, NSInteger, age)
    @end
    

    三、Block注意点:

    1、Block变量捕获机制:

    ​ Block拥有捕获外部变量的功能,大体分为三类:

    ​ 1)局部Auto变量:其实就是一般的临时变量,捕获到block内部,会拷贝一份副本,所以前面代码block中打印的num1是0,而不是新赋值的3;

    ​ 2)局部Static变量、全局变量:直接访问,也就是在block中使用时会用最新的赋值

    ​ 3)对象、成员变量或者属性:会捕获,如果用copy修饰会对block进行copy,copy到堆区,block拷贝到堆区的时候会retain其引用的外部变量。

    总结:不使用的不会捕获,使用copy或者strong修饰,在block内使用成员变量或者属性都会捕获self

    2、Block类型:

    ​ 根据block在内存中的位置分为三种类型:

    ​ 1)_ NSGlobalBlock _ :位于全局区的block,在程序的数据区域,也就是.data区;

    ​ 2)_ NSStackBlock _:位于栈区,超出变量作用域,栈上的block会被销毁;

    ​ 3)_ NSMallocBlock _:位于堆区,在变量作用域结束时不受影响;

    如果使用strong修饰,ARC环境会自动copy,在堆上

    注意:几乎所有博客分析block的类型都是使用copy修饰的,然后分析捕获外部变量和未捕获外部变量的情况。所以这里特殊说明:假如block属性使用assign修饰,如果捕获了外部变量,那么block类型是_ NSStackBlock_;如果未捕获外部变量,那么就是_ NSGlobalBlock _;也就是说,使用assign修饰不会造成循环引用,但是超过变量作用域,block会被销毁,此时调用block()会程序崩溃,所以项目中一般使用copy修饰而不是用assign

    下面说明使用copy修饰的block情况:


    block捕获外部变量的类型
    • 在 ARC 中,捕获外部了变量的 block 的类会是 _ NSMallocBlock_ 或者 _ NSStackBlock_,如果 block 被赋值给了某个变量在这个过程中会执行 block copy 将原有的 _ NSStackBlock_ 变成 _ NSMallocBlock_;但是如果 block 没有被赋值给某个变量,那它的类型就是 _ NSStackBlock_;没有捕获外部变量的 block 的类会是 _ NSGlobalBlock_ 即不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。
    • 在非 ARC 中,捕获了外部变量的 block 的类会是 _ NSStackBlock_,放置在栈上,没有捕获外部变量的 block 时与 ARC 环境下情况相同。

    ​上面说明了block三种存储方式:全局、栈、堆,获取block中的ISA指针的值,可以得到上面其中的一个。

    ​ 测试代码如下:

    // .m文件
    @interface ViewController ()
    @property(nonatomic, copy) void (^globalBlock)(NSInteger num); //__NSGlobalBlock__
    // 实际项目开发,不会使用assign,一般使用copy
    @property(nonatomic, assign) void (^stackBlock)(NSInteger num); //__NSStackBlock__
    @property(nonatomic, copy) void (^copyBlock)(NSInteger num); //__NSMallocBlock__
    @property(nonatomic, strong) void (^strongBlock)(NSInteger num); //__NSMallocBlock__
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSInteger num1 = 2;
        _globalBlock = ^(NSInteger num) {
            
        };
        _globalBlock(4);
        NSLog(@"_globalBlock = %@", _globalBlock);
        NSLog(@"-----------------");
        
        _stackBlock = ^(NSInteger num) {
            NSLog(@"sum = %ld", num1 + num);
        };
        _stackBlock(4);
        NSLog(@"_stackBlock = %@", _stackBlock);
        NSLog(@"-----------------");
        
        _copyBlock = ^(NSInteger num) {
            NSLog(@"sum = %ld", num1 + num);
        };
        _copyBlock(4);
        NSLog(@"_copyBlock = %@", _copyBlock);
        NSLog(@"-----------------");
        
        _strongBlock = ^(NSInteger num) {
            NSLog(@"sum = %ld", num1 + num);
        };
        _strongBlock(4);
        NSLog(@"_strongBlock = %@", _strongBlock);
    }
    
    @end
    

    打印结果:

    _globalBlock = <__NSGlobalBlock__: 0x108511468>
    -----------------
    sum = 6
    _stackBlock = <__NSStackBlock__: 0x7ffee77193a0>
    -----------------
    sum = 6
    _copyBlock = <__NSMallocBlock__: 0x600002afc8d0>
    -----------------
    sum = 6
    _strongBlock = <__NSMallocBlock__: 0x600002afcb70>
    
    3、Block引起的循环引用:

    ​ 我们设置block之后,在合适的时间调用block,而不希望回调block的时候block已经被释放了,所以我们需要对block进行copy,copy到堆区,block拷贝到堆区的时候会retain其引用的外部变量,如果block中引用了它的持有对象,那很可能引起循环引用。解决循环引用的三种方法:

    ​ 1)、_ _weak:弱引用,项目中一般使用这个,当对象释放时,weak指针会置为nil;

    ​ 2)、_ unsafe_unretained:不安全指针,当对象释放时, _unsafe_unretained指针不会重置;

    ​ 3)、_ _block:在MRC常用,在ARC不建议通过这个解决循环引用问题;

    代码写法如下:

    // 下面都是ARC模式下的解决方案
    // 第一种_ _weak
    __weak typeof(self) weakSelf = self;
        self.testBlock = ^{
            NSLog(@"%@", weakSelf);
        };
        
    // 第二种_ _unsafe_unretained
    __unsafe_unretained id weakSelf = self;
        self.testBlock = ^{
            NSLog(@"%p", weakSelf);
        };
        
    // 第三种_ _block 不建议使用
    __block id weakSelf = self;
        self.testBlock = ^{
            NSLog(@"%@", weakSelf);
            weakSelf = nil; //必须置为nil
        };
        self.testBlock(); // 必须调用
    

    ​ 在使用weak时会有一个隐患,你不知道self什么时候会被释放,为了保证在block内不被释放,我们添加_ _strong,代码如下:

    // .m文件
    @interface ViewController ()
    @property(nonatomic, copy) void (^testBlock)(void);
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        __weak typeof(self) weakSelf = self;
        self.testBlock = ^{
            __strong __typeof(weakSelf) strongSelf = weakSelf;
            [strongSelf doSomeThing];
        };
        self.testBlock();
    }
    
    - (void)doSomeThing {
        NSLog(@"doSomeThing");
    }
    
    @end
    

    项目中一般使用宏定义,代码如下:

    #ifndef weakify
    #if DEBUG
    #if __has_feature(objc_arc)
    #define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
    #else
    #define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
    #endif
    #else
    #if __has_feature(objc_arc)
    #define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
    #else
    #define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
    #endif
    #endif
    #endif
    
    #ifndef strongify
    #if DEBUG
    #if __has_feature(objc_arc)
    #define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
    #else
    #define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
    #endif
    #else
    #if __has_feature(objc_arc)
    #define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
    #else
    #define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
    #endif
    #endif
    #endif
    
    // 使用
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        @weakify(self);
        self.testBlock = ^{
            @strongify(self);
            [self doSomeThing];
        };
        self.testBlock();
    }
    
    4、是不是所有的block里面的self都要weak:

    ​ 很显然答案是不都需要,很多情况是可以直接使用self的,比如调用UIView动画的block:

    [UIView animateWithDuration:0.5 animations:^{
            NSLog(@"self = %@", self);
    }];
    

    虽然block持有了self,但是self并没有直接或间接持有该block,所以不会造成循环引用。项目中常见不需要weak的还有:Masonry布局框架的block不需要weak,AFNetworking网络请求回调不需要weak等。记住一点:是不是相互持有强引用

    5、_ _block修饰符:

    ​ 前面使用_ _block解决了循环引用问题,这个还可以解决block内部无法修改Auto变量值的问题,但是它不能修饰全局变量和静态变量,代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        __block NSInteger age = 10;
        self.testBlock = ^{
            age = 20;
        };
        NSLog(@"block调用之前 age = %ld", age);
        self.testBlock();
        NSLog(@"block调用之后 age = %ld", age);
    }
    

    打印结果:

    block调用之前 age = 10
    block调用之后 age = 20
    

    觉得写的不错,有些启发或帮助,点个赞哦!

    相关文章

      网友评论

        本文标题:iOS Block

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