这是Objective-C系列的第6篇,也是《Effective Objective-C 2.0》系列的最后一篇。数组
@synchronized
或NSLock
对象更简单;performSelector
系列方法在内存管理易有疏漏,它没法肯定将要执行的选择子具体是什么,因此ARC编译器也就没法插入适当的内存管理方法;performSelector
系列方法所能处理的选择子太过局限,选择子返回值类型及发送方给方法的参数个数都受到限制;performSelector
系列方法,而是应该把任务封装到Block里,调用GCD来实现。NSOperation
。NSOperation
对象许多属性都适合经过键值观察机制来监听,好比isCancelled
,isFinished
;dispatch_once
来执行只需运行一次的线程安全代码dispatch_get_current_queue
dispatch_group
中,开发者能够在这组任务完毕时得到通知;dispatch_group
,能够在并发时派发队列同时执行多项任务。 Block用“^”(脱字符或插入符)来表示:安全
^{
//Block implementation here
}
复制代码
Block实际上是个值,自有其类型,与int、float或Objective-C对象同样,也能够把Block赋给变量,其与函数指针相似。 Block的完整的语法结构以下:网络
return_type (^block_name)(parameters)
复制代码
看一个实例:多线程
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a+b;
}
复制代码
调用:并发
int add = addBlock(2, 5) //add = 7
复制代码
下面是各类状况下的Block的写法:框架
//属性
@property (copy ,nonatomic)void (^callBack)(NSString *);
//函数参数
- (void)callbackAsAParameter:(void (^)(NSString *print))callBack {
callBack(@"i am alone");
}
[self callbackAsAParameter:^(NSString *print) {
NSLog(@"here is %@",print);
}];
//typedef
typedef void (^callBlock)(NSString *status);
CallBlock block = ^void(NSString *str){
NSLog(@"%@",str);
};
复制代码
Block的强大之处:在声明它的范围里,全部变量均可觉得其所捕获。就是Block里能够用该范围的全部变量。异步
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a+b+additional;
}
int add = addBlcok(2, 5); //add = 12
复制代码
若是须要修改Block所捕获的变量,须要加上__block。async
为了更好说明Block,这里说明下函数指针。函数
函数指针是指向函数的指针变量。 于是“函数指针”自己首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组同样,这里是指向函数。如前所述,C在编译时,每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其余类型变量同样,在这些概念上是大致一致的。函数指针有两个用途:调用函数和作函数的参数。下面是个实例:oop
#include<stdio.h>
int max(int x,int y){return (x>y? x:y);}
int main()
{
int (*ptr)(int, int);
int a, b, c;
ptr = max; //ptr = &max;
scanf("%d%d", &a, &b);
c = (*ptr)(a,b);
printf("a=%d, b=%d, max=%d", a, b, c);
return 0;
}
复制代码
Block自己是个对象,在存放Block的内存区域里,第一个个变量是指向Class对象的指针,该指针叫作isa,其他内存里含有对象正常运转所需的各类信息:
Impl 是个结构体。内部有个FuncPtr指向Block的实现代码,此参数表明Block。Block实现了把原来标准C语言中须要“不透明的void指针”传递状态变的透明,并且简单易用。
descriptor是指向结构体的指针,每一个Block都包含该结构体。其中声明了copy及dispose这两个辅助函数所对应的函数指针。辅助函数在Block拷贝或者丢弃Block对象是运行。
Block会将其所捕获的全部变量都拷贝一份,置于descriptor以后,要注意的是,拷贝的并非对象自己,而是指向这些对象的指针变量。
invoke函数为什么须要把Block做为对象参数传进来呢?缘由在于,执行Block时,要从内存中把这些捕获到的变量读出来。
定义Block时,其所占的内存区域是分配在栈中的,即Block只在定义它的那个范围内有效。如:
void (^block)();
if(//) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"Block B");
};
}
复制代码
if/else中定义的Block,都是在栈中,当离开了相应的范围后,该栈内存有可能会被覆写。因此在运行时,有可能正确运行,也有可能发生崩溃。这取决于编译器是否覆写了该Block内存。
栈内存中的Block对象,无需考虑对象的释放,由于栈内存是系统管理的,系统会保证回收对象。
为了解决该问题,能够给Block对象发送copy消息,以执行拷贝。就可把Block对象从栈内存拷贝到堆内存。
堆内存中的Block对象,同普通对象一致,有引用计数,拷贝是递增引用计数,在ARC时无需手动释放,在引用计数为0时自动释放等。
void (^block)();
if(//) {
[block = ^{
NSLog(@"Block A");
} copy];
} else {
[block = ^{
NSLog(@"Block B");
} copy];
}
复制代码
除了上面的“栈Block”和“堆Block”,还有一类叫作“全局Block”。全局Block,有下面几个特色:
不会捕捉任何状态,好比外围的变量等,运行时也无需有状态来参与;
Block所使用的是整个内存区域,在编译器已经彻底肯定,所以全局Block能够声明在全局内存里,而不须要每次用到的时候在栈中建立;
全局Block的拷贝操做是个空操做,由于全局Block决不可能为系统所回收;
全局Block至关于单例;
下面是个全局Block:
void (^block) = ^{
NSLog(@"This is a block");
};
复制代码
因为运行该Block所需的所有信息都能在编译器肯定,因此可把它作成全局Block,这彻底是种优化技术。若把如此简单的Block当作复杂的Block来处理,那就会在复制或者丢弃该Block执行一些无谓的操做。
委托代理能很大程度上实现异步回调处理这样的事,可是委托代理这种模式却会使得代码极度分散。
用handler来集中代码,是个不错的选择。
//风格一:
HONetworkFetcher *fetcher = [HONetworkFetcher alloc] initWirhURL:url];
[fetcher startWithCompletionHandler:^(NSData* data){
//handle success
} failureHandler:^(NSError *error){
//handle failure
}];
//风格二:
HONetworkFetcher *fetcher = [HONetworkFetcher alloc] initWirhURL:url];
[fetcher startWithCompletionHandler:^(NSData* data,NSError *error){
if(error){
//handle success
}else{
//handle failure
}
}];
复制代码
风格一:代码易懂,将成功与失败的逻辑分开来写,必要时能够忽略成功或者失败的处理情形。
风格二:
总结:
在建立对象时,可使用内联的handler Block将相关业务逻辑一并声明。
在有多个实例须要监控时,若是采用委托模式,那么常常须要根据传入的对象来切换,而若改用handler Block来实现,则可直接将Block与相关对象放在一块儿;
设计API时,若是遇到handler Block,那么能够新增一个参数,使调用者能够经过该参数来决定应该把Block安排在哪一个队列上执行。
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block NS_AVAILABLE(10_6, 4_0);
复制代码
以下代码:
@interface HONetworkFetcher()
@property (nonatomic ,strong ,readwrite) NSURL *url;
@property (nonatomic ,copy)HONetworkFetcherCompletionHadler completionHandler;
@property (nonatomic ,strong)NSData *downloadedData;
@end
复制代码
@implementation HONetworkFetcher
- (instancetype)initWithURL:(NSURL *)url {
if (self = [super init]) {
_url = url;
}
return self;
}
- (void)startWithCompletionHandler:(HONetworkFetcherCompletionHadler)completion
{
self.completionHandler = completion;
//start the request
//request sets downloadedData property
//When request is finished ,p_requestCompleted is called
}
- (void)p_requestCompleted
{
if (_completionHandler) {
_completionHandler(_downloadedData);
}
}
@end
复制代码
某个类做了以下的调用:
@interface HOClass : NSObject
@end
@interface HOClass()
{
HONetworkFetcher *_networkFetcher;
NSData *_fetchData;
}
@end
@implementation HOClass
- (void)downloadData
{
NSURL *url = [NSURL URLWithString:@"www.com"];
_networkFetcher = [[HONetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchData = data;
}];
}
@end
复制代码
分析下场景:
HOClass的实例对象实例变量_networkFetcher引用获取器,_networkFetcher持有completionHandler,completionHandler又引用_fetchData,至关于持有HOClass的实例对象,因此就形成了循环引用。
解除循环引用的方式很简单,打破这个三角循环,要么是使得_networkFetcher再也不引用,要么获取器再也不持有completionHandler。
下面是一种解决方式:
- (void)downloadData
{
NSURL *url = [NSURL URLWithString:@"www.com"];
_networkFetcher = [[HONetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchData = data;
_networkFetcher = nil;
}];
}
复制代码
另一种状况是:completion handler所引用的对象最终又引用了这个Block自己。其中获取器持有completion handler,而completion handler中又对获取器的url进行引用。
- (void)downloadData
{
NSURL *url = [NSURL URLWithString:@"www.com"];
HONetworkFetcher * networkFetcher = [[HONetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"Request URL %@ finished",networkFetcher.url)
_fetchData = data;
}];
}
复制代码
上面这种保留环,打破也很简单:
- (void)p_requestCompleted
{
if (_completionHandler) {
_completionHandler(_downloadedData);
}
self.completionHandler = nil;
}
复制代码
在Objective-C中,多线程执行同一份代码,使用锁来实现某种同步机制,在GCD以前,有两种办法:
其一是“同步Block”:
- (void)synchronizedMethod
{
//此处同步行为所针对的对象是self,会根据给定的对象自动建立一个锁,并等待Block中的代码执行完毕。执行到代码结尾处,锁就释放了。若对self对象频繁加锁,则会须要等到另外一端与此无关的代码执行完毕才能继续执行当前的代码。
@synchronized(self){
//Safe
//do whatever
}
}
复制代码
另一种就是:
_lock = [[NSlock alloc] init];
- (void)synchronizedMethod
{
[_lock lock];
//Safe
[_lock unlock];
}
复制代码
上面两种方法有其缺陷:极端状况下,都会致使死锁,其效率也不高。
替代方案就是:GCD。
_syncQueue = dispatch_queue_create("sync.queue", DISPATCH_QUEUE_SERIAL);
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString
});
}
复制代码
上面将保证全部读写的操做都在同一队列中,这相比上面加锁机制,更为高效(GCD基于底层的优化),也更为整洁(全部的同步在GCD中实现)。
上面能够优化的就是,能够将取值方法,异步读取,串行队列里派发异步操做,会开启一个新线程来执行异步操做,而不是同步操做那样全部的操做在同一个线程。以下:
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_async(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
复制代码
但虽然是优化,不过有个优化陷进,就是执行异步派发是,须要拷贝Block。若拷贝Block的执行时间比Block执行所用的时间长,那么就是个“伪优化”,则比原来更慢。因为本例简单,因此改完以后可能更慢。
多个获取方法能够并发执行,而获取方法与设置方法之间不能并发执行。能够采用栅栏函数,此次改用并发队列:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_sync(_syncQueue, ^{
_someString = someString
});
}
复制代码
下面是执行:
并发队列若是发现接下来要处理的块是个栅栏块,那么就一直要等当前全部并发块都执行完毕,才会单独执行这个栅栏块,待栅栏块执行事后,再按正常方式继续向下处理。
测试一下性能,这种作法比刚才的确定更快。
注意,设置函数也能够用同步的栅栏块来实现,那样作可能会更高效,由于异步须要拷贝代码块。
要选方案,仍是最好测一下实际的性能。
@synchronized
或NSLock
对象更简单; NSObject中能够调用任何方法,最简单以下:
- (id)performSelector:(SEL)selector
复制代码
若是选择子在运行期决定,就能体现出此方式的强大之处了。这就至关于在动态绑定上再次使用动态绑定:
SEL selector;
if(/*some condition */) {
selector = @selector(bar);
} else if(/* some ohter condition */) {
selector = @selector(foo);
} else {
selector = @selector(baz);
}
[object performSelector:selector];
复制代码
使用此特性的代价是:若是在ARC下编译此代码 ,那么编译器会发出下面警告:
warning:performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]
由于没法肯定选择子,也就没有运用内存管理规则判断返回值是否是须要释放。ARC采用了比较谨慎的方法,就是不添加释放操做。然而这么作可能致使内存泄漏。下面是一个实例:
SEL selector;
if(/*some condition */) {
selector = @selector(newObject);
} else if(/* some ohter condition */) {
selector = @selector(copy);
} else {
selector = @selector(someProperty);
}
id ret =[object performSelector:selector];
复制代码
这段代码,在执行第一个和第二个选择子时,须要释放ret对象,而第三个则不须要。可是这个问题很容易被忽视,或者用静态分析器也没法侦测到。
编者按:根据苹果的命名规则,第一个和第二个选择子建立对象时,会拥有对象的全部权,因此须要释放。
其次,performSelector方法只返回id类型,即只能是void或者对象类型,而不能是整形等纯量类型。
另外,还有几个performSelector方法以下:
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
复制代码
@interface NSObject (NSThreadPerformAdditions)
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end
复制代码
然而,上面的延时执行均可以用dispatch_after来处理:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//todo
});
复制代码
performSelector
系列方法在内存管理易有疏漏,它没法肯定将要执行的选择子具体是什么,因此ARC编译器也就没法插入适当的内存管理方法;performSelector
系列方法所能处理的选择子太过局限,选择子返回值类型及发送方给方法的参数个数都受到限制;performSelector
系列方法,而是应该把任务封装到Block里,调用GCD来实现。 使用NSOperation
及NSOperationQueue
:
NSOperation
。NSOperation
对象许多属性都适合经过键值观察机制来监听,好比isCancelled
,isFinished
;+ (instancetype)sharedManager {
static HOClass *shared = nil;
@synchronized (self) {
if (!shared) {
shared = [[self alloc] init];
}
}
return self;
}
复制代码
更优的实现方式:
+ (instancetype)sharedManager {
static HOClass *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[self alloc]init];
});
return shared;
}
复制代码
dispatch_once
能够简化代码而且完全保证线程安全,此外更高效,它没有使用重量级的同步机制。
dispatch_get_current_queue
函数的行为经常与开发者所预期的不一样。此函数已经废弃,只应作调试只用。dispatch_get_current_queue
函数用于解决由不可重入代码说引起的死锁,然而此函数解决的问题,一般也能改用“队列特定数据”来解决。dispatch_group可以把任务分组,调用者能够等待这组任务执行完毕,也能够提供回调函数以后继续往下执行,这组任务完成时,调用者会获得通知。
dispatch_group
中,开发者能够在这组任务完毕时得到通知;dispatch_group
,能够在并发时派发队列同时执行多项任务。此时GCD会根据系统资源情况来调度这些并发执行的任务。开发者若本身来实现此功能,则须要编写大量代码。