美文网首页程序员
编写高质量iOS与OSX代码的52个有效方法-第六章:块Bloc

编写高质量iOS与OSX代码的52个有效方法-第六章:块Bloc

作者: 竹与豆 | 来源:发表于2018-07-23 16:45 被阅读33次

当前多线程编程的核心就是块(block)与大中枢派发(Grand Central Dispatch,GCD)。

块是一种可在C、C++、OC代码中使用的语法闭包(lexical closure),借由此机制,开发者可以将代码想对象一样传递,令其在不同环境下运行。还有个关键地方,在定义块的范围内,它可以访问到其中的全部变量。

GCD是一种与块相关的技术,它提供了对线程的抽象,而这种抽象则基于派发队列(dispatch Queue)。开发者可以将块排入队列中,有GCD负责处理所有调度事宜。CGD会根据系统资源情况,适时地创建、复用、摧毁后台线程(background Thread),以便处理每个队列。

GCD还可以完成常见编程任务,如果只执行一次的线层安全代码(thread-safe single-code execution),或者根据可用的系统资源并发执行多个操作。

37、理解“块”这一概念

block基础知识

block可以实现闭包。block与函数雷士,不过是直接定义在另一个函数里,和定义它的那个函数共享一个范围内的东西。

block 用^表示,后面跟一对花括号{},括号里就是要实现的的代码。

^{
}

block其实就是个值,而且有其相关类型。block语法与函数指针近似。

//定义一个名为someBlock,没有参数没有返回值的block变量。
void (^someBlock)() = ^{

};

block的语法结构:
return_type (^block_name)(parameters)

示例:

int addtional = 10;
    
int  (^addBlock)(int a, int b) = ^(int a,int b) {
    
    return a+ b + addtional;
};
NSLog(@"%d",addBlock(2,3));//15

block可以捕获声明在它范围内的所有变量。这份范围内的所有变量,在block中都可以使用。默认情况下,为块所捕获的变量,是不可以在块里修改的。

如果直接修改就会报错Variable is not assignable (missing __block type specifier)。但是如果声明时加上__block,在block内就可以修改了。

__block NSInteger count = 0;
NSArray *array = @[@1,@2,@3,@4,@5];
// 遍历数组
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL * _Nonnull stop) {
    if ([number compare:@2] == NSOrderedAscending) {//比2小
        count ++;
    }
}];
NSLog(@"%zd",count);//1

这里数组遍历的方法,演示了内联块(inline block)的用法。传给enumerateObjectsUsingBlock:的方法块,并没有先赋给局部变量,而是直接内联在函数里调用了。

如果block所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个block的时候,也会将其一并释放。这就引出一个与块有关的重要问题,块本身可视为对象。 在其他OC对象所能响应的选择子中,很多块也可以响应。块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了,回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。

如果将块定义在实例方法中,那么除了可以访问类的所哟实例变量之外,还可以使用self变量。

块总能修改实例变量,所在声明时不用加__block。不过如果通过读取或写入操作捕捉了实例变量,那么也会自动吧self变量一并捕获了。因为实例变量与self所指代的实例关联在一起。

int  (^addBlock)(int a, int b) = ^(int a,int b) {
    self->_titleString = @"xxxxx";
    _titleString = @"aaa";
    return a+ b + addtional;
};

如果不加self指代_titleString = @"aaa";,会有警告
Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior

它等同于self->_titleString = @"xxxxx";,可以通过属性访问:self.titleString = @"ccccc";

注:self也是个对象,因而在捕获它时也会将其保留,如果self所指代的那个对象同时也保留了块,那么这种情况就会导致保留环(循环引用)。

块的内部结构

块本身是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。

全局块,栈块,堆块

定义块的时候,其所占的内存区域是分配在栈中的,这就是说,块只有在定义它的那个范围内有效。

void (^block)(void);
if (addtional % 2 == 0) {
    block  = ^{
        NSLog(@"block a");
    };
} else {
    block = ^{
        NSLog(@"block b");
    };
}
block;

// 

定义在if和else语句中的两个块分配在栈内存中,编译器会给每个块分配好栈内存,等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是这两个块只能保证在if或else语句范围内有效。这样的代码可以编译,但是运行起来,时而正确,时而错误。若编译器未覆写执行的块,照常执行,如果覆写,则崩溃。

为解决此问题,可给块对象发送copy消息拷贝。把块从栈复制到堆,拷贝后的块可以在定义它的那个范围之外使用。并且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制都不会真的执行复制,只是递增块对象的引用计数。堆上的块服从引用计数规则,栈上的块无需明确释放,因为栈内存本来就自动回收。

//拷贝之后,代码就没有上面的安全问题了
void (^block)(void);
if (addtional % 2 == 0) {
    block  = [^{
        NSLog(@"block a");
    } copy];
} else {
    block = [^{
        NSLog(@"block b");
    } copy];
}
block();

除了栈块和堆块之外,还有全局块(global block)。这种块不会捕捉任何状态(比如外围变量等),运行时也无需有状态来参与,块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到的时候与栈内创建。

