`

iOS开发-代码块的使用(二)

    博客分类:
  • ios
阅读更多

翻译自 http://pragmaticstudio.com/blog/2010/7/28/ios4-blocks-2
译者水平有限,建议阅读原帖

    在本系列的第一部分,我们学会了如何声明和调用基本的Objective-C代码块。动机是为了了解如何有效的使用iOS4提供的使用代码块作为参数的API。在这一部分我们将重点转向写我们自己的使用代码块的方法。通过理解在自己的代码中如何使用代码块,你将会掌握一种新的设计技术。而且你可能会意识到,代码块会使你的代码易于阅读和维护。
编写使用代码块的方法
    在第一部分我们留下了一个任务:写一个Work类的调用代码块的类方法,并且重复调用代码块指定的次数,还要处理每次代码块的返回值。如果我们想要得到1到5的三倍的话,那么下面是我们该如何调这个带有内联代码块的方法:
[Worker repeat:5 withBlock:^(int number) {
    return number * 3;
}];
    我经常这样设计一个类,首先写代码调用一个虚构的方法,这也是在提交之前一种形成API的简单方式,一旦认为这个方法调用正确,我就去实现这个方法。这样,那个方法的名字是repeat:withBlock:,我认为不合适(我知道在第一部分是叫这个名字,但我已经改变注意了)。这个名字容易使人混淆,因为该方法实际上并不是重复做相同的事情。这个方法从1迭代到指定的次数,并处理代码块的返回。所以让我们开始正确的重命名它:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * 3;
}];
    我对这个使用两个参数的方法的名字iterateFromOneTo:withBlock:很满意,一个int型参数表示调用代码块的次数和一个要被调用的代码块参数。现在让我们去实现这个方法。
    对于初学者,我么该如何声明这个 iterateFromOneTo:withBlock:方法呢?首先我们需要知道所有参数的类型,第一个参数很容易,是个int类型;第二个参数是一个代码块,代码块是有返回类型的。在这个例子中,这个方法可以接受任何有一个int型参数并返回int型结果的代码块作为参数。下面是实际的代码块类型:
int (^)(int)
    已经有了方法的名字和它的参数类型,我们就可以声明这个方法了。这是Worker类的类方法,我们在worker.h中声明它:
@interface Worker : NSObject {
}
 
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
 
@end
    第一眼看去,代码块参数不容易理解。有个要记住诀窍是:在Objective-C中所有的方法参数有两个部分组成。被括起来的参数类型以及参数的名称。这个例子中,参数的要求是一个是int型和一个是int(^)(int)型的代码块(你可以为参数命名为任意的名字,不一定非得是block)。这个方法的实现是在Worker.m文件文件中,比较简单:
#import "Worker.h"
 
@implementation Worker
 
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block {
    for (int i = 1; i <= limit; i++) {
        int result = block(i);
        NSLog(@"iteration %d => %d", i, result);
    }
}
 
@end
    方法通过一个循环来每次调用代码块,并打印出代码块的返回结果。记住一旦我们在作用域内有一个代码块变量,那么就可以像函数一样使用它。在这里代码块参数就是一个代码块变量。因此,当执行block(i)时就会调用传入的代码块。当代码块返回结果后会继续往下执行。现在我们可以使用内联代码块的方式调用iterateFromOneTo:withBlock:方法,像这样:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * 3;
}];
我们也可以不使用内联代码块的方式,传入一个代码块变量作为参数:
int (^tripler)(int) = ^(int number) {
    return number * 3;
};
 
[Worker iterateFromOneTo:5 withBlock:tripler];
不论那种方式,我们得到的输出如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
    当然我们可以传入进行任何运算的代码块。想要得到数字的平方吗?没问题,只要传入一个不同的代码块:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * number;
}];
现在我们的代码是可以运行的,下面将代码稍微整理下吧。
善于使用Typedef
    匆忙的声明代码块的类型容易混乱,即使在这个简单的例子中,函数指正的语法还是有许多不足之处:
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
    试想代码块要使用多个参数,并且有些参数是指针类型,这样的话你几乎需要完全重写你的代码。为了提高可读性和避免在.h和.m中出项重复,我们可以使用typedef修改Worker.h文件:
typedef int (^ComputationBlock)(int);
 
@interface Worker : NSObject {
}
 
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block;
 
@end
    typedef是C语言的一个关键字,其作用可以理解为将一个繁琐的名字起了一个昵称。在这种情况下,我们定义一个代码块变量ComputationBlock,它有一个int型参数和一个int型返回值。然后,我们定义iterateFromOneTo:withBlock:方法时,可以直接使用ComputationBlock作为代码块参数。同样,在Worker.m文件,我们可以通过使用ComputationBlock简化代码:
#import "Worker.h"
 
@implementation Worker
 
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block {
    for (int i = 1; i <= limit; i++) {
        int result = block(i);
        NSLog(@"iteration %d => %d", i, result);
    }
}
 
@end
    嗯,这样就好多了,代码易于阅读,没有在多个文件重复定义代码块类型。事实上,你可以使用ComputationBlock在你程序的任何地方,只要import “Worker.h”,你会碰到类似的typedef在新的iOS4的API中。例如,ALAssetsLibrary类定义了下面的方法:
- (void)assetForURL:(NSURL *)assetURL      
        resultBlock:(ALAssetsLibraryAssetForURLResultBlock)resultBlock 
       failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
    这个方法调用两个代码块,一个代码块时找到所需的资源时调用,另一个时没找到时调用。它们 的 typedef如下:
typedef void (^ALAssetsLibraryAssetForURLResultBlock)(ALAsset *asset);
typedef void (^ALAssetsLibraryAccessFailureBlock)(NSError *error);
    然后在你的程序中可以使用ALAssetsLibraryAssetForURLResultBlock和ALAssetsLibraryAccessFailureBlock去表示相应的代码块变量。
    我建议在写一个使用代码块的公用方法时就用typedef,这样有助于你的代码整洁,并可以让其他开发人员方便使用。
再来看一下闭包
    你应该还记得代码块是闭包,我们简要的讲述一下在第一部分提及的闭包。在第一部分闭包的例子并不实用,而且我说闭包在方法间传递时会变得特别有用。现在我们已经知道如何写一个实用代码块的方法,那么就让我们分析下另一个闭包的例子:
int multiplier = 3;
 
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * multiplier;
}];
    我们使用之前写的iterateFromOneTo:withBlock:方法,有一点不同的是没有将要得到的倍数硬编码到代码块中,这个倍数被声明在代码块之外,为一个本地变量。该方法执行的结果与之前一致,将1到5之间的数乘3:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
    这个代码的运行是一个说明闭包强大的例子。代码打破了一般的作用域规则。实际上,在iteratefromOneTo:withBlock:方法中调用multiplier变量,可以把它看作是本地变量。
    记住,代码块会捕捉周围的状态。当一个代码块声明时它会自动的对其内部用到的变量做一个只读的快照。因为我们的代码块使用了multiplier变量,这个变量的值被代码块保存了一份供之后使用。也就是说,multiplier变量已经成为了代码块状态啊的一部分。当代码块被传入到iterateFromOneTo:withBlock:方法,快的状态也传了进去。
    好吧,如果我们想在代码块的内部改变multiplier变量该怎么办?例如,代码块每次被调用时要让multiplier变为上一次计算的结果。你可能会试着在代码块里直接改变multiplier变量,像这样:
int multiplier = 3;
 
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    multiplier = number * multiplier;
    return multiplier;  // compile error!
}];
    这样的话是通不过编译的,编译器会报错“Assignment of read-only variable 'mutilplier'”。这是因为代码块内使用的是变量的副本,它是堆栈里的一个常量。这些变量在代码块中是不可改变的。
    如果你想要修改一个在块外面定义,在块内使用的变量时,你需要在变量声明时增加新的前缀_block,像这样:
__block int multiplier = 3;
 
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    multiplier = number * multiplier;
    return multiplier;
}];
 
NSLog(@"multiplier  => %d", multiplier);
这样代码可以通过编译,运行结果如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 18
iteration 4 => 72
iteration 5 => 360
multiplier  => 360
    要注意的是代码块运行之后,multiplier变量的值已经变为了360。换句话说,代码块内部修改的不是变量的副本。声明一个被_block修饰的变量是将其引用传入到了代码块内。事实上,被_block修饰的变量是被所有使用它的代码块共享的。这里要强调的一点是:_block不要随便使用。在将一些东西移入内存堆中会存在边际成本,除非你真的确定需要修改变量,否则不要用_block修饰符。
编写返回代码块的方法
    有时我们会需要编写一个返回代码块的方法。让我先看一个错误的例子:
+ (ComputationBlock)raisedToPower:(int)y {
    ComputationBlock block = ^(int x) {
        return (int)pow(x, y);
    };
    return block;  // Don't do this!
}
    这种方法简单的创建了一个计算y的x次幂的代码块然后返回它。它使用了我们之前通过typedef使用的ComputationBlock。下面是我们对所返回代码块的期望效果:
ComputationBlock block = [Worker raisedToPower:2];
block(3);  // 9
block(4);  // 16
block(5);  // 25
    在上面的例子中,我们使用的得到代码块,传入相应的参数,它应该会返回传入值的平方。但是当我们运行它时,会得到运行时错误”EXC_BAD_ACCESS”。
    怎么办?解决这个问题的关键是了解代码块是怎么分配内存的。代码块的生命周期是在栈中开始的,因为在栈中分配内存是比较块的。是栈变量也就意味着它从栈中弹出后就会被销毁。方法返回结果就会发生这样的情况。
    回顾我们的raisedToPower:方法,可以看到在方法中创建了代码块并将它返回。这样创建代码块就是已明确代码块的生存周期了,当我们返回代码块变量后,代码块其实在内存中已经被销毁了。解决办法是在返回之前将代码块从栈中移到堆中。这听起来很复杂,但是实际很简单,只需要简单的对代码块进行copy操作,代码块就会移到堆中。下面是修改后的方法,它可以满足我们的预期:
+ (ComputationBlock)raisedToPower:(int)y {
    ComputationBlock block = ^(int x) {
        return (int)pow(x, y);
    };
    return [[block copy] autorelease];
}
    注意我们使用了copy后就必须跟一个autorelease从而平衡它的引用计数器,避免内存泄露。当然我们也可以在使用代码块之后将其手动释放,不过这就不符合谁创建谁释放的原则了。你不会经常需要对代码块进行copy操作,但是如果是上面所讲的情况你就需要了,这点请留意。
将所学的整合在一起
    那么,让我们来把所学的东西整合为一个更实际点的例子。假设我们要设计一个简单的播放电影的类,这个类的使用者希望电影播放完之后能够接受一个用于展现应用特定逻辑的回调。前面已经证明代码块是处理回调很方便的方法。
让我们开始写代码吧,从一个使用这个类的开发人员的角度来写:
MoviePlayer *player = 
    [[MoviePlayer alloc] initWithCallback:^(NSString *title) {
        NSLog(@"Hope you enjoyed %@", title);
}];
 
[player playMovie:@"Inception"];
    可以看出我们需要MoviePlayer类,他有两个方法:initWithCallback:和playMovie:,初始化的时候接受一个代码块,然后将它保存起来,在执行playMovie:方法结束后再调用代码块。这个代码块需要一个参数(电影的名字),返回void类型。我们对回调的代码块类型使用typedef,使用property来保存代码块变量。记住,代码块是对象,你可以像实例变量或属性一样使用它。这里我们将它当作属性使用。下面是MoviePlayer.h:
typedef void (^MoviePlayerCallbackBlock)(NSString *);
 
@interface MoviePlayer : NSObject {
}
 
@property (nonatomic, copy) MoviePlayerCallbackBlock callbackBlock;
 
- (id)initWithCallback:(MoviePlayerCallbackBlock)block; 
- (void)playMovie:(NSString *)title;
 
@end
下面是MoviePlayer.m:
#import "MoviePlayer.h"
 
@implementation MoviePlayer
 
@synthesize callbackBlock;
 
- (id)initWithCallback:(MoviePlayerCallbackBlock)block {
    if (self = [super init]) {
        self.callbackBlock = block;
    }
    return self;
}
 
- (void)playMovie:(NSString *)title {
    // play the movie
    self.callbackBlock(title);
}
 
- (void)dealloc {
    [callbackBlock release];
    [super dealloc];
}
 
@end
    在initWithCallback:方法中将要使用的代码块声明为callbackBlock属性。由于属性被声明为了copy方式,代码块会自动进行copy操作,从而将其移到堆中。当playMovie:方法调用时,我们传入电影的名字作为参数来调用代码块。
    现在我们假设一个开发人员要在程序中使用我们的MoviePlayer类来管理一组你打算观看的电影。当你看完一部电影之后,这部电影就会从组中移除。下面是一个简单的实现,使用了闭包:
NSMutableArray *movieQueue = 
    [NSMutableArray arrayWithObjects:@"Inception", 
                                     @"The Book of Eli", 
                                     @"Iron Man 2", 
                                     nil];
 
MoviePlayer *player = 
    [[MoviePlayer alloc] initWithCallback:^(NSString *title) {
        [movieQueue removeObject:title];
}];
 
for (NSString *title in [NSArray arrayWithArray:movieQueue]) {
    [player playMovie:title];
};

    请注意代码块使用了本地变量movieQueue,它会成为代码块状态的一部分。当代码块被调用,就会从数组movieQueue中移除一个电影,尽管此时数组是在代码块作用域之外的。当所有的电影播放完成之后,movieQueue将会是一个空数组。下面是一些需要提及的重要事情:
1、movieQueue变量是一个数组指针,我们不能修改它的指向。我们修改的是它指向的内容,因此不需要使用_block修饰。
2、为了迭代movieQueue数组,我们需要创建一个它的copy,否则如果我们直接使用movieQueue数组,就会出现在迭代数组的同事还在移除它的元素,这会引起异常。
3、如果不使用代码块,我们可以声明一个协议,写一个代理类,并注册这个代理作为回调。很明显该例子使用内联代码块更方便。
4、在不改变MoviePlayer类的前提下可以给他增加新功能。比如另一个开发者可以在看完一部电影后将其分享到twitter或对电影进行评价等。
接下来做什么呢
这是这个系列的结束,谢谢您的阅读。关于代码块的知识还有很多,当然我们所学的已经够平常的使用了。如果你想对其做深入研究,我推荐以下资源:
A Short Practical Guide to Blocks by Apple
Blocks Programming Topics by Apple
Block Basics by Bill Bumgarner
Blocks Tips & Tricks by Bill Bumgarner
Friday Q&A: Blocks by Mike Ash
How blocks are implemented (and the consequences) by Matt Gallagher
Language Specification for Blocks
WWDC Session 206 - Introducing Blocks and Grand Central Dispatch on iPhone
    作为最后的一些提示,我希望你已经明白代码块是怎么提供了一种不同编码风格的,并且在你的程序设计中使用它。尝试着使用代码块,期待你的评论。
祝你愉快!
(Thanks to Matt Drance (@drance) and Daniel Steinberg (@dimsumthinking) for reviewing drafts of this article.)

分享到:
评论

相关推荐

    探索Objective-C中的Block:强大灵活的代码块

    Objective-C(通常缩写为Obj-C或OC)是一种通用的编程语言,它主要被用于苹果公司的操作系统,如macOS和iOS,以及它们的应用开发框架,如Cocoa和Cocoa Touch。Objective-C是C语言的一个超集,这意味着它包含了C语言...

    iOS开发--fmdb数据库

    在iOS应用开发中,数据库是存储和管理应用程序数据的关键组件。FMDB是一个流行且易于使用的Objective-C库,它为SQLite数据库提供了封装,使得在iOS应用中执行SQL操作变得简单。本篇文章将深入探讨如何在iOS项目中...

    ios 开发常用代码

    标题与描述中的“iOS开发常用代码”涉及到的是iOS应用程序开发中的常见编程实践和技术要点,主要聚焦于使用Objective-C或Swift语言进行UIKit框架下的界面元素定制和优化。以下将详细解析和扩展这部分内容所涵盖的...

    iOS开发进阶-完整目录-高清

    ARC是Objective-C的内存管理机制,帮助开发者管理内存而无需手动释放,而Block则是Objective-C中的闭包功能,能够方便地封装代码块。 综上所述,文档中涉及到的知识点涵盖了iOS开发的各种方面,从工具的应用、文档...

    iOS 开发Xcode代码块.zip

    下载完之后,解压打开readMe.txt文本,按照上面的操作或者按下面的说明: 首先终端执行: cd ~/Library/Developer/Xcode/UserData/ 将CodeSnippets文件夹拷贝到这个目录下 重启Xcode即可看到自定义的代码块

    2016年IOS培训-开发官方教程(汉化版)

    主要内容涵盖iOS开发环境的搭建、核心编程概念的学习、基础UI组件的操作、常见功能模块的实现、以及最终的应用测试与发布等环节。下面将详细阐述这些知识点。 #### 1. 开发环境搭建 - **Xcode**: Apple官方提供的...

    值得收藏的iOS开发常用代码块

    在iOS开发过程中,经常会遇到一些重复性的工作,这些工作可以通过编写一些常用的代码块来提高开发效率。以下是一些值得收藏的iOS开发常用代码块,涵盖了数据操作、界面适配、表格视图处理以及用户默认设置等方面。 ...

    iOS程序开发教程(PPT+代码)

    1. **MVC(模型-视图-控制器)模式**:Title Lecture 1 MVC and Intro to Objective-C深入介绍了iOS开发的核心设计模式——MVC。它将应用程序分为三个主要部分:模型负责数据管理,视图负责显示,控制器则协调两者...

    iOS开发-复用代码块(XcodeCodeSnippets)

    第二,较高的软件质量;第三,适当的使用复用可以改善系统的可维护性。复用不仅仅是代码的复用,代码复用只是复用的初等形式传统的复用:代码的剪贴复用,算法的复用,数据结构的复用。在一个面向对象的语言中,数据...

    ios-ios开发 短信验证码 加输入框.zip

    这个压缩包“ios-ios开发 短信验证码 加输入框.zip”似乎提供了一个预封装好的组件,用于快速集成短信验证码功能,特别是包含了输入框的界面元素。以下是对这个组件可能涉及的技术点的详细解释: 1. **短信验证码...

    iphone ios objective-c

    6. **Block(Block)**:Objective-C支持内联函数,即代码块,常用于异步操作。 ### iOS编程核心概念 1. **UIKit框架**:提供了构建用户界面的控件和功能,如UILabel、UIButton、UITableView等。 2. **App Delegate...

    iOSAPP分析无用代码

    通过运行Analyzer,我们可以找出那些从未被执行的代码块。 2. **Clang-Tidy**:这是一款现代化的源代码检查工具,它提供了大量的检查规则,用于查找不符合编码规范、可能导致问题或者可以优化的代码。例如,我们...

    iOS Objective-C 编码规文档

    Objective-C是iOS开发的核心编程语言,良好的编码规范能提升代码的可读性、可维护性,使团队协作更为高效。以下是对iOS Objective-C编码规范的详细说明: 1. **语言选择**: 在编写Objective-C代码时,建议使用...

    iOS GCD-Program-master

    在iOS开发中,Grand Central Dispatch(GCD)是Apple推出的一种多核处理器并行运算技术,用于优化应用程序的性能。GCD是基于C语言的,但可以无缝集成到Objective-C和Swift中。这个名为"IOS GCD-Program-master"的...

    很好用的Xcode qmui-ios-codesnippets

    安装过程通常是将解压后的文件夹移动到Xcode的代码片段库目录下,然后在Xcode中就可以直接使用这些预定义的代码块了。 代码片段库的使用非常便捷。在编写代码时,只需输入与片段关联的触发词,Xcode会自动提供补全...

    iOS -- DNS加密

    在iOS开发中,DNS(Domain Name System)加密是一种重要的安全技术,它旨在保护用户的网络查询隐私,防止中间人攻击和数据被监听。DNS加密通过使用特定的加密算法,确保了DNS查询和响应在传输过程中不被篡改或窃取。...

    IOS应用源码Demo-电子书阅读器 - iOS PDF Reader-毕设学习.zip

    【标题】"IOS应用源码Demo-电子书阅读器 - iOS PDF Reader-毕设学习.zip" 是一个专门针对iOS平台的电子书阅读器应用程序的源代码示例,它旨在帮助学生进行毕业设计学习。这个项目可能包含了一个完整的PDF阅读器应用...

Global site tag (gtag.js) - Google Analytics