您的位置:首页 > 移动开发 > Objective-C

Programming with Objective-C(六)

2016-05-18 16:15 288 查看
本次的主要内容是块,对初学者来说,代码中涉及到块的内容确实很容易让人疑惑。首先谈一下块的概念,块(Block)是苹果为 C、C++以及 OC 添加的一种特性,它包含了部分代码,可以被当做是参数传递给函数,并且它的实质是 OC 中的对象,也就是我们完全可以把它放到集合中,比如我们可以定义 NSArray 或者 NSDictionary 的对象来放置一系列的块,然后通过代码来决定执行哪一个块。块还有一大特性,就是可以从相应的代码块中截取变量的值,就像闭包或者 lambda 表达式一样,但是块所截取的只是单纯的值。

语法

关于块的声明,直接使用一个 ^ 就可以了,就像这样

^{
NSLog(@"This is a block");
}


这么一看的话,其实块和普通的代码快并没有多大的区别,但是块却具有代码块所无法实现的特性。打个比方,块定义之后,它就会作为一个 OC 的对象而存在,而普通的代码块则做不到这样。既然块可以作为对象来存在,那么我们就应该有办法去获取一个块的对象,然后去使用它。对于这点,我们可以通过一个指针来实现:

void (^simpleBlock)(void);


其实这个指针的形式和函数指针的形式非常像,第一个 void 指明返回类型,第二个 void 表明参数,然后中间是块的名称。考虑到块的其他特性,其实我们可以把块看做是一种特殊的函数。这里我们还是继续看一下这个指针,如果我们想让它指向一个具体的代码块,只要这样:

simpleBlock = ^{
NSLog(@"This is a block");
};


实际上,这就是一个赋值的过程,,当赋值完成后,我们就可以直接调用块:

simpleBlock();


参数和返回值

之前也提到了,块可以看做是一个特殊的函数,那么它自然也是可以定义参数以及返回类型的,并且前面也提到了块的定义中各个部分代表的意义,按照前面的说明,如果我们要给块定义参数和返回值,那么应该是这种形式:

double (^multiplyTwoValues)(double, double);


这么看的话,确实块指针的声明和函数指针的声明几乎是一模一样。并且在返回的时候也是通过 return 关键字来返回相应数据。另外,对于块的调用也和函数的调用非常相似:

double (^multiplyTwoValues)(double, double) =
^(double firstValue, double secondValue) {
return firstValue * secondValue;
};

double result = multiplyTwoValues(2,4);

NSLog(@"The result is %f", result);


所以在对块的概念还是感到疑惑,难以理解的时候,不妨先把它看做是一种特殊的函数。

值的截取

前面也有说到,块中可以截取相应作用域内的值。比如在一个函数中定义了一个块,那么它会保存当前时刻这个函数作用域内的状态。其中包含了作用域内的值,所以可以直接使用:

- (void)testMethod {
int anInteger = 42;

void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};

testBlock();
}


但是关于这种截取是基于值的,也就是说我们在块中使用的值,可能并不是这个变量当前的值,而是在块创建的那个时候所截取的一个记录。这也就是说,在块中我们没有办法实时地获取外部变量的值,并且也没有办法去修改变量的值,就像下面这样:

int anInteger = 42;

void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};

anInteger = 84;

testBlock();


这一段代码最终输出的结果是 Integer is: 42,所以很明显我们并没有直接获取这个变量的值,只是保存了一份副本。另外,在块中我们也无法修改截取到的变量的值,因为它是 const 类型的。

使用 __block 来分享变量

之前说到,关于块中截取到的变量,我们既无法获取到它实时的值,也无法对获取到的值做出修改,但是对于原本的变量,我们可以使用 __block 来进行修饰。这个声明实际上是针对存储的一种声明,当我们用 __block 来修饰一个变量的时候,这个变量就会放到原本它所在的作用域以及所有块所共享的存储区中。

还是用之前的这个例子,我们把 anInteger 改成一个在块间共享的变量:

__block int anInteger = 42;

void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};

anInteger = 84;

testBlock();


这一次运行的结果就变成了 anInteger,也就是我们已经实时获取倒了变量的值,并且,这个时候我们也可以在块的内部修改变量的值了。

把块作为参数

实际上,我们之所以定义了块,并不是为了在定义之后就去直接调用,更多的时候,我们只是希望在某个操作之后,可以根据这个操作的结果来调用一段代码,但是这么一段代码我们没有办法预先定义好,所以这个时候就可以用到块,这也是块比较重要的一个特性,那就是作为参数来传递。一般来说,我们在传递块的时候,主要是把它作为回调的内容或者是用作多线程的开发。

