02-iOS-OC常用语法-----简介与常规应用|Block

726 阅读9分钟

前言

最近在围绕iOS编程语言(OC&&Swift)的语法 探索iOS的底层原理实现,在即将探索到 OC 面向对象语法中的:Block 之前,先简单对Block的语法特点和使用场景有一个简单的回顾。

一、Block的声明及定义

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

  • return_type 表示返回的对象/关键字等(可以是void,并省略)
  • blockName 表示block的名称
  • var_type 表示参数的类型(可以是void,并省略)
  • varName 表示参数名称

1. 标准声明与定义

return_type (^blockName)(var_type) = ^return_type (var_type varName) {
    // ...
};
blockName(var);

2. 当返回类型为void

void (^blockName)(var_type) = ^void (var_type varName) {
    // ...
};
blockName(var);

可简写成:

void (^blockName)(var_type) = ^(var_type varName) {
    // ...
};
blockName(var);

3. 当参数类型为void

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

可简写成:

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

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

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

可简写成:

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

5. 匿名Block

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

^return_type (var_type varName)
{
    //...
};

二、typedef简化Block的声明

利用typedef简化Block的声明:

  • 声明
typedef return_type (^BlockTypeName)(var_type);
  • 例子1:作属性
//声明
typedef void(^ClickBlock)(NSInteger index);
//block属性
@property (nonatomic, copy) ClickBlock imageClickBlock;
  • 例子2:作方法参数
//声明
typedef void (^handleBlock)();
//block作参数
- (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{
  ...

三、 Block的常见用法

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

  • 位置
return_type (^blockName)(var_type) = ^return_type (var_type varName) {
    // ...
};
blockName(var);
  • 例子
void (^globalBlockInMemory)(int number) = ^(int number){
     printf("%d \n",number);
};
globalBlockInMemory(90);

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

  • 位置
@property(nonatomic, copy)return_type (^blockName) (var_type);
  • 例子
//按钮点击Block
@property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);

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

  • 用法
- (void)yourMethod:(return_type (^)(var_type))blockName;
  • 例子

UIView+AddClickedEvent.h

- (void)addClickedBlock:(void(^)(id obj))clickedAction;

4. 在调用如上方法时,Block作实参

  • 例子

UIView+AddClickedEvent.m

- (void)addClickedBlock:(void(^)(id obj))clickedAction{
    self.clickedAction = clickedAction;
    // :先判断当前是否有交互事件,如果没有的话。。。所有gesture的交互事件都会被添加进gestureRecognizers中
    if (![self gestureRecognizers]) {
        self.userInteractionEnabled = YES;
        // :添加单击事件
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
        [self addGestureRecognizer:tap];
    }
}

- (void)tap{
    if (self.clickedAction) {
        self.clickedAction(self);
    }
}

四、 Block的其它用法

1. Block的内联用法

这种形式并不常用,匿名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框架里面的Case
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            NSMutableArray *children = NSMutableArray.new;
            for (id attr in attribute) {
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.secondViewAttribute = attr;
                [children addObject:viewConstraint];
            }
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            return compositeConstraint;
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

四、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];
};

其实,即使Block不传递任何参数,也可以传递事件的。但这种情况,无法区分事件的激活方(cell里面的哪一个按钮?)。即:

//按钮点击Block
@property (nonatomic, copy) void (^btnClickedBlock)(void);
// 激活事件
#pragma mark - 按钮点击事件
- (IBAction)btnClickedAction:(UIButton *)sender {
    if (self.btnClickedBlock) {
        self.btnClickedBlock();
    }
}
// 响应事件
cell.btnClickedBlock = ^{
    //标记消息已读
    [weakSelf requestToReadedMessageWithTag:nil];
    //刷新当前cell
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};

2 逆向传值

上面的响应事件,其实也是传递数据,只是它传递的对象是UIButton。如下所示,SubTableView是VC的一个属性和子视图。

  • 传递数值

SubTableView.h

@property (strong, nonatomic) void (^handleDidSelectedItem)(int indexPath);

