游戏中心
游戏中心是Apple技术,允许游戏开发者整合排行榜、成就、多人支持以及其它一些事物到他们的iOS应用程序中。它为啥如此重要?就是因为Apple为你处理了基础设施服务。Apple还为iOS开发者提供了一个框架,叫作GameKit,使得游戏中心集成到iOS应用程序相当容易。
1.1 GCD和块对象介绍
问题
你想学习如何使用块对象和Grand Central Dispatch,以在iOS中编写游戏中心应用程序。
解决方案
此处学习块对象和Grand Central Dispatch。
讨论
在某些时刻,我们已经使用了线程。我们使用线程来在代码中分隔执行路径,并对某些执行路径给予更高的优先级。一个经典的例子是iOS应用程序中的主UI线程。为了维持反应灵敏的用户界面,所有的iOS开发者都被鼓励,避免UI线程由于非UI相关的工作而忙碌。因此,所有的非UI相关的工作可以,实际上应当,在单独的线程中执行。
随着多核移动设备(如iPad 2,)的引入,线程及其管理变得比以前更加复杂。开发者不仅要知道任一实例正在运行的执行路径,还要知道该执行路径处于哪个处理器核,这是为了优化多核处理器的电源。为简化问题,Apple为iOS和OS X开发者提供了一套优秀的API,封装于一个叫作Grand Central Dispatch(GCD)的库中。GCD允许开发者简化焦点到那些不得不被执行的代码上,忘掉那些恶心的工作(这些工作需要被展开,以便在可能拥有多核的设备上,在多线程之间平衡)。
GCD协同块对象工作。块对象是“首类”函数,这意味着他们能够作为参数传入其它方法,并且能够作为返回值从方法中返回。块对象的语法和简单的C函数或过程不同。举个实例,一个拥有两个int参数(value1 和 value2)的C函数,返回int型的参数之和,可用如下方式实现:
int sum (int value1, int value2){
return value1 + value2;
}
该代码使用块对象的等效实现是(PS:个人觉得可以这么理解:前面是函数指针变量,后面是匿名函数):
int (^sum)(int, int) = ^(int value1, int value2){
return value1 + value2;
};
或者,假设我们要在C中实现一个向控制台打印字符串的过程,可能如下编写:
void printSomestring(void){
printf("Some string goes here...");
}
块对象的演示:
void (^printSomeString)(void) = ^(void){
printf("Some string goes here...");
};
如前所提,块对象是“首类”函数,因此能够作为参数传递给方法,过程和函数。例如,sortUsingComparator:NSMutableArray的实例方法,我们随后可以看到,接受(返回一个NSComparisonResult类型的值,有两个类型为id的参数的)块对象。下面是调用该方法来对数组进行排序:
NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:
@"Item 1",
@"Item 2",
@"Item 3",
nil];
[array sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
/* Sort the array here and return an appropriate value */
return NSOrderedSame;
}];
[array release];
除了传递内联块对象给方法,学习如何编写接受和处理内联块对象的方法也很重要。假定我们有一个Objective-C方法,sumOf:plus:,拥有两个类型为NSInteger的参数,计算参数的和,然后返回一个64位的long long类型的值。我们让该Objective-C方法调用一个块对象来执行计算和返回结果。我们可以这样实现:
long long (^sum)(NSInteger, NSInteger) =
^(NSInteger value1, NSInteger value2){
return (long long)(value1 + value2);
};
- (long long) sumOf:(NSInteger)paramValue1
plust:(NSInteger)paramValue2{
return sum(paramValue1, paramValue2);
}
块对象如同C过程和函数那样执行。在前面的sum块对象的情况下,我们可以在Objective-C方法中执行它,如下所示:
int (^sum)(int, int) = ^(int value1, int value2){
return value1 + value2;
};
- (int) calculateSumOfTwoNumbersUsingBlockObjects:(int)number1
secondNumber:(int)number2{
return sum(number1, number2);
}
Objective-C方法calculateSumOfTwoNumbersUsingBlockObjects:secondNumber:调用sum块对象,并且传递块对象的返回值给调用代码。你开始发现块对象如此简单了吗?我简易你开始在Xcode工程中编写一些块对象,以此熟悉一下语法。我非常清楚,对Objective-C开发者来说,块对象的语法并不那么可取;但你一旦学会块对象提供的能力,肯定会忘记其构建的困难,而把焦点放在优势上。
块对象的一个最重要的优势是他们能够被内联使用,因而也能够作为参数传递给其它方法。例如,我们想要对NSMutableArray的实例按升序排序,可以使用此处所示的NSMutableArray类的sortUsingComparator:方法。该方法接受一个有两个参数的块对象,返回一个NSComparison类型。由于sortUsingComparator:接受一个块对象作为参数,我们可以将它用于任何类型的数据,并相应地调整排序方法。
NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:
[NSNumber numberWithInteger:10],
[NSNumber numberWithInteger:1],
[NSNumber numberWithInteger:20],
[NSNumber numberWithInteger:15],
nil];
/* Start an ascending sort on the array */
[array sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
/* By default, let's assume the values are the same */
NSComparisonResult result = NSOrderedSame;
/* Get the two values as numbers */
NSNumber *firstNumber = (NSNumber *)obj1;
NSNumber *secondNumber = (NSNumber *)obj2;
/* If the second number is bigger than the first, we are on
an ascending trend */
if ([secondNumber integerValue] > [firstNumber integerValue]){
result = NSOrderedAscending;
}
/* Otherwise, if the second number is smaller than the first number,
we are on a descending trend */
else if ([firstNumber integerValue] > [secondNumber integerValue]){
result = NSOrderedDescending;
}
return result;
}];
NSLog(@"%@", array);
[array release];
示例代码中NSLog过程输出为:
(
1,
10,
15,
20
)
虽然还有更容易地数组排序方式,不过本示例演示块对象的使用以及其他类(如NSMutableArray)如何允许传递块对象作为参数。该示例在使用块对象作为“首类”函数上更进一步。
对GCD,Apple决定块对象完美匹配它们想要获得的东西:在单核或多核设备上开发多线程应用程序的简单性。想想GC作为一个控制器对象。你可能会问,是什么控制器?是一个在调度队列中的大线程池的控制器。调度队列是一个由系统或开发者提交的任务的队列。这些任务将在线程中执行,如同被GCD管理。以此,当我们讨论调度队列时,就想想任务队列。
GCD的核心,是一些全局并发队列,它们可以被用dispatch_get_global_queue函数访问:
dispatch_queue_t dispatchQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
方法的第一个参数是调度队列的优先级。第二个参数保留,当前必须设0。队列的优先级越高,(在理想情况下)其中的任务就会被越快地执行。第一个参数可以传递的值有:
DISPATCH_QUEUE_PRIORITY_HIGH:高优先级。
DISPATCH_QUEUE_PRIORITY_DEFAULT:中优先级。
DISPATCH_QUEUE_PRIORITY_LOW:低先级。
DISPATCH_QUEUE_PRIORITY_BACKGROUND:最低的优先级。
除了全局并发队列,还可以使用main queue。每个应用程序最多拥有一个主队列。主队列和全局并发队列之间的不同是,主队列永远在主线程执行代码,而全局并发队列对代码的执行依赖于系统的决定和由GCD创建与管理的各种其它线程。
为了获取应用程序的主队列,必须如下使用dispatch_get_main_queue函数:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
/* Dispatch tasks to the queue */
现在,我们知道了如何取得全局并发队列和主队列的句柄。但问题是:我们如何在这些队列执行一段代码呢?答案很简单:使用某个dispatch_过程。下面是其中的几种口味:
dispatch_sync:提交块对象给指定的调度队列,同步执行。
dispatch_async:提交块对象给指定的调度队列,异步执行。
dispatch_once:提交块对象给指定的调度队列,在应用程序有效期中仅执行一次。调用相同的方法,传递相同的块对象给任何调度队列都将立即返回,而不会再次执行块对象。
提交给上述调度方法的块对象必须返回void,并且不能有参数。
好了,我们让代码运行起来。我想使用三个循环,每个打印数字序列1-10,并且我想让它们异步同时执行。
/* Define our block object */
void (^countFrom1To10)(void) = ^{
NSUInteger counter = 0;
for (counter = 1;
counter <= 10;
counter++){
NSLog(@"Thread = %@. Counter = %lu",
[NSThread currentThread],
(unsigned long)counter);
}
};
谜题的第二个和最后一个片段是决定我们想要让代码在哪个调度队列上执行。对本示例,我们可以在主队列(运行在主线程)或更好地,在任意一个全局并发队列中执行代码。因此,我们继续,使用一个全局并发队列:
/* Calling this method will execute the block object
three times */
- (void) countFrom1To10ThreeTimes{
/* Get the handle to a global concurrent queue */
dispatch_queue_t concurrentQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/* Now run the block object three times */
dispatch_async(concurrentQueue, countFrom1To10);
dispatch_async(concurrentQueue, countFrom1To10);
dispatch_async(concurrentQueue, countFrom1To10);
}
如果你在应用程序中调用countFrom1To10ThreeTimes方法,控制台打印的结果可能和这些类似:
...
Thread = <NSThread: 0x94312b0>{name = (null), num = 3}. Counter = 7
Thread = <NSThread: 0x9432160>{name = (null), num = 5}. Counter = 6
Thread = <NSThread: 0x9431d70>{name = (null), num = 4}. Counter = 7
Thread = <NSThread: 0x94312b0>{name = (null), num = 3}. Counter = 8
Thread = <NSThread: 0x9432160>{name = (null), num = 5}. Counter = 7
Thread = <NSThread: 0x94312b0>{name = (null), num = 3}. Counter = 9
Thread = <NSThread: 0x9431d70>{name = (null), num = 4}. Counter = 8
Thread = <NSThread: 0x9432160>{name = (null), num = 5}. Counter = 8
Thread = <NSThread: 0x94312b0>{name = (null), num = 3}. Counter = 10
Thread = <NSThread: 0x9431d70>{name = (null), num = 4}. Counter = 9
Thread = <NSThread: 0x9432160>{name = (null), num = 5}. Counter = 9
Thread = <NSThread: 0x9431d70>{name = (null), num = 4}. Counter = 10
Thread = <NSThread: 0x9432160>{name = (null), num = 5}. Counter = 10
主线程的线程号是1;因此,看看本示例中打印的线程号,可以断定,没有块对象在主线程上执行。这也是全局并发队列确实在其它线程而不是主线程上执行块对象的证明。我们还可以断定,dispatch_async过程正确工作,异步地执行了我们的块对象代码。
现在让我们看看另一个示例。假设我们想要asynchronously下载三个URL的内容,并标记下载的结束(在用户界面显示提醒)。在主队列和全局并发队列间的选择相当简单。既然URL的内容可能很大,就最好不要让主线程忙于下载。换句话说,我们应当避免使用主队列。我们还想一个接着一个地下载URL。简单说,我们要等待第一个URL下载完成,在进行第二个之前,其它类推。我们有大量的同步URL请求,因为我们知道我们将在全局并发队列上执行块对象,而这并不会阻塞主线程。为此,我们应当使用dispatch_sync过程,它将在执行下一个代码块之前阻塞指定的队列。
我们将此分为两个代码段。一段是块对象,用来下载我们传递给它的URL,如果下载成功返回YES,失败返回NO。
BOOL (^downloadURL)(NSURL *) = ^(NSURL *paramURL){
NSURLRequest *request = [NSURLRequest requestWithURL:paramURL];
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:nil
error:nil];
if ([data length] > 0){
NSLog(@"Successfully downloaded %lu bytes of data",
(unsigned long)[data length]);
return YES;
} else {
NSLog(@"Failed to download the data.");
return NO;
}
};
我们拥有了块能够同步地下载URL。现在让我们取得全局并发队列的句柄,并且在其中同步地执行该块。完成之后,我们想在用户界面显示消息。任何UI相关的操作,我们必须在主队列中执行。它在主线程中执行任务,如下所示:
- (void) downloadThreeURLsAndDisplayAlert{
__block BOOL wasSuccessful = YES;
dispatch_queue_t concurrentQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(concurrentQueue, ^(void){
NSLog(@"Downloading iOS 4 Cookbook's main page data...");
wasSuccessful &= downloadURL([NSURL URLWithString:
@"http://www.ios4cookbook.com"]);
});
dispatch_sync(concurrentQueue, ^(void){
NSLog(@"Downloading a blog's main page data...");
wasSuccessful &= downloadURL([NSURL URLWithString:
@"http://vandadnp.wordpress.com"]);
});
dispatch_sync(concurrentQueue, ^(void){
NSLog(@"Downloading O'Reilly's main page data...");
wasSuccessful &= downloadURL([NSURL URLWithString:
@"http://www.oreilly.com"]);
});
/* Make sure the UI-related code is executed in the
main queue */
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^(void) {
if (wasSuccessful == YES){
NSLog(@"Successfully downloaded all URLs.");
/* Display an alert here */
} else {
NSLog(@"At least one URL failed to download.");
/* Display an alert here too */
}
});
}
__block指令使指定的变量可被块进行写访问。如果移除本示例代码中的__block指令,并试图给wasSuccessful变量赋值,编译器将抛出错误。默认情况下,块对象对其词法范围内的所有变量具有读权限,而不具有写权限。
如果你拥有一个Internet链接,运行上面的代码将给你如下所示的类似结果:
Downloading iOS 4 Cookbook's main page data...
Successfully downloaded 518 bytes of data
Downloading a blog's main page data...
Successfully downloaded 74849 bytes of data
Downloading O'Reilly's main page data...
Successfully downloaded 80530 bytes of data
Successfully downloaded all URLs.
如果没有Internet链接,运行的结果和这些类似:
Downloading iOS 4 Cookbook's main page data...
Failed to download the data.
Downloading a blog's main page data...
Failed to download the data.
Downloading O'Reilly's main page data...
Failed to download the data.
At least one URL failed to download.
dispatch_sync过程在全局并发队列中执行我们的块对象,这意味着块对象将在主线程以外的其他线程中被执行。同时,由于dispatch_sync过程的天性,代码的执行将阻塞并发队列直至其完成。然后第二个同步调度发生,依此类推;直至我们先要给用户显示消息的地方。在这个情况下,我们在主队列中执行我们的块对象,因为所有的UI相关代码(显示、隐藏,向窗口添加视图等)需要在主线程中执行。
在进行游戏中心相关的主题之前,我们还应当看看先前讨论过的dispatch_once过程。该过程在指定的调度队列中执行块对象,在应用程序生存期中,仅执行一次。使用dispatch_once过程时,必须注意几件事:
1. 本过程会阻塞。换句话说,它是同步的,它将阻塞其所运行于的调度队列直至代码执行完全。
2. 和dispatch_sync、dispatch_async不同,本过程没有使用一个调度队列作为参数。默认情况下,它将在当前的调度队列执行任务。
:调用dispatch_get_current_queue函数获得当前的调度队列。
3. 本过程的第一个参数是dispatch_once_t类型的指针。这是本过程保持追踪哪些块要执行、哪些块不要执行的方式。举例来说,如果你调用本过程,为该参数传入两个不同的指针,但传入的是完全相同的块对象,那么,块对象将被执行两次,因为传入的两个指针指向不同的内存块。
4. 本过程的第二个参数是要被执行的块对象。该块对象必须返回void,并且不能有参数。
我们来看个例子。假设有一个计数0-5的块对象,并仅想让在应用程序生存期中它执行一次,在全局并发队列中执行以避免阻塞主线程。下面是我的代码实现:
void (^countFrom1To5)(void) = ^(void){
NSUInteger counter = 0;
for (counter = 1;
counter <= 5;
counter++){
NSLog(@"Thread = %@, Counter = %lu",
[NSThread currentThread],
(unsigned long)counter);
}
};
- (void) countFrom1To5OnlyOnce{
dispatch_queue_t globalQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); static dispatch_once_t onceToken;
dispatch_async(globalQueue, ^(void) {
dispatch_once(&onceToken, countFrom1To5);
dispatch_once(&onceToken, countFrom1To5);
});
}
调用countFrom1To5OnlyOnce方法,运行程序,获得类似如下的结果:
Thread = <NSThread: 0x5f07f10>{name = (null), num = 3}, Counter = 1
Thread = <NSThread: 0x5f07f10>{name = (null), num = 3}, Counter = 2
Thread = <NSThread: 0x5f07f10>{name = (null), num = 3}, Counter = 3
Thread = <NSThread: 0x5f07f10>{name = (null), num = 3}, Counter = 4
Thread = <NSThread: 0x5f07f10>{name = (null), num = 3}, Counter = 5
如果在countFrom1To5OnlyOnce方法中传递不同的token给dispatch_once过程,那么类似结果为:
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 1
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 2
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 3
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 4
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 5
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 1
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 2
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 3
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 4
Thread = <NSThread: 0x6a117f0>{name = (null), num = 3}, Counter = 5
本示例中的代码被执行两次,这不是我们想要的。因此,要确保,无论要被执行一次的代码块是什么,都要传递相同的指针给dispatch_once过程的第一个参数。
你现在应当对块对象和GCD有了较好的理解,我们可以回到游戏中心相关的更有趣主题。如果你想进一步了解块对象和GCD,可以访问下面的一些链接:
1. Introducing Blocks and Grand Central Dispatch 2. Grand Central Dispatch (GCD) Reference
注:全书内容不多(翻译完成后将打包成chm格式)