您的位置:首页 > 移动开发 > IOS开发

Block

2016-06-08 21:50 281 查看
Objective-C编程(第2版)第28章 Block对象
Blocks
Block对象是一段代码。先给出一个Block对象的示例:
^{
    NSLog(@"This is an instruction within a block.");
}
看上去和C函数类似,都是在一个花括号内的一套指令。但是它没有函数名,相应的位置只有一个^符号。^表示这段代码是一个Block对象。
和函数一样,Block对象也可

Objective-C编程(第2版)以有实参和返回值。再给出一个Block对象的示例:
^(double dividend, double divisor) {
    double quotient = dividend / divisor;
    return quotient;
}
这段代码中的Block对象有两个实参,类型都是double,还返回一个double类型的值。
Block对象可以被当成一个实参来传递给可以接收block的方法。很多苹果的类都有可以接收block为实参的方法。
如果你有过其他编程语言的开发经验,则可能会将Block对象和匿名函数(anonymous function)、closure或lambda放在一起进行比较。如果你熟悉函数指针(function
pointer),那么Block对象也会看上去很熟悉。与函数指针相比,如果能正确地使用Block对象,就可以写出更简洁的代码。
创建一个新项目,类型为基于Foundation的Command Line Tool,名称为VowelMovement。VowelMovement将使用Block对象枚举数组中的字符串并移除所有的元音字母,并将去除了元音字母的字符串保存到一个新的数组中。
在main.m中,创建三个数组对象:一个用于保存最初的字符串;一个用于保存去除了元音字母的字符串;最后一个用于保存需要从字符串中移除的字符。
int main (int argc, const char * argv[])
{
    @autoreleasepool {

        // 创建两个数组对象,分别用于保存最初的字符串对象和去除元音字母后的版本
        NSArray *originalStrings = @[@"Sauerkraut", @"Raygun",
                                           @"Big Nerd Ranch", @"Mississippi"];

        NSLog(@"original strings: %@", originalStrings);

        NSMutableArray *devowelizedStrings = [NSMutableArray array];

        //  创建数组对象,保存需要从字符串中移除的字符
        NSArray *vowels = @[@"a", @"e", @"i", @"o", @"u"];

    }
    return 0;
}
这段代码没有新的知识点,仅仅是创建并设置数组对象。构建并运行程序,编译器会发出警告,提醒有未使用的变量,暂时忽略之。
28.1 使用Block对象
Using blocks
马上你就要编写自己的第一个Block对象。这个Block对象会复制一个给定的字符串,并移除给定字符串的所有元音字母,然后将去除了元音字母的字符串保存到devowelizedStrings数组中。
你将使用该Block对象作为实参,给originalStrings数组发送消息。首先我们要学习一些block语法。声明Block变量
Block对象可以用变量保存。将以下代码加入main.m,声明Block变量:
int main (int argc, const char * argv[])
{
    @autoreleasepool {
        // 创建两个数组对象,分别用于保存最初的字符串对象和去除元音字母后的版本
        NSArray *oldStrings = [NSArray arrayWithObjects:
            @"Sauerkraut", @"Raygun", @"Big Nerd Ranch", @"Mississippi", nil];

        NSLog(@"old strings: %@", oldStrings);

        NSMutableArray *newStrings = [NSMutableArray array];

        // 创建数组对象,保存需要从字符串中移除的字符
Objective-C编程(第2版)NSArray *vowels = [NSArray arrayWithObjects:
            @"a", @"e", @"i", @"o", @"u", nil];

        // 声明Block变量
        void (^devowelizer)(id, NSUInteger, BOOL *);

    }
    return 0;
}
下面对这段代码中的Block变量声明做一个详细的介绍。Block变量的名字(如devowelizer)是写在括号中,跟在^字符后面的。Block的声明需要包括Block的返回类型(void)以及它的实参的类型(id、NSUINteger、Bool*),这点类似函数的声明(见图28.1)。图28.1 Block变量声明
那么Block变量是什么类型的呢?它不是一个简单的“block(块)”。它的类型是一个有着三个参数(一个对象指针、一个整数和一个BOOL指针),并且没有返回值的Block对象。这是enumerateObjectsUsingBlock:方法期望的Block类型。下面将介绍这三个实参的用法。编写Block对象
现在你要写一个Block对象,并将它赋给新的变量。在main.m中,编写一个方法复制原始字符串,并移除原始字符串的所有的元音字母,然后将去除了元音字母的字符串保存到devowelizedStrings数组中,最后将它赋给devowelizer,代码如下:
int main (int argc, const char * argv[])
{
    @autoreleasepool {
        ...

        // 声明Block变量
        void (^devowelizer)(id, NSUInteger, BOOL *);

        //  将Block对象赋给变量
        devowelizer = ^(id string, NSUInteger i, BOOL *stop) {

            NSMutableString *newString = [NSMutableString stringWithString:string];

            // 枚举数组中的字符串,将所有出现的元音字母替换成空字符串
            for (NSString *s in vowels) {
                NSRange fullRange = NSMakeRange(0, [newString length]);
                [newString replaceOccurrencesOfString:s
                                               withString:@""
                                                  options:NSCaseInsensitiveSearch
                                                     range:fullRange];
            }

            [devowelizedStrings addObject:newString];

        }; // Block变量赋值结束

    }
    return 0;
}
注意,与其他的变量赋值一样,Block变量的赋值也需要以分号结束。再次构建程序,之前的警告信息(有未使用的变量)应该会消失。
此外,和其他变量一样,也可以将devowelizer的声明和赋值写在一起,代码如下:
void (^devowelizer)(id, NSUInteger, BOOL *) = ^(id string, NSUInteger i,
    BOOL *stop) {

    NSMutableString *newString = [NSMutableString stringWithString:string];

    // 枚举数组中的字符串,将所有的元音字母替换成空字符串
    for (NSString *s in vowels) {
        NSRange fullRange = NSMakeRange(0, [newString length]);
        [newString replaceOccurrencesOfString:s
                                   withString:@""
                                      options:NSCaseInsensitiveSearch
                                        range:fullRange];
    }

    [newStrings addObject:newString];
};
传递Block对象
在main.m中,调用enumerateObjectsUsingBlock:并传入devowelizer,然后输出去除了元音