SubTableView.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    _handleDidSelectedItem ? _handleDidSelectedItem(indexPath) : NULL;
}

VC.m

[_subView setHandleDidSelectedItem:^(int indexPath) {
        [weakself handleLabelDidSearchTableSelectedItem:indexPath];
    }];
- (void)handleLabelDidSearchTableSelectedItem:(int )indexPath {
    if (indexPath==0) {
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"telprompt:%@", self.searchNullView.telLabel.text]]];
    }else if (indexPath==1){
        [self.navigationController popViewControllerAnimated:YES];
    }
}
  • 传递对象

例如HPNetworking网络框架中请求成功时传递接口返回数据对象的Block:

[HPNetworking 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的形式返回,这样就可以实现了连续调用,即为链式编程。

Masonry的一个典型的链式编程用法如下:

[self.containerView addSubview:self.bannerView];
[self.bannerView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.equalTo(self.containerView.mas_leading);
    make.top.equalTo(self.containerView.mas_top);
    make.trailing.equalTo(self.containerView.mas_trailing);
    make.height.equalTo(@(kViewWidth(131.0)));
}];

现在,简单使用链式编程思想实现一个简单计算器的功能:

3.1 在CaculateMaker.h文件中声明一个方法add:

  • CaculateMaker.h
//  CaculateMaker.h
//  ChainBlockTestApp

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface CaculateMaker : NSObject

@property (nonatomic, assign) CGFloat result;

- (CaculateMaker *(^)(CGFloat num))add;

@end

3.2 在CaculateMaker.m文件中实现add方法:

  • CaculateMaker.m
//  CaculateMaker.m
//  ChainBlockTestApp


#import "CaculateMaker.h"

@implementation CaculateMaker

- (CaculateMaker *(^)(CGFloat num))add;{
    return ^CaculateMaker *(CGFloat num){
        _result += num;
        return self;
    };
}

@end

3.3 在viewController里面导入CaculateMaker.h文件,然后调用add方法就完成了链式语法:

  • ViewController.m
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);

五、 Block使用注意

1. 截获自动变量与__block说明符

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

  • 不能修改自动变量的值是因为:block捕获的是自动变量的const值,名字一样,不能修改
  • 可以修改静态变量的值:静态变量属于类的,不是某一个变量。由于block内部不用调用self指针。所以block可以调用。

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

2. 截获对象

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

@interface MyClass : NSObject {  
    NSObject* _instanceObj;  
}  
@end  
  
@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];  
  
    typedef void (^MyBlock)(void) ;  
    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  
  
int main(int argc, charchar *argv[]) {  
    @autoreleasepool {  
        MyClass* obj = [[[MyClass alloc] init] autorelease];  
        [obj test];  
        return 0;  
    }  
}  

执行结果为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

3. Block引起的循环引用

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

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

  • TestCycleRetain
- (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");
}
  • main
int main(int argc, char * argv[]) {
    @autoreleasepool {
        TestCycleRetain * obj = [[TestCycleRetain alloc] init];
        obj = nil;
        return 0;
    }
}
  • 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. 实用宏定义:避免Block引起循环引用

  • 第一步

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

//----------------------强弱引用----------------------------
#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。

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

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

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

  • View+MASAdditions.m
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
  • MASConstraintMaker.m
- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    self.constraints = NSMutableArray.new;
    
    return self;
}

持有链是这样的,并没有形成引用循环:

self ->self.headView ··· MASConstraintMaker构造block->self

注意观察,这个作为方法参数的Block体并没有被任何方持有。因此,我们放心在Masonry中使用self.xxx 不会循环引用的。而且这个block里面用weakSelf还有可能会出问题,因为mas_qeual如果得到一个nil参数的话应该会导致程序崩溃。

因为UIView未强持有block,所以这个block只是个栈block,而且构不成循环引用的条件。栈block有个特性就是它执行完毕之后就出栈,出栈了就会被释放掉。看mas_makexxx的方法实现会发现这个block很快就被调用了,完事儿就出栈销毁,构不成循环引用,所以可以直接放心的使self。另外,这个与网络请求里面使用self道理是一样的。