另外,全局块的拷贝操作是空操作,因为全局块不能为系统所回收,相当于单例。

void (^block) (void) = ^{
    NSLog(@"Block");
};

由于运行该块所需的全部信息都能在编译期确定,所以可把它做成全局块。这完全是优化技术:若把如此简单的块当成复杂的块处理,那就会在复制及丢弃该块时执行一些无谓的操作。


  • 块是C、C++、OC中的词法闭包
  • 块可以接受参数,可以返回值
  • 块可以分配在栈上、堆上,也可以是全局的。分配在栈上可拷贝到堆里,这样的话就和标准OC对象一样,具备引用计数。

38、为常用的块类型创建typedef

C语言中的类型定义(type definition)特性,typedef关键字用于给类型重新起一个别名。

可以为块起一个别名:

typedef int (^ZYDBlock)(BOOL flag,int value);


ZYDBlock sBlock = ^(BOOL flag,int value) {
    int result = 0;
    return result;
};
    
sBlock(YES,12);

实现块的方法,如果返回值不对,就会报相应错误:
Incompatible block pointer types initializing '__strong ZYDBlock' (aka 'int (^__strong)(BOOL, int)') with an expression of type 'void (^)(BOOL, int)'
实现块的方法,输入类型不对,报错:
Incompatible block pointer types initializing '__strong ZYDBlock' (aka 'int (^__strong)(BOOL, int)') with an expression of type 'int (^)(int)'

通过这项特性,可以把使用块的API做的更为易用些。类里的某些方法可能需要用块来做参数,不如执行异步任务时所用的completion handler,参数就是块,遇到这种情况,都可以通过定义别名使代买变得更为易读。

#import <Foundation/Foundation.h>

typedef void (^ ZYDCompletionHandler) (NSData *data,NSError *error);

@interface ZYDNetWorkFetcher : NSObject

- (void)startWithCompletionHandle:(ZYDCompletionHandler)completion;

@end

如果不定义块类型别名,则是这样的:

- (void)startWithCompletionHandles:(void (^)(NSData *data,NSError *error))completion;
相对而言比较复杂,不符合以往的方法定义习惯。

定义参数方法所用的块类型语法,可定义变量时不同。

使用类型定义还有个好处,当重构块的类型签名时会很方便。比如说要添加一个参数,只需要修改类型定义语句即可:

typedef void (^ ZYDCompletionHandler) (NSData *data,NSError *error,NSTimeInterval duration);
修改之后,凡是用了这个类型定义的地方,都无法编译,需要逐个修复。若不用类型定义,直接写块类型,那么代码中修改就多了,同时容易漏下一两处,出现问题。

最好在使用块类型的的类中定义这些typedef,而且还应该把这个类的名字加在由typedef所定义的新类型名前面,可以阐明块的用途,还可以用typedef给同一个块签名类型创建数个别名。在这件事上,多多益善。


  • 以typedef重新定义块类型,可令块变量用起来更简单。
  • 定义新类型时应遵从命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名明提供多个类型别名。如果重构代码使用了块类型的某被别名,那么只需要修改相应typedef中的块签名即可,无需改动其他typedef。

39、用handler块降低代码分散程度

为用户界面编码时,一种常用的范式就是异步执行任务(perform task asynchronously)。这种范式的好处是:处理用户界面的显示已触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程(main thread)。

iOS系统,系统监控器(system watchdog)在发现某个应用程序的主线程已经阻塞额一段时间之后,就会令其终止。所以避免一些任务阻塞主线程,需要采用一些异步的方式处理耗时任务。

异步方法在执行完任务之后,需要以某种手段通知相关代码。实现此功能的方法有很多。

  • 1、委托协议

声明代理协议

#import <Foundation/Foundation.h>

@class ZYDNetWorkFetcher;

@protocol ZYDNetworkFetcherDelegate <NSObject>

- (void)networkFetcher:(ZYDNetWorkFetcher *)fetcher didFinishWithData:(NSData *)data;

@end

被委托对象

#import <Foundation/Foundation.h>
#import "ZYDNetworkFetcherDelegate.h"

@interface ZYDNetWorkFetcher : NSObject

@property (nonatomic,weak) id<ZYDNetworkFetcherDelegate>delegate;
@end

委托对象

#import "ViewController.h"
#import "ZYDNetWorkFetcher.h"

@interface ViewController ()<ZYDNetworkFetcherDelegate>

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] init];
    fetcher.delegate = self;
}


#pragma mark -- ZYDNetworkFetcher Delegat
- (void)networkFetcher:(ZYDNetWorkFetcher *)fetcher didFinishWithData:(NSData *)data {
    
}

@end

遇到错误:

Undefined symbols for architecture x86_64:
  "l_OBJC_PROTOCOL_$_ZYDNetworkFetcherDelegate", referenced from:
      l_OBJC_CLASS_PROTOCOLS_$_ViewController in ViewController.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

报错原因:
在代理头文件里误用@protocol ZYDNetworkFetcherDelegate;应该是造成了命名冲突,同时没有引入ZYDNetworkFetcherDelegate.h的缘故。删除该行代码,重新引入代理头文件#import "ZYDNetworkFetcherDelegate.h"解决问题。