Objective-C编程(第2版)字母的字符串。
int main (int argc, const char * argv[])
{
    @autoreleasepool {
        …
        // 创建两个数组对象,分别用于保存最初的字符串对象和去除元音字母后的版本
        NSArray *oldStrings = [NSArray arrayWithObjects:
               @"Sauerkraut", @"Raygun", @"Big Nerd Ranch", @"Mississippi", nil];
        NSLog(@"old strings: %@", oldStrings);
        NSMutableArray *newStrings = [NSMutableArray array];

        // 创建数组对象,保存需要从字符串中移除的字符
        NSArray *vowels = [NSArray arrayWithObjects:
                              @"a", @"e", @"i", @"o", @"u", nil];

        // 声明Block变量
        void (^devowelizer)(id, NSUInteger, BOOL *);

        // 将Block对象赋给变量
        devowelizer = ^(id string, NSUInteger i, BOOL *stop) {

            NSMutableString *newString = [NSMutableString stringWithString:string];

            // 枚举数组中的字符串,将所有出现的元音字母替换成空字符串
            for (NSString *s in vowels) {
                NSRange fullRange = NSMakeRange(0, [newString length]);
                [newString replaceOccurrencesOfString:s
                                           withString:@""
                                              options:NSCaseInsensitiveSearch
                                                range:fullRange];
            }

            [newStrings addObject:newString];

        }; // Block变量赋值结束

        // 枚举数组对象,针对每个数组中的对象,执行Block对象devowelizer
        [oldStrings enumerateObjectsUsingBlock:devowelizer];
        NSLog(@"new strings: %@", newStrings);

    }
    return 0;
}
构建并运行程序,程序应该会输出以下两个数组。
2011-09-03 10:27:02.617 VowelMovement[787:707] old strings: (
    Sauerkraut,
    Raygun,
    "Big Nerd Ranch",
    Mississippi
)
2011-09-03 10:27:02.618 VowelMovement[787:707] new strings: (
    Srkrt,
    Rygn,
    "Bg Nrd Rnch",
    Msssspp
)
enumerateObjectsUsingBlock:方法要求传入的Block对象的三个实参类型是固定的。第一个实参是对象指针,指向当前(枚举)的对象。该指针的类型是id,所以无论数组包含的是什么类型的对象,都可以将地址赋给该指针。第二个实参的类型是NSUInteger,其值是当前对象在数组中的索引。第三个实参是指向BOOL变量的指针,该变量的默认值是NO。如果将该值设为YES,那么数组对象会在执行完当前的Block对象后终止枚举过程。修改block的代码,检查字符串是否包含字符y(包含大小写),如果有,则设置指针指向YES(会阻止Block对象进行枚举),终止枚举。
  NSRange yRange = [string rangeOfString:@"y"
                                   options:NSCaseInsensitiveSearch];

    // 是否包含字符'y'?
    if (yRange.location != NSNotFound) {
        *stop = YES; // 执行完当前的Block对象后终止枚举过程
        return;       // 结束当前正在执行的Block对象
    }

    NSMutableString *newString = [NSMutableString stringWithString:string];

    // 枚举数组中的字符串,将所有出现的元音字母替换成空字符串
    for (NSString *s in vowels) {
         NSRange fullRange = NSMakeRange(0, [newString length]);
         [newString replaceOccurrencesOfString:s
                                   withString:@""
                                      options:NSCaseInsensitiveSearch
                                        range:fullRange];
    }

    [newStrings addObject:newString];

}; // Block变量赋值结束

