iOS Block详细用法

2018-08-30  本文已影响8人  ikonan

Block:带有自动变量(局部变量)的匿名函数。它是C语言的扩充功能。之所以是拓展,是因为C语言不允许存在这样匿名函数。

匿名函数

匿名函数是指不带函数名称函数。C语言中,函数是怎样的呢?类似这样:

int func(int count);

//调用的时候:
int result = func(10);

func就是它的函数名。也可以通过指针调用函数,看起来没用到函数名

int (*funcptr)(int) = &func;
int result = (*funcptr)(10);

而通过Block,就能够使用匿名函数,即不带函数名称的函数。

带有自动变量

关于“带有自动变量(局部变量)”的含义,这是因为Block拥有捕获外部变量的功能。在Block中访问一个外部的局部变量,Block会持用它的临时状态,自动捕获变量值,外部局部变量的变化不会影响它的的状态。

捕获外部变量,看一个经典block面试题:

int val = 10;
void (^blk)(void) = ^ {
    NSLog(@">>>>:%@",@(val));
};
val = 2;
blk();

上面这段代码,输出值是:val = 10,而不是2。

block 在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,然后在 block 块内使用该只读拷贝;换句话说block截获自动变量的瞬时值;或者block捕获的是自动变量的副本。

由于block捕获了自动变量的瞬时值,所以在执行block语法后,即使改写block中使用的自动变量的值也不会影响block执行时自动变量的值。

所以,上面的面试题的结果是2不是10。

解决block不能修改自动变量的值,这一问题的另外一个办法是使用__block修饰符。

__block int val = 10;
void (^blk)(void) = ^ {
    NSLog(@">>>>:%@",@(val));
};
val = 2;
blk();

上面的代码,跟第一个代码段相比只是多了一个__block修饰符。但是输出结果确是2。

Block语法

约定: 用法中的符号含义列举如下:

Block声明及定义语法,及其变形

标准声名与定义:

returnType(^blockName)(varType) = ^returnType(varType varName) {
  //...
}
blockName(var);

当返回类型为void

void(^blockName)(varType) = ^void(varType varName) {
  //...
}

//可以略写
void(^blockName)(varType) = ^(varType varName) {
  //...
}

当参数类型为void

returnType(^blockName)(void) = ^returnType(void) {
  //...
}

//可以略写
returnType(^blockName)(void) = ^returnType {
  //...
}

当返回类型和参数类型都为void

void(^blockName)(void) = ^void(void) {
  //...
}

//可以略写
void(^blockName)(void) = ^ {
  //...
}

匿名Block
Block实现时,等号右边就是一个匿名Block,它没有blockName,称之为匿名Block:

^returnType (varType varName) {
};
typedef简化Block的声明

用typedef简化Block的声明:

typedef returnType(^BlockTypeName)(varType);

举例

//作为属性
typedef void(^ClickBlock)(NSInteger index);
@property (nonatomic, copy) ClickBlock imageClickBlock;

//作为方法参数
typedef void (^handleBlock)(); 
- (void)requestForRefuseOrAccept: handle:(handleBlock)handle {
    //...
}
Block常见用法

局部位置声明一个Block型的变量

returnType (^blockName)(varType) = ^returnType (varType varName) { 
    // ... 
};

//返回类型为空可以省略
void(^globalBlockInMemory)(int) = ^(int number) {
    //...
};
globalBlockInMemory(90);

@interface位置声明一个Block型的属性

//此时的参数名(varName)不能省略
@property(nonatomic, copy)returnTtype (^blockName) (varType varName);

//例子
@property (nonatomic, copy) void(^btnClickedBlock)(UIButton *sender);

在定义方法时,声明Block型的形参

- (void)funcName:(returnType (^)(varType))blockName;

//例子
- (void)addClickedBlock:(void(^)(id obj)) clickedBlock;

Block的少见用法

Block的内联用法
这种形式并不常用,匿名Block声明后立即被调用:

^returnType (varType varName){
     //... 
}(var);


//例子
int sum = ^int(int x, int y){
    return x + y;
}(10,9);
Block的递归调用

Block内部调用自身,递归调用是很多算法基础,特别是在无法提前预知循环终止条件的情况下。注意:由于Block内部引用了自身,这里必须使用__block避免循环引用问题。

//递归求和例子
__block int(^sumBlock)(int) = [^int (int num) {
    if (num < 1) {
        sumBlock = nil;
        return num;
    }
    return num + sumBlock(num-1);    
} copy];
Block作为返回值

方法的返回值是一个Block,可用于一些链式编程上:


#import <Foundation/Foundation.h>

@interface CalculatorManager : NSObject
@property(assign, nonatomic) NSInteger result;
- (CalculatorManager *(^)(NSInteger))add;

@end


#import "CalculatorManager.h"
@implementation CalculatorManager

- (CalculatorManager *(^)(NSInteger))add {
    
    return ^CalculatorManager *(NSInteger value) {
        self.result += value;
        return self;
    };
}

@end

//使用:
CalculatorManager *manager = [[CalculatorManager alloc] init];
manager.add(1).add(2).add(3);
NSLog(@">>>>result:%@",@(manager.result));


