iOS技术

iOS Block

2019-09-30  本文已影响0人  阿饼six

​ 前言: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捕获外部变量的类型

​上面说明了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

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

上一篇下一篇

猜你喜欢

热点阅读