这种方法确实可行,没有错误,不过改由块来写的话,更清晰。

#import <Foundation/Foundation.h>

typedef void (^ ZYDCompletionHandler) (NSData *data,NSError *error);

@interface ZYDNetWorkFetcher : NSObject

- (void)startWithCompletionHandle:(ZYDCompletionHandler)completion;
@end
ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] init];
    
[fetcher startWithCompletionHandle:^(NSData *data, NSError *error) {
    //处理数据和错误
}];

将异步执行完毕后所需要运行的业务逻辑和启动异步任务所用的的代码放在一起,更整洁。由于块声明在创建获取器的范围内,所以他可以访问此范围内的所有变量。

委托模式有两个缺点:如果类要分别使用多个获取器下载不同数据,那么就得在delegate回调方法里传入的获取器参数来切换。这么做,不仅会让delegate回调方法变得很长,而且还要把网络数据获取器对象保存为实例变量,以便在判断语句中使用。这会使类的代码激增。代码块的好处是:无需保存获取器,也无需在回调方法里切换。每个completion handler的业务逻辑,都是和相关的获取器对象一起来定义的。

另外代码块还可以处理不同的数据,比如将正确结果与错误分开处理:

#import <Foundation/Foundation.h>

typedef void (^ ZYDCompletionSuccessHandler) (NSData *data);

typedef void (^ ZYDCompletionFailureHandler) (NSError *error);

@interface ZYDNetWorkFetcher : NSObject

- (void)startWithCompletionSuccess:(ZYDCompletionSuccessHandler)successHandler failure:(ZYDCompletionFailureHandler)failureHandler;

@end

ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] init];
    
[fetcher startWithCompletionSuccess:^(NSData *data) {
    
} failure:^(NSError *error) {
    
}];

只用一个块处理返回结果的好处:

1、处理更为灵活,比如传入错误信息时,可以把数据也传进来,尤其是在下载过程中,可以利用已经下载的数据,做些处理。

2、调用API的代码可能会在处理成功响应的过程中发现错误。

一般情况下,使用一个块来处理成功与失败的情况。

基于handler设计API还有一个原因,就是某些代码必须运行在特定的线程上。可以设计自己的PAI,根据API所处的细节层次,选做那个操作队列甚至GCD队列来作为参数。


  • 在创建对象时,可以使用内联的handler块将相关业务一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,如果该有handler来实现,可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可以通过此参数来决定应该把这个块安排在哪个队列上执行。

40、用块引用其所属对象时不要出现保留环

即不要出现循环引用。

  • 问题:
// ZYDNetWorkFetcher.m
@interface ZYDNetWorkFetcher()
@property (nonatomic,copy) ZYDCompletionHandler completeHandler;

@property (nonatomic,strong) NSData *downloadData;

@property (nonatomic,strong,readwrite) NSURL *url;
@end

@implementation ZYDNetWorkFetcher

- (instancetype)initWithUrl:(NSURL *)url {
    if (self = [super init]) {
        _url = url;
    }
    return self;
}

- (void)startWithCompletionHandle:(ZYDCompletionHandler)completion {
    self.completeHandler = completion;
}

- (void)p_requestCompleted {
    if (_completeHandler) {
        _completeHandler(_downloadData,nil);
    }
}

// viewController.m
@interface ViewController ()<ZYDNetworkFetcherDelegate>
{
    ZYDNetWorkFetcher *_fetcher;
    // self 持有fetcher
    NSData *_fetcherData;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _fetcher = [[ZYDNetWorkFetcher alloc] init];
    
    [_fetcher startWithCompletionHandle:^(NSData *data, NSError *error) {
        //处理数据和错误
        self->_fetcherData = data;
        // _fetcher持有 self
    }];
}

有一点说明:completion handler中引用实例变量时,会捕获self变量。

  • 解决方式:
    要么令_fetcher不再引用获取器,要么令获取器的CompletionHandle不再持有handler块。

调用方法更改,解开获取器与self的一个环。

NSURL *url = [NSURL URLWithString:@""];
ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] initWithUrl:url];
[fetcher startWithCompletionHandle:^(NSData *data, NSError *error) {
    //处理数据和错误
    self->_fetcherData = data;
}];

获取器与completionHandler的环,运行过completion handler后,不在保留handler。

- (void)p_requestCompleted {
    if (_completeHandler) {
        _completeHandler(_downloadData,nil);
    }
    self.completeHandler = nil;
}

设计API用到了这样的回调块,就很容易形成保留环,必须注意这个问题。一般来说,只要适时清理掉环中的而某个引用,即可解决此问题。

要想清楚块可能会捕获并保留哪些对象,如果这些对象又直接或间接保留了块,那么就要考虑怎样在适当的时间解除保留环。


  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
  • 一定要找个适当的机会解除保留环,而不能把责任推给API的调用者。

相关文章

网友评论

    本文标题:编写高质量iOS与OSX代码的52个有效方法-第六章:块Bloc

    本文链接:https://www.haomeiwen.com/subject/zkpjmftx.html