//运行结果
>>>>result:6

Block返回值实现高阶函数(map,filter),请看我的另外一篇文章 Block高阶函数(map,filter)

Block应用场景

情景: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];
};

Block使用注意

截获自动变量与__block说明符

前面讲过block所在函数中的,捕获自动变量。但是不能修改它,不然就是“编译错误”。但是可以改变全局变量、静态变量、全局静态变量。其实这两个特点不难理解:

解决block不能修改自动变量的值,这一问题的另外一个办法是使用__block修饰符。

截获对象

对于捕获ObjC对象,不同于基本类型;Block会引起对象的引用计数变化。

#import <Foundation/Foundation.h>
@interface MyClass : NSObject {
     NSObject *_instanceObj;
}
- (void)test;
@end


#import "MyClass.h"
typedef void (^MyBlock)(void) ;
@implementation MyClass

NSObject *__globalObj = nil;
- (id)init {
    if (self = [super init]) {
        _instanceObj = [[NSObject alloc] init];
    }
    return self;
}
    
- (void)test {
    static NSObject* __staticObj = nil;
    __globalObj = [[NSObject alloc] init];
    __staticObj = [[NSObject alloc] init];
    NSObject* localObj = [[NSObject alloc] init];
    __block NSObject* blockObj = [[NSObject alloc] init];
    

    
    MyBlock aBlock = ^{
        NSLog(@"%@", __globalObj);
        NSLog(@"%@", __staticObj);
        NSLog(@"%@", _instanceObj);
        NSLog(@"%@", localObj);
        NSLog(@"%@", blockObj);
    };
    
    
    aBlock = [[aBlock copy] autorelease];
    aBlock();
    NSLog(@"%d", [__globalObj retainCount]);
    NSLog(@"%d", [__staticObj retainCount]);
    NSLog(@"%d", [_instanceObj retainCount]);
    NSLog(@"%d", [localObj retainCount]);
    NSLog(@"%d", [blockObj retainCount]);
}
@end


#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "MyClass.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyClass* obj = [[MyClass alloc] init];
        [obj test];
    }
}

//运行结果
2018-08-30 15:58:11.551895+0800 BlockDemo[4354:2555646] 1
2018-08-30 15:58:11.551912+0800 BlockDemo[4354:2555646] 1
2018-08-30 15:58:11.551920+0800 BlockDemo[4354:2555646] 1
2018-08-30 15:58:11.551927+0800 BlockDemo[4354:2555646] 2
2018-08-30 15:58:11.551934+0800 BlockDemo[4354:2555646] 1

执行结果为1 1 1 2 1。

__globalObj和__staticObj在内存中的位置是确定的,所以Block copy时不会retain对象。

_instanceObj在Block copy时也没有直接retain _instanceObj对象本身,但会retain self。所以在Block中可以直接读写_instanceObj变量。

localObj在Block copy时,系统自动retain对象,增加其引用计数。
blockObj在Block copy时也不会retain。

Block引起的循环引用

一般来说我们总会在设置Block之后,在合适的时间回调Block,而不希望回调Block的时候Block已经被释放了,所以我们需要对Block进行copy,copy到堆中,以便后用。

Block可能会导致循环引用问题,因为block在拷贝到堆上的时候,会retain其引用的外部变量,那么如果block中如果引用了他的宿主对象,那很有可能引起循环引用,

在上述使用 block中,虽说使用__weak,但是此处会有一个隐患,你不知道 self 什么时候会被释放,为了保证在block内不会被释放,我们添加__strong。更多的时候需要配合strongSelf使用,如下:

__weak __typeof(self) weakSelf = self; self.testBlock =  ^{
       __strong __typeof(weakSelf) strongSelf = weakSelf;
       [strongSelf test]; 
});
实用宏定义:避免Block引起循环引用

在工程的TestAPP-Prefix.pch的文件中直接(不推荐)或在其导入的头文件中间接写入以下宏定义:

#define weakObj(o) __weak typeof(o) weak##o = o;
#define strongObj(o) __strong typeof(weak##o) strong##o = weak##o;

在设置Block体的时候,像如下这样使用即可。

@weakObj(self);
[footerView setClickFooterBlock:^{
    @strongObj(self);
    [self handleClickFooterActionWithSectionTag:section];
}];
所有的Block里面的self必须要weak一下?

很显然答案不都是,有些情况下是可以直接使用self的,比如调用系统的方法:

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

因为这个block存在于静态方法中,虽然block对self强引用着,但是self却不持有这个静态方法,所以完全可以在block内部使用self。

另外,来看一个Masonry代码布局的例子,这里面的self会不会造成循环引用呢?

[self.headView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerY.equalTo(self.otherView.mas_centerY);
}];

并不是 block 就一定会造成循环引用,是不是循环引用要看是不是相互持有强引用。block 里用到了 self,那 block 会保持一个 self 的引用,但是 self 并没有直接或者间接持有 block,所以不会造成循环引用。

下一篇 KVC的使用技巧

上一篇下一篇

猜你喜欢

热点阅读