还是要好好学习英文啊,笔者只能看中文版的,下载地址如下:
http://download.csdn.net/detail/m6830098/7977521
看书的时候还是困的不行不行的-
今天来学习学习本书的第六章。
第一条:理解"块"的概念。块可以实现闭包。这项语言特性是做为"扩展(extension)"而加入GCC编译器中的,在近期版本的Clang中都可以使用(Clang是开发Mac OS X及 iOS程序的编译器)。块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用" ^ "符号来表示,后面跟一对花括号,括号里面是块的实现代码。如:
^{
//Block implementation here
}
块其实就是个值,而且自有其相关类型。与int、float和Object-C对象一样,也可以吧块赋值给变量,然后像使用其他变量一样使用块。块类型的语法与函数指针类似。
void (^someBlock) () = ^{
//Block implementation here
};
块类型的语法结构如下:
return_type (^block_name) (parameters)
例如:返回int值,有两个参数的块:
int (^addBlock) (int a, int b) = ^(int a, int b){
return a+b;
}
定义好之后就可以像函数一样使用了。
int add = addBlock(2, 5); // add = 7
快的强大之处在于:在声明它的范围里,所有变量都可以为其所捕获。即那个范围里的全局变量在块里依然可以使用。默认情况下,为块所捕获的变量,是不可以在块里修改的。如果修改了,编译器就会报错。不过在声明变量的时候可以加上 _ _block修饰符,这样就可以在块内修改了。
NSArray *array = @[@0,@1,@2,@3,@4];
__block int count = 0;
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL * _Nonnull stop) {
if ([number compare:@2] == NSOrderedAscending) {
count ++;
}
}];
//cout = 2
如果块所捕获的变量是对象类型,那么久会自动保留它。系统在释放这个块的时候,也会将其一并释放。如果将块定义在Object-C类的实例方法中,那么除了可以访问类的所有变量之外,还可以使用self变量。块总能修改实例变量,所以在声明是无须加_ _block。不过,如果通过读取或者写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量是与self所指代的实例管理在一起的。
@interface WWClass
- (void)anInstanceMethod {
//...
void (^someBlock) () = ^{
_anInstanceVariable = @"SomeThing";
NSLog(@"_anInstanceVariable = %@", _anInstanceVariable);
};
//...
}
@end
如果某个WWClass实例正在执行anInstanceMethod方法,那么self变量就指向此实例。直接访问实例变量和通过self来访问是等效的:
self ->_anInstanceVariable = @"SomeThing";
注意:self也是个对象,因而块在捕获它时也会将其保留,如果self所指代的那个对象同时也保留了块,那么这种情况通常就会导致"保留环"。
块的内部结构:每个Object-C对象都占据着某个内存区域。因为实例变量的个数以及对象所包含的关联数据互不相同,所以每个对象所占的内存区域也有大有小。块本身也是对象,在存放块的对象内存区域中,首个变量是指向Class对象的指针,改指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。
在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void *型的参数,此参数代表块。descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。
全局块、栈块以及堆块。定义块的时候,其所占的内存区域是分配在栈中的。也就是说,块只有在定义它的那个范围内有效。下面这段代码有危险:
void (^block) ();
if (/* some condition */ ) {
block = ^ {
NSLog(@"Block A");
};
} else {
block = ^ {
NSLog(@"Block B");
};
}
block();
定义在if以及else语句中的两个块都分配在栈内存中。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。为解决此问题,可以给块对象发送copy消息以拷贝之。这样就能把块从栈复制到堆了。并且一旦复制到堆上,块就成了带引用计数的对象了,后续复制操作都不会真的执行复制,只是递增块对象的引用计数。下面这段代码就是安全的:
void (^block) ();
if (/* some condition */ ) {
block = [^ {
NSLog(@"Block A");
} copy];
} else {
block = [^ {
NSLog(@"Block B");
} copy];
}
block();
除了"栈块"和"堆块",还有一种"全局块"。这种块不会捕捉任何状态(比如外围的变量等)。运行时也无效有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要再每次用到的时候于栈中创建。全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。
第二条:为常见的快类型创建typedef。每个块都具备其"固有类型(inherent type)",因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。与定义其他变量时一样,变量类型在左边,变量名在右边。
- (void)startWithCompletionHandler:(void (^) (NSData *data, NSError, *error))completion;
给参数定义一个别名,然后使用此名称来定义块:
typedef void (^WWCompletionHandler) (NSData *data, NSError, *error);
- (void)startWithCompletionHandler:(WWCompletionHandler)completion;
要点:1.以typedef重新定义块类型,可以令块变量用起来更加简单。2.定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。3.为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改响应的typedef中的快签名既可,无须改动其他typedef。
第三条:用handler块降低代码分散程度。处理用户界面的显示及触摸操作所用的线程,不会应为要执行I/O或者网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。假设把执行异步任务的方法做成同步的,那么在执行任务时,用户界面就变得无法响应用户输入了。某些情况下,如果应用程序在一定时间内无响应,那么就会自动终止,ios系统上的应用就是如此,"系统监控器(system watchdog)"在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。委托模式有个缺点:如果类要分别使用多个获取器下载不同的数据,那么就得在delegate回调方法里根据传人的获取器参数来切换。这样不仅会令delegate回调方法变得很长,而且还要把网络数据获取器对象保存为实例变量,用来判断。改用块的好处:无须保存获取器,也无须在回调方法里面切换,每个completion handler的业务逻辑,都是和相关的获取器对象一起来定义的。这种写法还有其他的用途,比如:现在很多基于块的API都是用块来处理错误。可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况的的代码,都封装到同一个completion handler块中,该块多一个NSError类型的参数而已。
第四条:用块引用其所属对象是不要出现保留环。使用块时,很容易形成保留环。
第五条:多用派发队列,少用同步锁。通常要使用锁来实现某种同步机制。在GCD出现之前,有两种办法,第一种是采用内置的"同步块(synchronization block)":
- (void)synchronizedMethod {
@synchronized(self) {
//safe
}
}
这种方法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码的结尾处,锁就释放了。另一个办法是直接使用NSLock对象:
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
//safe
[_lock unlock];
}
也可以使用NSRecursiveLock这种"递归所(recursive lock)",线程能够多次持有该锁,而不会出现死锁(deadlock)现象。有种简单而高效的办法可以代替同步块或者锁对象,那就是使用"传销同步列队(serial synchronization queue)"。将读取操作以及写入操作都安排在同一个列队中,既可包装数据同步。用法:
_syncQueue = dispatch_queue_create("com.effectiveobjectc.syncQueue", NULL);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
此模式的思路是:把设置操作和获取操作都安排在序列化的列队里执行,这样的话,所有震动属性的访问操作就都同步了。多个的获取方法可以并发执行,而获取方法与设置方法之间不能并发执行,可以使用并发队列(concurrent queue):
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
像现在这样写代码,还是无法正确实现同步。所有读取操作与写入操作都会在同一个队列上执行,不过由于是并发队列,所有读取与写入挫折可以随时执行。而我们不想让这些操作随意执行。此时用一个简单的GCD功能即可,它就是栅栏(barrier)。下列函数可以向队列中派发块,将其作为栅栏使用:
void dispatch_barrier_async (dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_sync (dispatch_queue_t queue, dispatch_block_t block);
在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的快总是按顺序逐个来执行的。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
第六条:多用GCD,少用performSelector系列方法。performSelector系列函数中,最简单的是"performSelector:",它接受一个参数,就是要执行的那个选择子:
- (id)performSelector:(SEL)selector;
该方法与直接调用选择子方法等效。下面2行代码效果相同:
[object performSelector:@selector(selectorName)];
[object selectorName];
如果选择子是在运行期决定的,那么就能体现出此方式的强大之处了,这就等于在动态之上再次动态绑定。
SEL selector;
if (/* some condition */) {
selector = @selector(foo);
} else if (/* some other condition */) {
selector = @selector(bar);
} else {
selector = @selector(baz);
}
[object performSelector:selector];
不够这样可能导致内存泄漏,改进如下:
SEL selector;
if (/* some condition /) {
selector = @selector(foo);
} else if (/ some other condition */) {
selector = @selector(bar);
} else {
selector = @selector(baz);
}
id ret = [object performSelector:selector];
performSelector:还有如下两个版本,可以在发消息时顺便传递参数:
- (id)performSelector:(SEL)selector withObject:(id)object;
- (id)performSelector:(SEL)selector withObject:(id)objectA withObject:(id)objectB;
这个方法的局限比较多,由于参数类型是id,所以传人的参数必须是对象才行,如果选择子所介接受的参数是浮点数或者整数,就不能用该方法了,此外,该方法最多只能接受两个参数。performSelector系列方法还有个功能就是延迟执行选择子。或将其放在另一个线程执行。
- (id)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimeInterval)delay;
- (id)performSelector:(SEL)selector onThread:(NSThread *)thread withObject:(id)argument waitUntilDone:(BOOL)wait;
- (id)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait;
如果要延后执行某项任务,可以有两种方法,应该优先第二种:
//Using performSelector:withObject:afterDelay:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
//Using dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});
把任务放在主线程上执行,同理:
//Using performSelectorOnMainThread:withObject:waitUntilDone
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];
//Using dispatch_async
//(or if waitUntilDone is YES,then dispatch_sync)
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
要点:performSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
第七条:掌握GCD及操作列队的使用时机。对于那些只需执行一次的代码,使用GCD的dispatch_once最为方便。然而在执行后台任务时,GCD并不一定是最佳方法。还有一种叫做NSOperationQueue,它虽然与GCD不同,但却与之相关,开发者可以把操作以NSOperation子类的形式放在列队中,而这些操作也能够并发执行。两者的差别,首先要注意:GCD是纯C的API,而操作列队则是Object-C的对象。在GCD中,任务用块来表示,而块是个轻量级的数据结构。与之相反,"操作(operation)"则是个重量级的Object-C对象。用NSOperationQueue类的"addOperationWithBlock:"方法搭配NSBlockOperation类来使用操作列队,其语法与纯GCD方式非常类似。好处:1.取消某个操作。如果使用操作列队,那么想要取消操作是很容易的。运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需要执行,不过,已经启动的任务无法取消。如果不使用操作列队,而是把块安排到GCD队列,那就无法取消了。2.指定操作间的依赖关系。一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖体系,使特定的操作必须在另一个操作顺利执行完毕后方可执行。3.通过键值观测机制监控NSOperation对象的属性。NSOperation对象有许多属性都适合通过键值观测机制(KVO)来监听,比如可以通过isCancelled属性来判断任务是否已取消,通过isFinished属性来判断任务是否已完成。4.指定操作的优先级。操作的优先级表示此操作与队列中的其他操作之间的优先级关系,优先级高的操作先执行。GCD没有直接实现此功能的办法,GCD的队列确实有优先级,但那是针对整个列队的。5.重用NSOperation对象。系统内置了一下NSOperation的子类(NSBlockOperation),也可以自己创建。有一个API选用了操作队列而非派发列队,那就是NSNotificationCenter,开发者通过其中的方法来注册监听器,以便在发生相关的事件时得到通知,而这个方法的接受参数是块,不是选择子:
- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void (^) (NSNotification *))block;
第八条:通过Dispatch Group机制,根据系统资源状况来执行任务。dispatch group 是GCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行。dispatch_group的创建方法:
dispatch_group_t group = dispatch_group_create();
把任务编组的两种方法:
1.使用下面的函数:
void dispatch_group_async (dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
它是普通dispatch_async函数的变体,比原来的多一个参数,用于表示待执行的块的所归属的组。
2.使用下面的一对函数:
void dispatch_group_enter (dispatch_group_t group);
void dispatch_group_leave (dispatch_group_t group);
前者能够使分组里正要执行的任务数递增,而后者则使之递减。故调用了dispatch_group_enter以后,必须有与之对应的dispatch_group_leave才行。
下面这个函数用于等待dispatch group执行完毕:
long dispatch_group_wait (dispatch_group_t group,dispatch_time_t timeout);
此函数接受二个参数,一个是要等待的group,另一个是代表等待时间的timeout值,timeout参数表示函数在等待dispatch group执行完毕时,应该阻塞多久。如果执行dispatch group所需的时间小于timeout,则返回0;否则返回非0值。次参数也可以取常量DISPATCH_TIME_FOREVER,这表示函数会一直等着dispatch group执行完,而不会超时。
还有一个函数:
void dispatch_group_notify (dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
与wait函数不同的是:可以向此函数传人块,等dispatch group执行完之后,块会在特定的线程上执行。
也可以用GCD来实现循环。函数
void dispatch_apply (size_t iterations dispatch_queue_t queue, void(^block)(size_t));
此函数会反复执行一定的次数,每次传给块的参数值都会递增,从0开始,直到"iterations - 1",其用法如下:
dispatch_queue_t queue = dispatch_queue_create("com.effectiveobjectc.syncQueue", NULL);
dispatch_apply (10, queue, ^(size_t i){
//perform task
});
需要注意的是:dispatch_apply所用的队列可以是并发队列。如果采用并发队列,循环处理数组:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL);
dispatch_apply (array.count, queue, ^(size_t i){
//perform task
id object = array[i];
[object performTask];
});
第九条:使用dispatch_once来执行只需运行一次的线程安全代码。对于单例模式(singleton)而言,在类中编写sharedInstance的方法,该方法只会返回全类共用的单例实例,而不会在每次调用的时候都创建新的实例。没有GCD的时候:
+ (id)sharedInstance {
static WWClass *sharedInstance = nil;
@synchronized(self) {
if (! sharedInstance) {
sharedInstance = [[self alloc] init];
}
}
return sharedInstance;
}
有了GCD之后,使用下面的函数重新改写单例函数:
void dispatch_once (dispatch_once_t *token, dispatch_block_t block);
此函数的接受类型为dispatch_once_t的特殊参数,我们可以称之为"标记"(token),此外还有块参数。对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行一次。首次调用该函数时,必然会执行块中的代码,最重要的是,此操作完全是线程安全的。注意:对于只需执行一次的块来说,每次调用函数是传人的标记(token)必须是完全相同的。改写后的单例模式:
+ (id)sharedInstance {
static WWClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once ( &onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
使用dispatch_once 可以简化代码并且彻底保证线程安全。也更高效。
第十条:不要使用dispatch_get_current_queue。 文档中说此函数返回当前正在执行代码的队列。该函数有种典型的错误用法(antipattern,"反模式"),就是用它检测当前列队是不是某个特定的列队,试图以此方法来避免执行同步派发时可能遭遇的死锁问题。但有时候并不能解决死锁问题。使用这种API的开发者可能误以为:在回调块里调用dispatch_get_current_queue所返回的"当前队列",总是起调用API时指定的那个,但实际上返回的却是API内部的那个同步队列。解决这个问题最好的办法就是通过GCD所提供的功能来设定"队列特有数据"(queue_specific data),此功能可以吧任意数据以键值对的形式关联到队列里。最重要在于,假如根基指定的键获取不到关联数据,那么系统就会沿着体系向上查找,直到找到数据或达根列队为止。
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.quequeA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.quequeB", NULL);
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
} else {
dispatch_sync(queueA, block);
}
};
});
这样就可以通过"队列特有数据"(queue_specific data)来获取某个队列,处理死锁问题。这个方法:
void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor)
此函数的首个参数表示待设置的数据的队列,其后两个参数是键与值。键与值都是不透明的void指针。对于键来说,有一个问题要注意:函数是按指针值来比较键的,而不是按照其内容。故"队列特有数据"(queue_specific data)的行为与NSDictionary对象不同,后者是比较键的"对象等同性"。"队列特有数据"更像是"关联引用"。值(函数原型里叫做"context")也是不透明的void指针,于是可以在其中存放任意数据。函数的最后一个参数是"析构函数(destructor function)",对于给定的键来说,当队列所占的内存为系统所回收,或者有新的值与键相关联时,原有的值对象就会移除,而析构函数会于此时运行。dispatch_function_t 类型的定义如下:
typedef void (*dispatch_function_t ) (void *)
由此可知,析构函数只能带一个指针参数且返回值必须为void。
注意:dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,也能用"队列特有数据"队列来解决,所以还是少用dispatch_get_current_queue。
最后,本书一共7个章节,此为第六章节:块与大中枢派发。
共勉!一步一个巴掌印。。。。。