一、Block的定义
约定:用法中的符号含义列举如下:
- return_type 表示返回的对象/关键字等(可以是void,并省略)
- block_name 表示block的名称
- var_type 表示参数的类型(可以是void,并省略)
- var_name 表示参数名称
1、Block声明及定义
//定义
return_type (^ blockname)(var_type) = ^return_type (var_type var_name){
};
//调用
block_name(var);
//返回类型为void
void (^block_name)(var_type) = ^void (var_type var_name){
};
//可以简写
void (^block_name)(var_type) = ^(var_type var_name){
};
//参数为void
return_type (^block_name)(void)= ^ return_type(void){
};
//可以简写
return_type (^block_name)(void)= ^ return_type{
};
//返回值和参数都为void
void (^block_name)(void)= ^void(void){
};
//可以简写
void (^block_name)(void)= ^{
};
2、使用typedef来声明Block
typedef return_type (^BlockTypeName)(var_type);
(1)用作属性
//声明
typedef void(^CompleteBlock)(Bool error,id response);
//block属性
@property (nonatomic, copy) CompleteBlock networkCompleteBlock;
(2)用作参数
//声明
typedef void (^ConfigBlock)(Config *config);
//block作参数
- (void)setNetworkConfig:(ConfigBlock)configblock {
};
3、Block用法
(1)局部位置声明一个Block型的变量
void (^globalBlockInMemory)(int number) = ^(int number){
printf("%d \n",number);
};
globalBlockInMemory(90);
(2)@interface位置声明一个Block型的属性
//按钮点击Block
@property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);
(3)在定义方法时,声明Block型的形参
- (void)addClickedBlock:(void(^)(id obj))clickedAction;
4、Block中少见用法
(1)Block的内联用法
^return_type (var_type varName)
{
//...
}(var);
(2)Block的递归调用
Block内部调用自身,递归调用是很多算法基础,特别是在无法提前预知循环终止条件的情况下。注意:由于Block内部引用了自身,这里必须使用__block避免循环引用问题。
__block return_type (^blockName)(var_type) = [^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) {
// ...
};
}
Masonry框架里面的:
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
二、Block的应用
1、响应事件
情景:UIViewContoller有个UITableView并是它的代理,通过UITableView加载CellView。现在需要监听CellView中的某个按钮(可以通过tag值区分),并作出响应。
在CellView.h中@interface位置声明一个Block型的属性,为了设置激活事件调用Block,接着我们在CellView.m中作如下设置:
// 激活事件
#pragma mark - 按钮点击事件
- (IBAction)btnClickedAction:(UIButton *)sender {
if (self.btnClickedBlock) {
self.btnClickedBlock(sender);
}
}
随后,在ViewController.m的适当位置(- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{...代理方法)中通过setter方法设置CellView的Block属性。Block写着当按钮被点击后要执行的逻辑。
// 响应事件
cell.btnClickedBlock = ^(UIButton *sender) {
//标记消息已读
[weakSelf requestToReadedMessageWithTag:sender.tag];
//刷新当前cell
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};
2、传递数据
例如HYBNetworking网络框架中请求成功时传递接口返回数据对象的Block:
[HYBNetworking postWithUrl:kSearchProblem refreshCache:NO params:params success:^(id response) {
typeof(weakSelf) strongSelf = weakSelf;
// [KVNProgress dismiss];
NSString *stringData = [response mj_JSONString];
stringData = [DES3Util decrypt:stringData];
NSLog(@"stirngData: %@", stringData);
...
}
3、链式语法
链式编程思想:核心思想为将block作为方法的返回值,且返回值的类型为调用者本身,并将该方法以setter的形式返回,这样就可以实现了连续调用,即为链式编程。
简单使用链式编程思想实现一个简单计算器的功能:
// CaculateMaker.h
// ChainBlockTestApp
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface CaculateMaker : NSObject
@property (nonatomic, assign) CGFloat result;
- (CaculateMaker *(^)(CGFloat num))add;
@end
// CaculateMaker.m
// ChainBlockTestApp
#import "CaculateMaker.h"
@implementation CaculateMaker
- (CaculateMaker *(^)(CGFloat num))add;{
return ^CaculateMaker *(CGFloat num){
_result += num;
return self;
};
}
@end
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);
三、Block使用注意事项
1、截获基本类型局部变量与_ _block修饰符
先来看一段代码:
int c = 10;
static int d = 10;
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10;
static int b = 10;
void (^CatchVarBlock)(void) = ^{
NSLog(@"%d",a);
//编译报错
// a = 30;
b = 30;
NSLog(@"%d",b);
c = 30;
NSLog(@"%d",c);
d = 30;
NSLog(@"%d",d);
};
a = 20;
b = 20;
c = 20;
d = 20;
CatchVarBlock();
}
打印结果:
2019-11-19 20:31:25.971462+0800 TestApp[3613:151099] 10
2019-11-19 20:31:25.971589+0800 TestApp[3613:151099] 30
2019-11-19 20:31:25.971687+0800 TestApp[3613:151099] 30
2019-11-19 20:31:25.971773+0800 TestApp[3613:151099] 30
(1)block所在函数中的,捕获局部变量。但是不能修改它,不然就是“编译错误”。
(2)可以改变全局变量、静态变量、全局静态变量。
- 不能修改自动变量的值是因为:block捕获的是自动变量的const值,名字一样,不能修改。
- 可以修改静态变量的值:静态变量属于类的,不是某一个变量。由于block内部不用调用self指针。所以block可以调用。
解决block不能修改自动变量的值,这一问题的另外一个办法是使用__block修饰符。
2、截获OC对象
不同于基本类型,对应截获OC对象,Block会使得对象的引用计数加1。
@interface ViewController (){
NSObject *instanceObj;
}
@end
NSObject *globalObj = nil;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
instanceObj = [[NSObject alloc] init];
globalObj = [[NSObject alloc] init];
static NSObject *staticObj = nil;
staticObj = [[NSObject alloc] init];
NSObject *localObj = [[NSObject alloc] init];
__block NSObject *blockObj = [[NSObject alloc] init];
typedef void (^TestBlock)(void);
TestBlock testBlock1 = ^{
NSLog(@"%@", globalObj);
NSLog(@"%@", staticObj);
NSLog(@"%@", self->instanceObj);
NSLog(@"%@", localObj);
NSLog(@"%@", blockObj);
};
testBlock1();
NSLog(@"%d", [[globalObj valueForKey:@"retainCount"] intValue]);
NSLog(@"%d", [[staticObj valueForKey:@"retainCount"]intValue]);
NSLog(@"%d", [[instanceObj valueForKey:@"retainCount"]intValue]);
NSLog(@"%d", [[localObj valueForKey:@"retainCount"]intValue]);
NSLog(@"%d", [[blockObj valueForKey:@"retainCount"]intValue]);
}
打印结果:11121
总结:globalObj和staticObj在内存中的位置是确定的,所以Block copy时不会retain对象。
instanceObj在Block copy时也没有直接retain instanceObj对象本身,但会retain self。所以在Block中可以直接读写instanceObj变量。
localObj在Block copy时,系统自动retain对象,增加其引用计数。
blockObj在Block copy时也不会retain。
3、Block中的循环引用
一般来说我们总会在设置Block之后,在合适的时间回调Block,而不希望回调Block的时候Block已经被释放了,所以我们需要对Block进行copy,copy到堆中,以便后用。
Block可能会导致循环引用问题,因为block在拷贝到堆上的时候,会retain其引用的外部变量,那么如果block中如果引用了他的宿主对象,那很有可能引起循环引用,如:
- (void) dealloc {
NSLog(@"no cycle retain");
}
- (id) init {
self = [super init];
if (self) {
#if TestCycleRetainCase1
//会循环引用
self.myblock = ^{
[self doSomething];
};
#elif TestCycleRetainCase2
//会循环引用
__block TestCycleRetain * weakSelf = self;
self.myblock = ^{
[weakSelf doSomething];
};
#elif TestCycleRetainCase3
//不会循环引用
__weak TestCycleRetain * weakSelf = self;
self.myblock = ^{
[weakSelf doSomething];
};
#elif TestCycleRetainCase4
//不会循环引用
__unsafe_unretained TestCycleRetain * weakSelf = self;
self.myblock = ^{
[weakSelf doSomething];
};
#endif NSLog(@"myblock is %@", self.myblock);
}
return self;
}
- (void) doSomething {
NSLog(@"do Something");
}
- MRC情况下,用__block可以消除循环引用。
- ARC情况下,必须用弱引用才可以解决循环引用问题,iOS 5之后可以直接使用__weak,之前则只能使用__unsafe_unretained了,__unsafe_unretained缺点是指针释放后自己不会置空。
在上述使用 block中,虽说使用__weak,但是此处会有一个隐患,你不知道 self 什么时候会被释放,为了保证在block内不会被释放,我们添加__strong。更多的时候需要配合strongSelf使用,如下:
__weak __typeof(self) weakSelf = self;
self.testBlock = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf test];
});
4、使用宏定义:避免循环引用
//----------------------强弱引用----------------------------
#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
在设置Block体的时候,像如下这样使用即可。
@weakify(self);
[footerView setClickFooterBlock:^{
@strongify(self);
[self handleClickFooterActionWithSectionTag:section];
}];
5、所有的Block里面的self必须要weak一下?
很显然答案不都是,有些情况下是可以直接使用self的,比如调用系统的方法:
[UIView animateWithDuration:0.5 animations:^{
NSLog(@"%@", self);
}];
因为这个block存在于静态方法中,虽然block对self强引用着,但是self却不持有这个静态方法,所以完全可以在block内部使用self。并不是 block 就一定会造成循环引用,是不是循环引用要看是不是相互持有强引用。
四、Block于内存管理
1、Block的三种类型
根据Block在内存中的位置分为三种类型:
- NSGlobalBlock是位于全局区的block,它是设置在程序的数据区域(.data区)中。
- NSStackBlock是位于栈区,超出变量作用域,栈上的Block以及 __block变量都被销毁。
- NSMallocBlock是位于堆区,在变量作用域结束时不受影响。
注意:在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
正如它们名字显示得一样,表明了block的三种存储方式:栈、全局、堆。获取block对象中的isa的值,可以得到上面其中一个,下面开始说明哪种block存储在栈、堆、全局。
(1)全局区:GlobalBlock
生成在全局区block有两种情况:
- 定义全局变量的地方有block语法时
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
}
- block语法的表达式中没有使用截获的变量时
int(^block)(int count) = ^(int count) {
return count;
};
block(2);
(2)栈内存:StackBlock
这种情况,在非ARC下是无法编译的,在ARC下可以编译。
- block语法的表达式中使用截获的自动变量时
NSInteger i = 10;
block = ^{
NSLog(@"%ld", i);
};
block();
设置在栈上的block,如果其作用域结束,该block就被销毁。同样的,由于__block变量也配置在栈上,如果其作用域结束,则该__block变量也会被销毁。
另外,例如:
typedef void (^block_t)() ;
-(block_t)returnBlock{
__block int add=10;
return ^{
printf("add=%d\n",++add);
};
}
(3)堆内存:MallocBlock
堆中的block无法直接创建,其需要由_NSConcreteStackBlock类型的block拷贝而来(也就是说block需要执行copy之后才能存放到堆中)。由于block的拷贝最终都会调用_Block_copy_internal函数。
void(^block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSInteger i = 10;
block = [^{
++i;
} copy];
++i;
block();
NSLog(@"%ld", i);
}
return 0;
}
我们对这个生成在栈上的block执行了copy操作,Block和__block变量均从栈复制到堆上。上面的代码,有跟没有copy,在非ARC和ARC下一个是stack一个是Malloc。这是因为ARC下默认为Malloc(即使如此,ARC下还是有一些例外,下面会讲)。
block在ARC和非ARC下有巨大差别。多数情况下,ARC下会默认把栈block被会直接拷贝生成到堆上。那么,什么时候栈上的Block会复制到堆上呢?
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 将方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时
block在ARC和非ARC下的巨大差别
-
在 ARC 中,捕获外部了变量的 block 的类会是 NSMallocBlock 或者 NSStackBlock,如果 block 被赋值给了某个变量,在这个过程中会执行 _Block_copy 将原有的 NSStackBlock 变成 NSMallocBlock;但是如果 block 没有被赋值给某个变量,那它的类型就是 NSStackBlock;没有捕获外部变量的 block 的类会是 NSGlobalBlock 即不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。
-
在非 ARC 中,捕获了外部变量的 block 的类会是 NSStackBlock,放置在栈上,没有捕获外部变量的 block 时与 ARC 环境下情况相同。
- 无论当前环境是ARC还是MRC,只要block没有访问外部变量,block始终在全局区
- MRC情况下
- block如果访问外部变量,block在栈里
- 不能对block使用retain,否则不能保存在堆里
- 只有使用copy,才能放到堆里
- ARC情况下
- block如果访问外部变量,block在堆里
- block可以使用copy和strong,并且block是一个对象
2、Block的复制
- 在全局block调用copy什么也不做
- 在栈上调用copy那么复制到堆上
- 在堆上调用block 引用计数增加
不管block配置在何处,用copy方法复制都不会引起任何问题。在ARC环境下,如果不确定是否要copy这个block,那尽管copy即可。
最后的强调,在 ARC 开启的情况下,除非上面的例外,默认只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
五、Block的底层原理
通过clang -rewrite-objc main.m命令,来来编译一下block的文件:
#include <stdio.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
typedef void (^blk_t)(void);
blk_t block = ^{
printf("Hello, World!\n");
};
block();
// return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
这里只选取部分关键代码。
不难看出int main(int argc, char * argv[]) {就是主函数的实现。
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
typedef void (*blk_t)(void);
blk_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
}
其中,__main_block_impl_0是block的一个C++的实现(最后面的_0代表是main中的第几个block),也就是说也是一个结构体。
(1) __main_block_impl_0
__main_block_impl_0定义如下:
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;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
(2) __block_impl
如上,其中__block_impl的定义如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
其结构体成员如下:
isa,指向所属类的指针,也就是block的类型
Flags,标志变量,在实现block的内部操作时会用到
Reserved,保留变量
FuncPtr,block执行时调用的函数指针
可以看出,它包含了isa指针(包含isa指针的皆为对象),也就是说block也是一个对象(runtime里面,对象和类都是用结构体表示)。
(3) __main_block_desc_0
__main_block_desc_0的定义如下:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
其结构成员含义如下:
reserved:保留字段
Block_size:block大小(sizeof(struct __main_block_impl_0))
以上代码在定义__main_block_desc_0结构体时,同时创建了__main_block_desc_0_DATA,并给它赋值,以供在main函数中对__main_block_impl_0进行初始化。
(4) __main_block_func_0
如上的main函数中,__main_block_func_0也是block的一个C++的实现:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello, World!\n");
}
总结:
- __main_block_impl_0的 isa 指针指向了_NSConcreteStackBlock。
- 从main函数的main.cpp中看,__main_block_impl_0的 FuncPtr 指向了函数__main_block_func_0。
- __main_block_impl_0的 Desc 也指向了定义__main_block_desc_0时就创建的__main_block_desc_0_DATA,其中纪录了block结构体大小等信息。
block的变量传递
- 如果block访问的外部变量是局部变量,那么就是值传递,外界改了,不会影响里面
- 如果block访问的外部变量是__block或者static修饰,或者是全局变量,那么就是指针传递,block里面的值和外界同一个变量,外界改变,里面也会改变
- 验证一下是不是这样
- 通过Clang来将main.m文件编译为C++
- 在终端输入如下命令clang -rewrite-objc main.m
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
网友评论