我们可以用一个很简单的小例子来说明一下,比如说网络请求,一般来说在进行网络请求的时候,我们都会先放出一个加载框,等到请求完成之后再关闭加载框,那么我们就可以这样去实现这个过程:

- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];

XYZWebTask *task = ...

[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}


这里涉及到了一个 self 的问题,在块中,我们看到,对于 self 变量没有做任何的处理,就直接调用了相应的方法,实际上,对于块中出现的 self,是要多加小心的,因为非常容易产生强引用循环。

这里先看一下 beginTaskWithCallbackBlock 这个函数,其实这个函数比较简单,需要在意的是他的声明,我们先看一下这个函数的申明:

- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;


实际上和块的声明基本类似,第一个 void 指明返回类型,第二个 void 说明参数类型,中间表明是块。和块的声明不同的地方在于,块的名称放到了最后面。

块作为参数时应当放到参数列表的最后面

这个可以算是一种编码规范了,在编写函数的时候,如果参数中有块,那么就放到最后面,毕竟这样在调用的时候,代码看起来更加简洁明了。一般对于块的命名,要么像之前一样使用 callback,要么直接叫做 completion,基本上块的命名就这么两种。

通过自定义类型来简化块的语法

块变量的声明其实挺麻烦的,尤其是如果要声明多个块的变量的时候,一遍又一遍重复地写返回类型、参数,估计也是挺要命的。所以如果要简化这里的写法,我们可以预先定义好一个对应块的类型,也就是使用 typedef 关键字来实现:

typedef void (^XYZSimpleBlock)(void);


像上面这样,就定义了一个返回值、参数都为空的一种块类型 XYZSimpleBlock,接下来去定义块的类型的时候,就可以直接定义:

XYZSimpleBlock anotherBlock = ^{
...
};


当然,就这么看的话感觉好像自定义类型并没有什么用,但是如果代码中会涉及到多个同种类的块的时候,这样做就方便很多了。并且在函数中涉及到块的参数,这里的定义就方便很多了:

- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
...
callbackBlock();
}


上面的代码基本上就和普通的函数参数声明没什么区别了,一下子简化了很多。

当然,还有另一种情况下自定义一个类型是非常有必要的,比如,如果说一个块的返回值,是另一个块,可以先考虑一下这个要怎么写。标准的块的声明是这样的 (返回值)(^块名)(参数),那么如果把返回值换成另一个块再看看:

void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
...
return ^{
...
};
};


说实话,看到这段代码我个人是觉得挺头痛的,这段代码基本上就是一个块,它会返回一个 (void (^) (void)) 类型的块,再看看类型的定义,整个人头都大了,这个块本身还用了另外一个块作为参数,基本上就是不能让人好好玩耍的状态,但是,假如我们自定义了 void (^) (void) 这个类型,再看看这段代码会变成什么样:

XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
...
return ^{
...
};
};


一下子整个代码都清晰了,有时候可读性就是这么重要。

将块设置为属性

实际上之前也提到了,块有一个很有趣的地方,那就是它本身其实也是一个 OC 中的对象,所以我们完全可以把它当做是一个类的属性。如果说要把一个块作为一个类的对象,那么得考虑清楚它的作用,否则单单为了某一个函数的回调特意去设置一个属性意义不大。当然,这里主要只是说明用法:

@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end


由于块的特殊性,所以在声明的时候我们没办法直接写一个简单的类型,除非是自定义,否则只能像这样整个写出来。另外值得注意的一点就是,如果要把块当做一个属性,那就要把它设置为 copy 的,这是因为当一个块在捕获外部域的状态的时候,一个块会被复制,这个过程有点类似于快照,我们保存的其实是当时的一种状态。把块作为对象的属性之后,它的使用其实也没有太大的变动:

self.blockProperty = ^{
...
};
self.blockProperty();


当然,如果配合一下自定义类型的话,看起来会更好:

typedef void (^XYZSimpleBlock)(void);
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end


对于块的理解,有一点不能忘记,那就是它的本质是 OC 的一个对象。

避免强引用循环