10aa6
建并运行程序,程序应该还是会输出两组对象。但是,对于第二个数组,因为程序会在找到y字符后终止枚举过程,所以输出结果只有Srkrt。
Objective-C编程(第2版)typedef
Block对象的语法可能会比较复杂。通过使用第11章介绍过的typedef关键字,可以将某个Block对象类型定义为一个新类型,以方便使用。需要注意的是,不能在方法的实现代码中使用typedef。也就是说,应该在实现文件的顶部,或者头文件内使用typedef。在main.m中,添加以下代码:
#import <Foundation/Foundation.h>

typedef void (^ArrayEnumerationBlock)(id, NSUInteger, BOOL *);

int main (int argc, const char * argv[])
{
这段代码中的typedef语句看上去与Block变量声明很像,但是,这里定义的是一个新的类型,而不是变量。跟在^字符后面的是类型名称。创建这个新类型后,就能简化相应Block对象的声明。
现在使用新的类型声明devowelizer:
int main(int argc, const char * argv[])
{

    @autoreleasepool {

        ...

        // 声明Block变量
        void (^devowelizer)(id, NSUInteger, BOOL *);
        ArrayEnumerationBlock devowelizer;

        // 将Block对象赋给变量
        devowelizer = ^(id string, NSUInteger i, BOOL *stop) {
            ...
注意,这里的Block类型只是声明了Block对象的实参和返回类型,并没有实现真正的Block对象。
28.2 Block对象vs.其他回调
Blocks vs. other callbacks
第27章曾介绍过两种回调机制:委托机制(delegation)和通告机制(notifications)。通过回调机制,程序能够在特定事件发生时调用指定的方法。虽然以上两种回调机制能够很好地完成任务,但是也有一个缺点,即回调的设置代码和回调方法的具体实现无法写在同一段代码中。而且这两段代码经常会间隔很远,甚至会出现在不同的文件中。以第27章所介绍的Callbacks为例,以下代码会回调zoneChange:方法:
[[NSNotificationCenter defaultCenter]
                         addObserver:logger
                            s    elector:@selector(zoneChange:)
                                    name:NSSystemTimeZoneDidChangeNotification
                               object:nil];
阅读这段代码的人很自然地会问,“zoneChange:方法是做什么的?”为了回答这个问题,程序员必须要找到zoneChange:方法的实现,而这段代码可能相隔很多行代码。
然而,通过Block对象,将与回调相关的代码写在同一代码段中。例如,NSNotificationCenter有一个addObserverForName:object:queue:usingBlock:方法。这个方法和addObserver:selector:name:object:类似,但是它可以使用Block对象作为实参,而不使用选择器。调用addObserverForName:object:queue:
usingBlock:之后,下一行代码就可以定义Block对象。这样其他的程序员阅读这段代码就非常方便。
本章最后的练习2就是让你使用Block对象改写Callbacks程序的。

Objective-C编程(第2版)28.3 深入学习Block对象
More on blocks
下面将介绍Block对象的其他功能。
返回值
为VowelMovement创建的Block对象没有返回值,但是很多其他的Block对象有。对于有返回值的Block对象,可以像调用函数那样调用Block对象,然后使用其返回值。
让我们再一起看看本章开头介绍的Block对象示例:
^(double dividend, double divisor) {
    double quotient = dividend / divisor;
    return quotient;
}
这个Block对象有两个类型为double的实参,返回一个double类型的值。要在变量中保存这个Block,需要声明一个double类型的变量,然后再将Block赋值给这个变量:
// 声明divBlock变量
double (^divBlock)(double,double);

// 将Block对象赋给变量
divBlock = ^(double dividend, double divisor) {
    double quotient = dividend / divisor;
    return quotient;
}
你可以像调用函数一样调用divBlock,得到它的返回值:
double myQuotient = divBlock(42.0, 12.5);
匿名Block对象
匿名的Block对象是可以传递给方法的Block对象的,而不需要先赋值给变量。
让我们先看看匿名的整数。有三种方法可以将整数传递给方法:
//方法1:
声明、赋值和使用完全分开
int i;
i = 5;
NSNumber *num = [NSNumber numberWithInt:i];

// 方法2:
在一行中声明赋值使用
int i = 5;
NSNumber *num = [NSNumber numberWithInt:i];

//方法3:跳过变量声明步骤
NSNumber *num = [NSNumber numberWithInt:5];
如果采用第三种方法,就是匿名地传递一个整数。因为它没有名字,所以说它是匿名的。
而将Block对象传递给方法的办法和传递整数相同。分别用三行代码来声明Block对象,然后赋值,最后使用。但是匿名传递Block对象更加常用。本章最后的练习1就是使用一个匿名的Block对象来修改VowelMovement程序。
外部变量
Block对象通常会(在其代码中)使用外部创建的其他变量(基本类型的变量,或者是指向其他对象的指针)。这些外部创建的变量叫做外部变量(external variables)。当执行Block对象时,为了确保其下的外部变量能够始终存在,相应的Block对象会捕获(captured)这些变量。
对基本类型的变量,捕获意味着程序会拷贝变量的值,并用Block对象内的局部变量保存。对指针类型的变量,Block对象会使用强引用。这意味着凡是Block对象用到的对象,都会被保留。所以在相应的Block对象被释放前,这些对象一定不会被释放(这也是Block对象和函数之间的差别,函数无法做到这点)。
在Block对象中使用self
如果需要写一个使用self的Block对象,就必须要多做几步工作来避免造成强引用循环。考虑一下这个例子,BNREmployee实例创建了一个Block对象,每次执行的时候就会打印出这个BNREmployee

Objective-C编程(第2版)实例:
myBlock = ^{
    NSLog(@"Employee: %@", self);
};
BNREmployee有一个指向Block对象(myBlock)的指针。这个Block对象会捕获self,所以它有一个指回BNREmployee实例的指针。现在就陷入强引用循环了。
为了打破这个强引用循环,可以先在Block对象外声明一个__weak指针;然后将这个指针指向Block对象使用的self;最后在Block对象中使用这个新的指针:
__weak BNREmployee *weakSelf = self; // 一个弱引用指针
myBlock = ^{
    NSLog(@"Employee: %@", weakSelf);
};
现在这个Block对象对BNREMployee实例是弱引用,强引用循环打破了。
然而,由于是弱引用,所以self指向的对象在Block执行的时候可能会被释放。
为了避免这种情况的发生,可以在Block对象中创建一个对self的局部强引用:
__weak BNREmployee *weakSelf = self; // 弱引用
myBlock = ^{
    BNREmployee *innerSelf = weakSelf; // 局部强引用
    NSLog(@"Employee: %@", innerSelf);
};
通过创建innerSelf强引用,就可以在Block和BNREmployee实例中再次创建一个强引用循环。但是,由于innerSelf引用是针对Block内部的,所以只有在Block执行的时候它才会执行,而Block结束之后就会自动消失。
每次写Block对象的时候都引用self会是一个很好的练习。
在Block对象中无意使用self
如果直接在Block对象中使用实例变量,那么block会捕获self,而不会捕获实例变量。这是实例变量的一个鲜为人知的特点。例如,以下这段代码直接存取一个实例变量:
__weak BNREmployee *weakSelf = self;
myBlock = ^{
    BNREmployee *innerSelf = weakSelf; // 局部强引用
    NSLog(@"Employee: %@", innerSelf);
    NSLog(@"Employee ID: %d", _employeeID);
};
编译器是这么解读这段代码的:
__weak BNREmployee *weakSelf = self;
myBlock = ^{
    BNREmployee *innerSelf = weakSelf; // 局部强引用
    NSLog(@"Employee: %@", innerSelf);
    NSLog(@"Employee ID: %d", self->_employeeID);
};
->语法看上去是不是很熟悉?这个语法实际是用来后去堆上的成员结构的。从最底层来说,对象实际就是结构。
由于编译器将_employeeID看成是self->_employeeID,self就被Block对象无意地捕获了。这样又会造成之前使用weakSelf和innerSelf避免的强引用循环。
怎样解决呢?不要直接存取实例变量。使用存取方法!
__weak BNREmployee *weakSelf = self;
myBlock = ^{
    BNREmployee *innerSelf = weakSelf; // 局部强引用
    NSLog(@"Employee: %@", innerSelf);
    NSLog(@"Employee ID: %d", innerSelf.employeeID);
};
现在没有直接地使用self了,就不会造成无意识地强引用循环。
在这种情况下,重要的是要理解编译器是如何思考的,这样才能避免隐藏的强引用循环。然而,绝不要使用->语法来存取对象的实例变量。这么做非常危险,可能会超越这个Block对象的范围。存取方法是你的小伙伴,应该坚持使用它们。
修改外部变量
在Block对象中,被捕获的变量是常数,程序无法修改变量所保

Objective-C编程(第2版)存的值。
如果需要在Block对象内修改某个外部变量,则可以在声明相应的外部变量时,在前面加上__block关键字。
例如,以下代码可以在Block对象内将外部变量counter的值增加1。
__block int counter = 0;
void (^counterBlock)() = ^{ counter++; };
...
counterBlock(); // counter增加1,数值为1
counterBlock(); // counter增加1,数值为2
如果这段代码没有使用__block关键字,那么编译器会在Block对象的定义处报错。28.4 练习
Challenge
练习1:匿名Block对象
试修改本章中的代码,要求在调用enumerateObjectsUsingBlock:方法时使用匿名Block对象。也就是说,保留Block对象,但是不声明Block变量。
练习2:NSNotificationCenter
第27章的Callbacks程序使用NSNotificationCenter的addObserver:selector:
name:object:方法注册了一个观察者要求当指定的通知发生时调用zoneChange:方法。Callbacks程序以此实现了回调机制。试修改Callbacks程序,改用addObserverForName:object:queue:usingBlock:方法。
addObserverForName:object:queue:usingBlock:方法要求传入一个Block对象。当指定的通知发生时,通知中心就会执行这个Block对象,而不是与观察者打交道。这意味着修改后的程序永远不会调用zoneChange:方法,所以要将相应的代码移入Block对象中。
对传入addObserverForName:object:queue:usingBlock:方法的Block对象,需要有一个参数(类型为NSNotification *),并且没有返回值。这一点与zoneChange:方法相同。
queue:参数可以传入nil,该参数与并发(concurrency)有关,本书不做讨论。
关于addObserverForName:object: queue:usingBlock:方法,可以在开发文档中查找关于该方法的详细说明
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  iOS Block