iOS技术图谱之Block

一、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 = https://www.it610.com/article/[response mj_JSONString]; stringData = [DES3Util decrypt:stringData]; NSLog(@"stirngData: %@", stringData); ... }

3、链式语法
链式编程思想:核心思想为将block作为方法的返回值,且返回值的类型为调用者本身,并将该方法以setter的形式返回,这样就可以实现了连续调用,即为链式编程。
简单使用链式编程思想实现一个简单计算器的功能:
//CaculateMaker.h //ChainBlockTestApp#import #import @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; } @endNSObject *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 引用计数增加
【iOS技术图谱之Block】不管block配置在何处,用copy方法复制都不会引起任何问题。在ARC环境下,如果不确定是否要copy这个block,那尽管copy即可。
最后的强调,在 ARC 开启的情况下,除非上面的例外,默认只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
五、Block的底层原理 通过clang -rewrite-objc main.m命令,来来编译一下block的文件:
#include 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 = https://www.it610.com/article/{ 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));

可以看到在编译后的代码最后可以发现被__block修饰过得变量使用的是&a,而局部变量是a

    推荐阅读