看到这里很多人都会觉得奇怪,块在捕获变量的时候类似于快照,为什么还会产生强引用循环?实际上,在块对变量进行捕获的时候,它对对象产生的是一个强引用,也许这听上去很奇怪,不过,毕竟我们有办法让变量处于块的存储区之中,而块可以去直接访问这样的变量,所以为了保险,在捕获的时候就直接采用强引用避免在块中代码执行到一半的时候对象已经被销毁了。再回过头来审视一下强引用循环,因为块也是一个对象,所以如果产生了强引用循环,那么就是对象间相互引用的状况,再结合之前的可以把块当做属性,强引用循环产生的原因也就很明确了。关键问题就在于如何解决,其实和普通的强引用循环一样,解决方法就是加入弱引用循环,但是问题在于把哪一方设置成为弱引用。考虑在一个块的内部,这个时候块本身肯定是不会释放的,并且此时它持有了一个对象的强引用,这个对象又保持了对块的一个强引用,这意味着单单考虑块中的情况而言,块要释放,就必须先把对象对它的强引用撤销掉,因为块不执行完是不可能被消除的,所以我们需要做的,就是块对对象的引用改成弱引用,这一点可以通过一个小方法做到:

- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
self.block = ^{
[weakSelf doSomething];   // capture the weak reference
// to avoid the reference cycle
}
}


透过这段代码,我们还可以发现另外一件事,那就是块对变量的捕获方式,因为这里其实我们捕获到的是 weakSelf,而不是 self,否则强引用循环依旧存在,这也就是说块对变量的捕获并不是站在全局的,而是局部的捕获。

块的常见用法

谈到块的用法,首先会想到的就是回调,没错,块经常用于函数的回调,因为它正好可以当做一个执行工作的单元,这样一来通过块来实现一些异步的操作就非常方便。

说到这里的话,先简单的说一下 OC 中的多线程。OC 中的多线程严格意义上可以说就两种,C 语言中的 Thread, OC 提供的操作队列。如果放宽一点的话,iOS 和 OS X 中可以对应 posix 的线程标准,通过 iOS 中还有个 GCD,其实它实质上也是操作队列,只是官方为我们封装好的固定的队列。多线程具体的内容后续还会详细说明,现在就点到为止。

操作队列

回过头来再接着看块,在使用操作队列的时候,我们通常都是创建一个 NSOperation 的实例,这个实例其实就是封装了某些操作,接着我们就会把它加入 NSOperationQueue 这个队列中执行。关于 NSOperation 的使用,大体上如下:

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
...
}];


可以看到,我们在实例化 NSOperation 的时候就是直接使用块来进行实例化,再考虑到 NSOperation 的定义,其实块非常符合操作队列的要求。接下来可以看看操作队列的用法:

// schedule task on main queue:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];

// schedule task on background queue:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];


这里直接取了一个 mainQueue,其实我们也可以自己去定义一个队列,这样我们就可以非常灵活地拿到自己想要的队列,并行或串行,同步或异步,优先级等等都可以灵活定制,当然,这些是以后的内容了。

GCD

对于 iOS 开发来说,GCD 应该是非常熟悉的了,iOS 中几大多线程编程,GCD 应该是最方便的,系统为我们制作的每个应用程序都准备好了几个队列,想用的时候直接拿就可以了,并且常用的队列都覆盖到了,当然,基于 GCD 我们也可以自己来定义一个队列,言归正传,还是来看看 GCD 的用法:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);


就这么一句简单的话,我们就获取到了一个队列,并且我们可以通过参数来告诉系统我们要什么样的队列。然后,我们就可以开始执行操作了:

dispatch_async(queue, ^{
NSLog(@"Block for asynchronous execution");
});


枚举

实际上对于 Cocoa 以及 Cocoa Touch 中的 API,它们都会接受一个块作为对象来简化处理过程,比如对枚举这样的集合来说。以 NSArray 作为一个例子:

- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;


这个方法去取一个块作为参数,然后对集合内的每一个元素都执行这个块。对于集合类来说,很多的方法都会采用块作为参数。

总结

关于块的内容,整体来说的话,可以这么去看,为了理解的方便,我们可以把它看做是一个函数指针,只是这个指针的特殊之处在于,它本身是一个对象,并且它和函数不同,它是一组可以执行的代码单元。另外,块可以捕获外部的变量,但是这种捕获是局部的捕获,如果我们使用了全局变量,那么它也会去捕获全局变量,并且变量的捕获是类似于快照一样的,只是捕获了一个值。如果想要在块中实时的访问一个变量,就要让那个变量存储在快的共享区中。最后,块对于对象的引用,是强引用,所以为了避免强引用循环,我们需要主动把块对对象的引用,改成弱引用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: