浅谈Objectiv-C中的Block

前言

由于“业务”需求,最近我准备复习下Objective-C中的“块”,也就是大家并不陌生的block,顺便做点小笔记。说实话,其实自己之前对block不是很熟悉,所以这篇文章主要是基础知识,不会涉及太多深层次的东西,个人认为给自己也就是刚接触iOS开发的新手看看还是有作用的。

理解Block

在《Learning Cocoa with Objective-C》这本书中说我们可以把block看作是一个变量(variables),但这个变量和一般的变量有一点不同,它不像别的变量可能存储一个整型数或者字符串,它存储的是一段代码。我觉得这个理解很不错,以后我们遇到了block,不妨将block看作一个变量

如何看懂一个block?

要读懂,我们首先要会写block。block的声明大致可以分为下面五种形式。

1.block作为局部变量

returnType (^blockName)(parameterTypes) = ^returnType(parameters) {code};

这种形式可以看作最基本的block声明形式,最好动手在自己的Xcode中敲一敲。

2.block作为接口部分属性指令

@property (nonatomic, copy) returnType (^blockName)(parameterTypes);

3.block作为某个方法的形式参数

-(void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;

4.block作为一个被调用的方法

[someObject someMethodThatTakesABlock:^returnType (parameters) {code}];

5.宏定义

typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};

看完后相信读者和笔者都有一个感觉就是OC中得block非常反人类。但是我们还是可以做一些总结,比如任何情况下block的返回类型都不会单独在一对括号里;block所需要的形参都是单独在括号中的且括号之前没有^符号。^符号可以说是block的标志,凡是看到^符号就可以认为这是一个block,且括号内的一般式参数类型或者参数名,极少数情况会是block的名字(一般很少情况下会必须给block起名字,所以很多block是没有名字的)。至于block的调用方法是与函数的调用方法一致的。

这么复杂的定义为何我们还要去学呢?因为block的作用其实非强大。block可以说是OC中实现函数式编程的基础。当我们写一个动画的过程时需要使用block;当我们需要回调网络请求的时候需要block;当我们需要更新UI的时候我们需要block。

block是如何运行的?

我们先来看一个简单的demo。

//声明一个block 
void (^block)() = ^void (){     //Step.1
    NSLog(@"1");    //Step.3
};
//调用block 
block();        //Step.2

输出很简单就是一个1.为什么要举这个demo呢?我们不妨打个断点来看看。令人惊奇的是,在遇到block的时候我们的代码不是从上往下运行了,当执行到Step.1时函数不再往下执行NSLog方法而是到了Step.2,而Step.2调用了block函数此时才执行块中得NSLog也就是Step.3。

所以上文就说了,我们应该把block看作是一个变量。当我们定义了一个int x的时候编译程序不会在乎你x的值是什么,而当你要用x做输出或者加减法的时候编译程序才会在内存空间中取出x进行运算。block也是这个道理。

但我们也不能把Block简单的当做是一个变量,我们来看下一个demo;

int x = 10;

void (^block)() = ^void (){
    NSLog(@"%d", x);
};

block();
x = 11;
block();

结果两个调用block输出都是10。这就是我们常说的闭包

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。

可以认为,在调用block内部函数时,里面的变量值就是block创建时变量的值。block可以看作是一个封闭的块,一个“不受外界干扰”的块。

在我们的实际开发中,我们就是要利用block的回调特性和闭包特性。

block的应用

1.网络请求

在网络请求中,block主要是处理网络请求的返回值。一个网络请求不仅有返回数据的情况,也有超时或者链接错误等失败情况,这时我们就需要block。

[SomeClass getURL:someURL finished:^(NSData * data,NSURLResponse *response,NSError *error) {
        if (error) {
        ...
    }else{
        if (success) {
            ...
        }
    }
}];

为什么这里要用block呢?这里偷懒引用下美团的原文。(其实是怕我自己的理解有误)

这里网络请求是异步的,所以当block中代码执行时,getURL:finished:方法调用所在的栈很可能已经不存在了,但是因为回调block和someURL构成了closure,所以即使栈不存在,block仍然可以引用到someURL。

可能你会说,“我在block中增加一个NSURL类型的参数,把someURL传回来不也可以实现同样的目的吗?”不妨设想如果我们在block中要引用的对象有10个之多,用参数列表传递明显不再现实,用容器类或者专门定义一个类来传递虽然可以,但是前者没有编译器为我们检查错误,后者则相当繁琐。而利用闭包,可以轻易达到灵活性和简洁性的平衡。事实上,美团客户端就大量利用了闭包,在UI层发出请求,在回调中更新某些UI组件。

而再获取了数据之后,我们还可以直接在块中进行操作,例如更新UI,例如存储model,甚至错误的弹框。

2.动画

UIView *ppview = [[UIView alloc] initWithFrame:CGRectMake(0, SCREENHEIGHT/2-50, 50, 50)];
ppview.backgroundColor = [UIColor yellowColor];
[self.view addSubview:ppview];
[UIView animateWithDuration:1.0f animations:^{
    ppview.frame = CGRectMake(SCREENWIDTH, SCREENHEIGHT/2-50, 50, 50);
} completion:^(BOOL finished) {
    ppview.frame = CGRectMake(SCREENWIDTH/2-25, SCREENHEIGHT/2-50, 50, 50);
}];

这里为了使代码看起来简洁用了宏定义,所以代码前要加两句话

#define SCREENWIDTH [UIScreen mainScreen].bounds.size.width
#define SCREENHEIGHT [UIScreen mainScreen].bounds.size.height

动画的具体效果大家可以自己看,这不是重点。动画部分主要利用了块可以存储代码这一特性,使得动画的代码更简洁易懂。试想若不用block,先不管能不能实现动画效果,每个动画添加一个方法,可能一个简单的动画将有大量的代码。

总结

写这篇文章深深感受到自己的基础知识的不够用,其实自己写文的初衷也是想自己踏踏实实的学习iOS开发,写文对知识的总结还是很有效果的。

由于理解不到位,block的应用那一节可能说的不是很好,希望有错的部分大家一定要帮我指出来,我一定虚心接受。

参考文章

How Do I Declare A Block in Objective-C?

block在美团iOS的实践