iOS开发Block

释放双眼,带上耳机,听听看~!

​ 前言: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文章

iOS 修改键盘背景颜色

2020-2-7 16:22:54

iOS文章

iOS 生产者消费者

2020-2-7 17:00:59

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索