Objective-c 05 类别 类扩展 委托 非正式协议 协议 代码块 并发性 队列
类别
为已经存在的类添加行为时,通常采用创建子类的方法,不过有时子类并不方便,
比如:创建NSString的子类,但是NSString实际上只是一个类簇的表面形式。因而为这样的类创建子类会非常困难。在其他情况下,
也许可以创建它的子类,但是用到的工具集和库无法帮你处理新类的对象的。例如:当使用stringWithFormat:类方法生成新字符串时,你创建的
NSString类的新子类就无法返回。
利用Objective-C的动态运行时分配机制,可以为现有的类添加新的方法。这些新的方法在Objective-C里被称为类别(category).
类别是一种为现有的类添加新方法的方式。
程序员总是习惯把类别代码放在独立的文件中,通常会以“类名称+类别名称” 的风格命名。
开始创建类别:
打开项目,选择想让文件出现的群组,接下来可以选择File->New->New File,在新窗口的左侧选择Cocoa并在右侧选择Objective-C category。然后按照你要创建的类别进行填写并创建。
@interface部分
类别的声明看起来非常像类的声明:
@interface NSString (NumberConvenience)
-(NSNumber *) lengthAsNumber;
@end // NumberConvenience
该声明具有两个非常有趣的特点:
1、类的名称后面是位于括号中的新名称,这意味着类别叫做NumberConvenience,而且它是添加给NSString类的。只要保证类别名称唯一,可以向一个类中添加任意数量的类别。
2、其次,可以指定想要添加类别的类(如NSString)和类别的名称(如NumberConvenience),还可以列出要添加的方法,最后以@end结束。由于不能添加新的实例变量,因此这里不会像类声明那样包含实例变量的声明部分。
可以在类别中添加属性,但是不能添加实例变量,而且属性必须是@dynamic类别的。添加属性的好处在于你可以通过点表达式来访问setter和getter方法。
@implementation部分
@implementation NSString (NumberConvenience)
-(NSNumber *) lengthAsNumber
{
NSUInteger length = [self length];
return ([NSNumber numberWithUnsignedInteger: length]);
} // lengthAsNumber
@end // NumberConvenience
与类别的@interface部分相似,@implementation部分也包含类名、类别名以及新方法的实现。
[self length]来获取字符串的长度值。向该字符串发送lengthAsNumber消息,就会创建出一个包含了长度值的新NSNumber对象。
对于这段代码的内存管理,由于numberWithUnsignedInt不属于alloc、copy、new方法,因此它返回的对象可能是保留计数器的值为1并且已经被设置为自动释放了。在当前活动的自动释放池被销毁时,我们创建的这个NSNumber对象也会被清理。
main代码:
int main(int argc, const char* argv[])
{
@autoreleasepool
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject: [@"hello" lengthAsNumber] forKey: @"hello"];
[dict setObject: [@"iLikeFish" lengthAsNumber] forKey: @"iLikeFish"];
[dict setObject: [@"Once upon a time" lengthAsNumber] forKey: @"Once upon a time"];
NSLog(@"%@", dict);
}
return 0;
}
#import "NSString+NumberConience.h"
向main()函数导入头文件,这样才能知道我们拥有这个为NSString类而定义的方法。
@autoreleasepool
{
--创建一个自动释放池,这是启用了ARC的方法。最后末尾要加上花括号。
[dict setObject: [@"hello" lengthAsNumber] forKey: @"hello"];
请记住@"字符串"这种形式的字符串实际上就是地地道道的NSString对象。因为我们已经为NSString类创建了这个类别,所以任何字符串(包括像这样的字面量类型)都能响应lengthAsNumber消息。
类别的缺陷
类别有两个局限性:
1、无法向类中添加新的实例变量。类别没有空间容纳实例变量。
2、就是名称冲突。也即是类别中的方法与现有的方法重名。当发生名称冲突时,类别具有更高的优先级。你的类别方法将完全取代初始化方法,导致初始化方法不再可用。可以在类别方法中添加一个前缀,以确保不会发生名称冲突。
类别的优势:
在Cocoa中,类别主要有三个用途:
1、将类的实现代码分散到多个不同文件或框架中。
2、创建对私有方法的前向引用。
3、以及向对象添加非正式协议(informal protocol).
一个特殊的类别,它有一个特殊的名称:类扩展(class extension)
这个类别的特点之一就是不需要名字。之前都要为类别命名并且会在定义@interface部分的时候使用这个名字,而这个特殊的类扩展类别不需要命名,其特点如下:
1、不需要名字
2、可以在包含你的源代码的类(也就是自己的类)中使用它。
3、可以添加实例变量
4、将只读权限改成可读写的权限。
5、创建数量不限。
Things的类:
@interface Things : NSObject
@property (assign) NSInteger thing1;
@property (readonly, assign) NSInteger thing2;
-(void) resetAllValues;
@end
类扩展后的代码:
@interface Things ()
{
NSInteger thing4;
}
@property (readwrite, assign) NSInteger thing2;
@property (assign) NSInteger thing3;
@end
这看起来就像定义一个类,只不过这里没有继承的父类。我们所做的基本上就是获取Things类,并通过添加私有属性和方法来扩展它。这就是它被称为类扩展的原因。
对
于thing2属性,我们在类扩展中改变了它的读写权限。标记为readwrite,这样编译器就会生成setter方法了,不过它是只能在这个类中访问
的私有方法,在公共的接口则只有getter方法。同样也添加了私有属性thing3,只可以在这个类内部使用。thing4也是私有的。
这段代码的内存管理:因为使用了ARC,所以不需要释放对象。没有内存泄露问题。
可以拥有多个类扩展类别,不过这样会引发很难察觉的bug,所以理智使用。
使用类别分散实现代码:
如果想将大型的单个类分散到多个不同的.m文件中,可以使用类别。
在项目中使用类别:
CategoryThing.h
#import <Foundaton/Foundation.h>
@interface CategoryThing : NSObject
{
NSInteger thing1;
NSInteger thing2;
NSInteger thing3;
}
@end // CategoryThing
类声明后是3个类别声明,将这些实现代码放入不同的文件中。
@interface CategoryThing (Thing1)
-(void)setThing1:(NSInteger)thing1;
-(NSInteger)thing1;
@end // CategoryThing (Thing1)
@interface CategoryThing (Thing2)
-(void)setThing2:(NSInteger)thing2;
-(NSInteger)thing2;
@end // CategoryThing (Thing2)
@interface CategoryThing (Thing3)
-(void)setThing3:(NSInteger)thing3;
-(NSInteger)thing3;
@end // CategoryThing (Thing3)
以上就是CategoryThing.h的代码。
CategoryThing.m:
#import "CategoryThing.h"
@implementation CategoryThing
-(NSString *) description
{
NSString *desc;
desc = [NSString stringWithFormat: @"%d %d %d", thing1, thing2, thing3];
return (desc);
} // description
@end // CategoryThing
看下内存管理:
description方法能够处理好内存管理释放,因为stringWithFormat不同alloc、copy和new,所以可以假设它返回的对象的保留计数器的值为1且已经被设置为自动释放。在当前的自动释放池被销毁时,该对象将被清理。
各个类别的实现代码:
Thing1.m:
#import "CategoryThing.h"
@implementation CategoryThing (Thing1)
-(void)setThing1: (NSInteger) t1
{
thing1 = t1;
} // setThing1
-(NSInteger) thing1
{
return (thing1);
} // thing1
@end // CategoryThing
类别可以访问其继承的类的实例变量。类别的方法具有更高的优先级。
Thing2.m的内容与Thing1.m非常相似:
#import "CategoryThing.h"
@implementation CategoryThing (Thing2)
-(void)setThing2: (NSInteger) t2
{
thing2 = t2;
} // setThing2
-(NSInteger) thing2
{
return (thing2);
} // thing2
@end // CategoryThing
main.m文件的main()函数:
首先导入:
#import "CategoryThing.h"
int main (int argc, const char* argv[])
{
@autoreleasepool {
CategoryThing *thing = [[CategoryThing alloc] init];
[thing setThing1: 5];
[thing setThing2: 23];
[thing setThing3: 42];
NSLog(@"Things are %@ ", thing);
}
return (0);
} // main
@autoreleasepool {--该自动释放池将用于存放NSLog()函数调用的description方法所返回的会自动释放的字符串。
CategoryThing *thing = [[CategoryThing alloc] init];
在创建新对象时对内存管理进行分析:因为这里使用的是alloc方法,该thing对象的保留计数器的值为1,它未被放入自动释放池中.因为项目使用了ARC特性,所以不需要担心在哪里添加release语句.编译器会在正确的位置帮我们添加好.
类别不仅可以将一个类的实现代码分散到多个不同的源文件中,还可以分散到多个不同的框架中.
通过类别创建前向引用
编译器发现你调用对象的某个方法,但是却没有找到该方法的声明或定义,那么它将发出错误的报告.通常这种错误提示有益于你编程,但是如果你的某些方法的实现使用了在类的@interface部分未列出的方法,编译器的这种警惕性会带来一些问题.
这个问题可以通过类别来解决,只要在类别中声明一个方法,编译器就会表示:"该方法已经存在,如果遇到编程人员使用该方法,我不会提出警告".实际上,如果你不想实现这个方法的话,放着不管就可以了.
常用的策略是将类别置于实现文件的最前面.
假
设Car类有一个名为rotateTires的方法.我们可以使用另一个名为moveTireFromPosition:toPosition:的方法来
实现rotateTires方法。第二个方法是实现细节,而不是将其放在Car类的公共接口中。通过在类别中声明
moveTireFromPosition:toPosition:方法,rotateTires方法可以使用它,但不会引发编译器产生警告:
该类别如下:
@interface Car (Private)
-(void) moveTireFromPosition: (int) pos1 toPosition: (int) pos2;
@end // Private
当
你实现moveTireFromPosition:toPosition:方法时,它不一定要出现在@implementation
Car(PrivateMethods)代码块中,不过如果没有的话,编译器会给你一个警告,所以良好的习惯是把它们放入各自的
@implementation中。
非正式协议和委托类别
Cocoa中的类经常会使用一种名为委托(delegate)的技术,委托是一种对象,由另一个类请求执行某些工作。
最常见的情况是:编写委托对象并且将其提供给其他一些对象,通常是Cocoa框架中的对象。通过实现特定的方法,你可以控制Cocoa中对象的行为。
iTunesFinder项目:
#import <Foundation/Foundation.h>
#import "ITunesFinder.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
NSNetServiceBrowser *browser = [[NSNetServiceBrowser alloc] init];
ITunesFinder *finder = [[ITunesFinder alloc] init];
[browser setDelegate:finder];
//告诉网络浏览器使用ITunesFinder类的对象作为委托对象。
[browser searchForServicesOfType:@"_daap._tcp"
inDomain:@"local."];
NSLog (@"begun browsing");
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
--
字
符串“_daap._tcp”告诉了网络服务浏览器使用TCP网络协议去搜索DAAP(Digital Audio Access
Protocol,数字音频访问协议)类型的服务。该语句可以找到由iTunes发布的库。域“local.”表示只在本地网络中搜索该服务。互联网编号
分配机构(Internet Assigned Numbers Authority,
IANA)维护一个互联网协议族列表,该列表通常被映射为Bonjour服务名称。
[[NSRunLoop currentRunLoop] run];-- run循环是一种Cocoa概念,它在等待某些事情发生之前一直处于阻塞状态,既不执行任何代码。
在本示例中,它等待的事情是指网络服务浏览器发现了新的iTunes共享。
除了监听网络流量以外,run循环还处理像等待用户事件(如按键或鼠标单击)之类的其他操作。事实上,run方法将一直保持运行而不会返回,因此其后的代码永远也不会被执行。不过我们还是留下了这段代码,以便浏览代码的人能够知道我们一直在进行适当的内存管理。
因此,下面的清理代码实际上不会执行。
}
return 0;
ITunesFinder委托的代码:
#import <Foundation/Foundation.h>
@interface ITunesFinder : NSObject <NSNetServiceBroserDelegate>
@end // ITunesFinder
请记住,并不一定要在@interface中声明方法。委托对象只需实现已经打算调用的方法。
需要注意在NSObject后面使用了<NSNetServiceBroserDelegate>协议。它告诉编译器和其他对象,ITunesFinder类符合这个名称的协议并且实现了其方法。
ITunesFinder.m:
#import "ITunesFinder.h"
@implementation ITunesFinder
// 第一个委托方法:
- (void) netServiceBrowser:(NSNetServiceBrowser *) b
didFindService:(NSNetService *) service
moreComing:(BOOL) moreComing
{
[service resolveWithTimeout:10];
NSLog (@"found one! Name is %@", [service name]);
} // didFindService
当NSNetServiceBrowser对象发现一个新服务时,它将向委托对象发送
netServiceBrowser:didFindService:moreComing:消息。浏览器被作为第一个参数(与main函数中
browser变量的值相同)传递。如果有多个服务浏览器在同时进行搜索,你可以利用参数来分析哪个浏览器发现了新的服务。
第二个参数传递的NSNetService类的对象,描述了被发现的服务。最后一个参数moreComing用于标记一批通知是否已经完成。
[service resolveWithTimeout:10];
--告诉Bonjour系统获取关于该服务的所有有趣的属性。[service name]为我们获取该共享的名称。
// ITunesFinder类实现了第二个委托方法,该方法在某个网络服务消失时被调用。
- (void) netServiceBrowser:(NSNetServiceBrowser *) b
didRemoveService:(NSNetService *) service
moreComing:(BOOL) moreComing
{
[service resolveWithTimeout:10];
NSLog (@"lost one! Name is %@", [service name]);
} // didRemoveService
@end // ITunesFinder
该方法与didFindService方法非常相似,只不过它是在某个服务不再可用时被调用。
委托和类别:
委托对象与类别的关系:
委托强调类别的另一种应用:被发送给委托对象的方法可以声明为一个NSObject的类别。NSNetService委托方法的部分声明如下:
@interface NSObject (NSNetServiceBrowserDelegateMethods)
-(void) netServiceBrowserWillSearch: (NSNetServiceBroser *) browser;
-(void)
netServiceBrowser: (NSNetServiceBrowser *) aNetServiceBrowser
didFindService:(NSNetService *) service moreComing: (BOOL) moreComing;
-(void) netServiceBrowserDidStopSearch: (NSNetServiceBrowser *) browser;
-(void)
netServiceBrowser: (NSNetServiceBrower *) browser didRemoveService:
(NSNetService *) service moreComing: (BOOL) moreComing;
@end
通过将这些方法声明为NSObject的类别,NSNetServiceBrowser的实现可以将这些消息之一发送给任何对象,无论这些对象实际上属于哪个类。这也意味着,只要对象实现了委托方法,任何类的对象都可以成为委托对象。
创建一个NSObject的类别称为“创建一个非正式协议”。
计算机术语中的“协议”是一组管理通信的规则。
非正式协议只是一种表达方式。它表示“这里有一些你可能希望实现的方法,你可以使用它们更好地完成工作”。
当试图发送一个对象无法理解的消息时,NSNetServiceBrowser首先检查对象,询问其能否响应该选择器。如果该对象能够响应该选择器,NSNetServiceBrowser则给它发送消息。
选择器(selector):只是一个方法名称,但它以Objective-C运行时使用的特殊方式编码,以快速执行查询。
可以使用@selector()编译指令圆括号中的方法名称来指定选择器。
如:Car类的setEngine:方法的选择器是:
@selector(setEngine:)
而Car类的setTire:atIndex:方法的选择器如下:
@selector(setTIre:atIndex:)
NSObject提供了一个名为respondsToSelector:的方法,该方法询问对象以确定其是否能够响应某个特定的消息。
下面的代码段使用了respondToSelector:方法:
Car *car = [[Car alloc] init];
if ([car respondsToSelector: @selector(setEngine:)])
{
NSLog(@"yowzal");
}
这段代码将输出“yowzal”,因为Car类的对象确实能够响应应该setEngine:消息。
ITunesFinder *finder = [[ITunesFinder alloc] init];
if ([finder respondsToSelector: @selector(setEngine:)])
{
NSLog(@"yowzal");
}
这次未能输出“yowzal”,因为ITunesFinder类的对象没有setEngine:方法。
为
了确定委托对象能否响应消息,NSNetServiceBrowser将调用
respondsToSelector:@selector(netServiceBrowser:didFindService:moreComing:).
如果该委托对象能够响应给定的消息,则浏览器向该对象发送此消息。否则,浏览器将暂时忽略该委托对象,继续正常运行。
选择器可以被传递,可以作为方法的参数使用,甚至可以作为实例变量被存储。
协议
正式协议:
与非正式协议一样,正式协议是包含了方法和属性的有名称列表。但与非正式协议不同的是,正式协议要求显式地采用。
采用(adopt)协议的办法是在类的@interface声明中列出协议的名称。采用协议后,你的类就要遵守该协议,也意味着你承诺实现该协议的所有方法,否则编译器会生成警告来提醒你。
通常一个协议只有少数几个需要实现的方法,你必须实现所有这些方法才能获得一系列有用的功能。因此一般来说正式协议的要求并不是负担。
声明协议:
由Cocoa声明的一个协议:NSCopying。如果采用了NSCopying协议,你的对象将会知道如何创建自身的副本。
@protocal NSCopying
-(id) copyWithZone: (NSZone *) zone;
@end
使用@protocal告诉编译器:下面将是一个新的正式协议。@protocal后面是协议名称,协议名称必须要唯一。
也可以继承父协议,这点跟继承父类相似。
@protocol MySuperDuberProtocal <MyParentProtocol>
@end
第一行代码表示MySuperDuperProtocol协议继承于MyParentProtocol协议,因此必须实现两个协议中所有需要实现的方法。
通常使用NSObject作为根协议。请不要将其与NSObject类混淆。NSObject类符合NSObject协议,这意味着所有对象都符合NSObject协议。将NSObject协议作为你编写的协议的父协议是一个不错的方式。
接下来是一个方法声明列表,所有采用了此协议的类都必须实现这些方法。
Cocoa的NSCoding协议:
@protocol NSCoding
-(void) encodeWithCoder: (NSCoder *) encoder;
-(id) initWithCoder: (NSCoder *) decoder;
@end
采用协议:
要采用某个协议,可以在类的声明中列出该协议的名称,并用尖括号括起来。
@interface Car : NSObject <NSCopying>
{
// instance variables
}
// methods
@end // Car
如果Car类要同时采用NSCopying和NSCoding这两个协议,则其类声明将如下表示:
@interface Car : NSObject <NSCopying, NSCoding>
{
// instance variables
}
// methods
@end // Car
可以按任意顺序列出这些协议。
采用了这些协议,就相当于给阅读该类声明的编程人员传递了一条信息,表明该类的对象可以完成两个非常重要的操作:一是能够对自身进行编码或解码, 二是能够创建自身的副本。
采用NSCopying协议的项目:
copy消息会告诉对象创建一个全新的对象,并让新对象与接收copy消息的原对象完全一样。
复制Engine:
@interface Engine:NSObject <NSCopying>
@end // Engine
因
为Engine类采用了NSCopying协议,所以我们必须实现copyWithZone:方法。zone是NSZone类的一个对象,指向一块可供分
配的内存区域。当向一个对象发送copy消息时,该copy消息在到达你的代码之前会被转换为copyWithZone:方法。
Engine类的copyWithZone:方法的实现:
implementation:
-(id) copyWithZone: (NSZone *) zone
{
Engine *engineCopy;
engineCopy = [[[self class] allocWithZone: zone] init];
return (engineCopy);
} // copyWithZone
copyWithZone:
方法的首要任务是获得self参数所属的类,然后向self对象所属的类发送allocWithZone:消息,以分配内存并创建一个该类
的新对象。最后,copyWithZone:方法给这个新对象发送allocWithZone:消息使其初始化。
为什么要使用上面复杂的消息嵌套,尤其[self class]这个方法:
+(id) allocWithZone: (NSZone *) zone;
是一个类方法。我们需要将该消息发送给一个类,而不是一个实例变量。
[Engine allocWithZone: zone]
这
行代码适用于Engine类,但不适用与Engine类的子类。如果给Engine的子类Slant6类的对象发送copy消息,因为最后使用的是
Engine类的复制方法,所以这行代码最终会在Engine类的copyWithZone:方法中结束。而如果直接给Engine类发送
allocWithZone:消息,则将创建一个新的Engine类的对象,不是Slant6对象。假如给Slant6类增加了一些实例变量,情况会变得
更加扑朔迷离。这样做的话,Engine类的对象将无法容纳多余的变量,从而导致内存溢出错误。
所以通过使用[self class], allocWithZone:消息将会被发送给正在接受copy消息的对象所属的类。如果self是一个Slant6的对象,则这里将创建一个Slant6类的新对象。如果将来添加一些新对象,新的对象也会被正确的复制。
allocWithZone:方法的最后一行会返回新创建的对象。
检查一下内存管理问题:copy方法应该会返回一个保留计数器的值为1的对象,且对象不会自动释放。但这个新对象是我们通过alloc方法获得的。因此这个方法的内存管理没有问题。
复制Tire
@interface Tire: NSObject <NSCopying>
@property float pressure;
@property float treadDepth;
// -methods
@end // Tire
-(id) copyWithZone: (NSZone *) zone
{
Tire *tireCopy;
tireCopy = [[[self class] allocWithZone: zone] initWithPressure: pressure treadDepth: treadDepth];
return (tireCopy);
} // copyZone
由
于在创建对象时必须调用init方法,所以我们可以方便地使用Tire类的initWithPressure:treadDepth:方法,将新tire
对象的pressure和treadDepth设置为我们正在复制的tire对象的值。该方法正好是Tire类的指定初始化函数,但并不是必须要使用指定
初始化函数来执行复制操作。只要你愿意,也可以使用简单的init方法和访问器方法来修改对象的属性。
AllWeatherRadial类,接口内容不变:
@interface AllWeatherRadial: Tire
// ...properties
// ... methods
@end
这里没有<NSCopying>,因为当AllWeatherRadial类继承于Tire类时,它便已经获得Tire类的所有属性,包括对NSCopying协议的遵守。
重写copyWithZone:方法,确保子类的实例变量被复制。
-(id) copyWithZone: (NSZone *) zone
{
AllWeatherRadial *tireCopy;
tireCopy = [super copyWithZone: zone];
tireCopy.rainHandling = rainHandling;
tireCopy.snowHandling = snowHandling;
return (tireCopy);
} // copyWithZone
复制Car
@interface Car : NSObject <NSCopying>
// ...
@end
为遵守NSCopying协议,Car类必须实现原来的友元方法“copyWithZone:”:
-(id) copyWithZone: (NSZone *) zone
{
Car *carCopy;
carCopy = [[[self class] allocWithZone: zone] init];
carCopy.name = self.name;
Engine *engineCopy;
engineCopy = [[engine copy] autorelease];
// 也可以通过release来释放,对于ios应用,苹果公司建议你直接使用release而不是autorelease自动释放,因为你无法知道什么时候自动释放池会释放保留计数器的值。
carCopy.engine = engineCopy;
for (int i = 0; i < 4; i++){
Tire *tireCopy;
tireCopy = [[self tireAtIndex: i] copy];
[tireCopy autorelease];
[carCopy setTire: tireCopy atIndex: i];
}
return (carCopy);
}
main():
int main (int argc, const char* argv[]){
@autoreleasepool
{
Car *car = [[Car alloc] init];
car.name = @"Herbie";
for (int i = 0; i < 4; i++){
AllWeatherRadial *tire;
tire = [[AllWeatherRadial alloc] init];
[car setTire: tire atIndex: i];
[tire release];
}
Slant6 *engine = [[Slant6 alloc] init];
car.engine = engine; [engine release];
[car print];
Car *carCopy = [car copy];
[carCopy print];
[car release];
[carCopy release];
}
return (0);
}
协议和数据类型:
可以在使用的数据类型中为实例变量和方法参数指定协议名称。可以给Objective-C的编译器提供更多的信息,从而有助于检查代码中的错误。
id类型表示一个可以指向任何类型的对象的指针,它是一个泛型对象类型。如果一个用尖括号括起来的协议名称跟随在id之后,则编译器将知道你会接受任意类型的对象,但前提是要遵守该协议。
Objective-C 2.0的新特性:
增加了两个新的协议修饰符:@optional和@required。
指定了@optional关键字代码:
@protocol BaseballPlayer
-(void) drawHugeSalary;
@optional
-(void)slideHome;
-(void)catchBall;
-(void)throwBall;
@required
-(void)swingBat;
@end // BaseballPlayer
这段代码采用了BaseballPlayer协议并且必须实现-drawHugeSalary和-swingBat方法,而另外几个方法可以选择是否去实现。
看起来非正式协议就可以实现这样的效果,但是上面可以在类声明和方法中明确表达我们的意图。
委托方法:
委托(delegation),它是一个经常与协议公用的特性。委托就是某个对象指定另一个对象处理某些特定任务的设计模式。
代码块和并发性
代码块对象(通常简称为“代码块”)是对C语言中函数的扩展。除了函数中的代码,代码块还包含变量绑定。代码块有时也被称为闭包(closure)。
代码块包含两种类型的绑定:自动型和托管型。
自动绑定(automatic binding)使用的是栈中的内存。
托管绑定(managed binding)是通过堆创建的。
因为代码块实际上是由C语言实现的,所以它们在各种以C作为基础的语言内都是有效的, 包括Objective-C、C++以及Objective-C++.
代码块和函数指针:
与函数指针类似,代码块具有以下特征:
1、返回类型可以手动声明也可以由编译器推导。
2、具有指定类型的参数列表;
3、拥有名称。
先声明一个函数指针:
void (*my_func) (void);
这是非常基础的函数指针,它没有参数和返回结果,只要把*(星号)替换成^(幂符号),就可以把它转换成一个代码块的定义了。比如:
void (^my_block)(void);
在声明代码块变量和代码块实现的开头位置都要使用幂操作符。与在函数中一样,代码块的代码要放在{}(花括号)中。
int (^square_block)(int number) = ^(int number) {return (number * number);};
int result = square_block(5);
printf("Result = %d\n", result);
这个特别的代码块获取了一个整型参数并返回了这个数字的评分。等号前面是代码块的定义,而等号后面是实现内容。一般可以用如下关系来表示他们:
<returntype> (^blockname)(list of arguments) = ^(arguments){body;};
编译器可以通过代码块的内容推导出返回类型,所以可省略它。如果代码块没有参数,也可以省略。简洁的:
void (^theBlock)() = ^{printf("Hello Blocks!\n");};
使用代码块:
代码块声明成了变量,所以可以向函数一样使用它:
int result = square_block(5);
幂符号只有在定义代码块的时候才需要使用它,调用时则不需要.
代码块还有一个特性:可用来替换原先的函数:代码块可以访问与它相同的(本地)有效范围内声明的变量,也就是说代码块可以访问与它同时创建的有效变量。
int value = 6;
int (^multiply_block)(int number) = ^(int number) {return (value * number);};
int result = multiply_block(7);
printf("Result = %d\n", result);
直接使用代码块:
使用代码块时通常不需要创建一个代码块变量,而是在代码中内联代码块的内容。通常,会需要一个将代码块作为参数的方法或函数。
NSArray *array = [NSArray arrayWithObjects: @"Amir", @"Mishal", @"Irrum", @"Adam", nil];
NSLog(@"Unsorted Array %@", array);
NSArray *sortedArray = [array sortedArrayUsingComparator: ^(NSString *object1, NSString *object2){
return [object1 compare:object2];
}];
NSLog(@"Sorted Array %@", sortedArray);
使用typedef关键字:
typedef double (^MKSampleMultiply2BlockRef) (double c, double d);
这行语句定义了一个名称为MKSampleMultiply2BlockRef的代码块变量。包含了两个双浮点型参数并返回一个双浮点型数值。
有了typedef,可以像下面这样使用这个变量:
MKSampleMultiply2BlockRef multiply2 = ^(double c, double d) {return c * d;};
printf("%f, %f", multiply2(4, 5), multiply2(5, 2));
还可以定义其他一些不同类型的代码块变量:
typedef void (^MKSampleVoidBlockRef)(void);
typedef void (^MKSampleStringBlockRef)(NSString *);
typedef double (^MKSampleMultiplyBlockRef)(void);
代码块和变量:
代码块被声明后会捕捉创建点时的状态。代码块可以访问函数用到的标准类型的变量:
1、全局变量,包括在封闭范围内声明的本地静态变量。
2、全局函数(明显不是真实的变量)
3、封闭范围内的参数
4、函数级别(即与代码块声明时相同的级别)的__block变量。它们是可以修改的变量。
5、封闭范围内的非静态变量会被获取为常量
6、Objective-C的实例变量。
7、 代码块内部的本地变量。
本地变量:就是与代码块在同一范围内声明的变量。
typedef double (^MKSampleMultiplyBlockRef)(void);
double a = 10, b = 20;
MKSmapleMultiplyBlockRef multiply = ^(void){ return a * b;};
NSLog(@"%f", multiply());
a = 20;
b = 50;
NSLog(@"%f", multiply());
上面的代码中第一个输出会输出200,第二个NSLog也会输出200,而不是1000.因为变量是本地的,代码块会在定义时复制并保存它们的状态。所以NSLog只会输出200;
全局变量
在上面的本地变量示例中,我们说过变量与代码块拥有相同的有效范围,可以根据需要把变量标记为静态的(全局的)
static double a = 10, b = 20;
MKSmapleMultiplyBlockRef multiply = ^(void){ return a * b;};
NSLog(@"%f", multiply()); // 200
a = 20;
b = 50;
NSLog(@"%f", multiply()); // 1000
参数变量
代码块中的参数变量与函数中的参数变量具有同样的作用。
typedef double (^MKSampleMultiply2BlockRef)(double c, double d);
MKSampleMultiply2BlockRef multiply2 = ^(double c, double d) {return c * d;};
NSLog(@"%f, %f", multipley2(4, 5), multiply2(5, 2));
__block变量
本地变量会被代码块作为常量获取到。如果想要修改它们的值,必须将它们声明为可修改的,否则就会像下面的错误例子一样编译时出错:
double c = 3;
MKSmapleMultiplyBlockRef multiply = ^(double a, double b) { c = a * b;};
编译器将会警告这个错误:
Varibale is not assignable (missing _block type specifier)
想要修复这个编译错误,将变量c标记为_block。如下:
__block double c = 3;
MKSampleMultiplyBlockRef multiply = ^(double a, double b) { c = a * b; };
有些变量是无法声明为__block类型的。有两个限制:
1、没有长度可变的数组;
2、没有包含可变长度数组的结构体。
代码块内部的本地变量
这些变量与函数中的本地变量具有相同的作用:
void (^MKSampleBlockRef) (void) = ^(void) {
double a = 4;
double c = 2;
NSLog(@"%f", a * c);
};
MKSampleBlockRef();
Objective-C 变量
代码块是Objective-C语言中的优秀公民,可以像使用其他对象一样使用。使用时会遇到的最大问题就是内存管理。以下规则能帮助你处理内存管理:
1、如果引用了一个Objective-C对象,必须要保留它。
2、如果通过引用访问了一个实例变量,要保留一次self(即执行方法的对象)。
3、如果通过数值访问了一个实例变量,变量需要保留。
对于第一个规则:
NSString *string1 = ^{
return [_theString stringByAppendingString: _theString];
};
_theString是声明了代码块的类里面的实例变量。因为在代码块中直接访问了实例变量,所以包含它的对象(self)需要保留。
下一个例子:
NSString *localObject = _theString;
NSString *string2 = ^{
return [localObject stringByAppendingString: localObject];
};
这个例子中,我们是间接访问:创建了一个指向实例变量的本地引用并在代码块里面使用。因此这次要保留的是localObject,而不是self。
因为代码块是对象,可以向它发送任何与内存管理有关的消息。在C语言级别中,必须使用Block_copy()和Block_release()函数来适当地管理内存。
MKSmapleVoidBlockRef block1 = ^{NSLog(@"Block1");};
block1();
MKSampleVoidBlockRef block2 = ^{
NSLog(@"Block2");
};
block2();
Block_release(block2);
block2 = Block_copy(block1);
block2();
并发性:
能够在同一时间执行多项任务的程序称为并发的(concurrent)程序。
利用并发性最基础的方法是使用POSIX线程来处理程序的不同部分使其能够独立执行。POSIX线程拥有支持C语言和Objective-C的API。
苹果公司引入了Grand Central Dispatch,称之为GCD。这个技术减少了不少线程管理的麻烦。想要使用GCD,需要提交代码块或函数作为线程来运行。
GCD是一个系统级别(system-level)的技术,因此你可以在任意级别的代码中使用它。
GCD决定需要多少线程并安排它们运行的进度。因为它是运行在系统级别上的,所以可以平衡应用程序所有内容的加载,这样可以提高计算机或设备的执行效率。
同步:
Objective-C提供了一个语言级别的(language-level)关键字@synchronized。这个关键字拥有一个参数,通常这个对象是可以修改的。
@synchronized(theObject)
{
// Critical section
}
它可以确保不同的线程会连续地访问临界区的代码。
若定义了一个属性并且没有指定关键字nonatomic作为属性的特性,编译器会生成强制彼此互斥的getter和setter方法。
编译器生成了@synchronize(mutex, atomic)语句来确保彼此互斥。这样设置代码和变量会产生一些消耗,它会比直接访问更慢一些。
选择性能
想
让一些代码在后台执行,NSObject也提供了方法。这些方法的名字中都有performSelector:,最简单的就是
performSelectorInBackground:withObject:,他能在后台执行一个方法。它通过一个线程来运行方法。定义这些方法时
必须遵从以下限制:
1、这些方法运行在各自的线程里,因此必须为这些Cocoa对象创建一个自动释放池,而自动释放池是与主线程相关的。
2、这些方法不能有返回值,并且要么没有参数,要么只有一个参数对象。换句话说,你只能使用以下代码格式中的一种:
-(void)myMethod;
-(void)myMethod:(id)myObject;
记住这些限制,实现的代码如下:
-(void)myBackgroundMethod
{
@autoreleasepool
{
NSLog(@"My Background Method");
}
}
或:
-(void)myOtherBackgroundMethod:(id)myObject
{
@autoreleasepool
{
NSLog(@"My Background Method %@", myObject);
}
}
在后台执行你的方法,调用performSelectorInBacktground:withObject:方法:
[self performSelectorInBackground:@selector(myBackgoundMethod) withObject:nil];
[self performSelectorInBackground:@selector(myOhterBackgroundMethod:) withObject:arugmentObject];
当方法执行结束后,Objective-C运行时会特地清理并弃掉线程。
调度队列:
GCD可以使用调度队列(dispatch queue),它与线程很相似但使用起来更简单。只要写下你的代码,把它指派为一个队列,系统就会执行它,可以同步或异步执行任意代码。
一共有以下3种类型的队列:
1、连续队列:每个连续队列都会根据指派的顺序执行任务。可以按自己的想法创建任意数量的队列,它们会并行操作任务。
2、并发队列:每个并发队列都能并发执行一个或多个任务。任务会根据指派到队列的顺序开始执行。你无法创建连续队列,只能从系统提供的3个队列内选择一个来使用。
3、主队列:它是应用程序中有效的主队列,执行的是应用程序的主线程任务。
连续队列
有时有一连串任务需要按照一定的顺序执行,这时便可以使用连续队列。任务执行顺序为先入先出(FIFO):只要任务是异步提交的,队列会确保任务根据预定顺序执行。这些队列都是不会发生死锁的。
连续队列的使用:
dispath_queue_t my_serial_queue;
my_serial_queue = dispatch_queue_create("com.apress.MySerialQueue1", NULL);
第一个参数是队列的名称,第二个负责提供队列的特性(现在用不到,所以必须是NULL)。当队列创建好之后,就可以给它指派任务。
并发队列:
并发调度队列适用于那些可以并行运行的任务。并发队列也遵从先入先出(FIFO)的规范且任务可以在前一个任务结束前就开始执行。
一次所运行的任务数量是无法预测的,它会根据其他运行的任务在不同时间变化。所以每次你运行同一个程序,并发任务的数量可能会是不一样的。
每个应用程序都有3种并发队列可以使用:高优先级(high)、默认优先级(default)和低优先级(low)。如果想要引用到它们,可以调用dispatch_get_global_queue方法:
dispatch_queue_t myQueue;
myQueue = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
其他优先级的选项分别是DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORTY_LOW.第二个参数暂时都用0。
因为它们都是全局的,所以无需为它们管理内存。不需要保留这些队列的引用,在需要的时候使用函数来访问就可以了。
主队列
使用dispatch_get_main_queue可以访问与应用程序主线程相关的连续队列。
dispatch_queue_t main_queue = dispatch_get_current_queue(void);
这个队列与主线程相关,必须小心安排这个队列中的任务顺序,否则它们可能会阻塞主应用程序运行。
通常要以同步方式使用这个队列,提交多个任务并在它们操作完毕后执行一些动作。
获取当前队列
可以通过dispatch_get_current_queue()来找出当前运行的队列代码块。
如果在代码块对象之外调用了这个函数,则返回主队列。
dispatch_queue_t myQueue = dispatch_get_current_queue();
队列也要内存管理
调度队列是引用计数对象。可以使用dispatch_retain()和dispatch_release()来修改队列的保留计数器的值。
无法用在全局调度队列上。
编写一个使用了垃圾回收的OS X应用程序,那么必须手动管理这些队列。
队列的上下文:
可以向调度对象(包括调度队列)指派全局数据上下文,可以在上下文中指派任意类型的数据。系统只能知道上下文包含了与队列有关的数据,上下文数据的内存管理需要程序员自己做。在需要它的时候分配内存并在队列销毁之前进行清理。
在为上下文数据分配内存的时候,可以使用dispatch_set_context()和dispatch_get_context()函数。
NSMutableDictionary *myContext = [[NSMutableDictionary alloc] initWithCapacity: 5];
[myContext setObject:@"My Context" forKey: @"title"];
[myContext setObject:[NSNumber numberWithInt: 0] forKey: @"value"];
dispatch_set_context(_serial_queue, (_bridge_retained void *)myContext);
创建一个字典来存储上下文。也可以使用其他的指针类型。
使用_bridge_retained来给myContext的保留计数器的值加1.
清理函数:
不用知道上下文对象在何时何地会被弃用。要清理上下文对象的数据,可以让对象在它弃用的时候调用一个函数,像dealloc函数。
函数格式:
void function_name(void *context);
创建一个会在上下文对象弃用时调用的示例函数,通常被称为终结器(finalizer)函数。
void myFinalizerFunction(void *context)
{
NSLog(@"myFinalizerFunction");
NSMutableDictionary *theData = (_bridge_transfer NSMutableDictionary*)context;
[theData removeAllObjects];
}
_bridge_transfer关键字把对象的内存管理由全局释放池变换成了我们的函数。当函数结束时,ARC将会给它的保留计数器的值减1.
在代码块里访问上下文内容:
NSMutableDictionary *myContext = (_bridge NSMutableDictionary *) dispatch_get_context(dispatch_get_current_queue());
_bridge关键字告诉ARC我们不想自己来管理上下文的内存,而想交给系统来管理。
添加任务
有两种方式可以向队列中添加任务:
同步:队列会一直等待前面任务结束。
异步:添加任务后,不必等待任务,函数会立刻返回。推荐优先使用这种方式,因为它不会阻塞其他代码的运行。
可以选择向队列提交代码块或函数。一共4个调度函数,分别是代码块和函数各自的同步与异步方式。
调度程序
添加任务的最简单的方法是通过代码块。代码块必须是dispatch_block_t这样的类型,要定义为没有参数和返回值才行。
typedef void (^dispatch_block_t)(void)
先添加异步代码块。这个函数拥有两个参数,分别是队列和代码块。
dispatch_async(_serial_queue, ^{NSLog(@"Serial Task 1");});
也可以定义一个代码块对象,而不是通过内联方式来创建:
dispatch_block_t myBlock = ^{NSLog(@"My Predfined block"); };
dispatch_async(_serial_queue, myBlock);
如果想要同步添加,请使用dispatch_sync函数。
函数的标准原型:
void function_name(void *argument)
示例:
void myDispatchFunction(void *argument)
{
NSLog(@"Serial Task %@", (_bridge NSNumber *) argument);
NSMutableDictionary *context = (_bridge NSMutableDictionary *) dispatch_get_context(dispatch_get_current_queue());
NSNumber *value = [context objectForKey:@"value"];
NSLog(@"value = %@", value);
}
接下来需要向队列添加这个函数。调度函数拥有3个参数:队列、需要传递的任意上下文以及函数。如果没有信息要发送给函数,可以只传递一个NULL值。
dispatch_async_f(_serial_queue, (_bridge void *)[NSNumber numberWithInt: 3], (dispatch_function_t)myDispatchFunction);
队列创建完之后,就做好接受任务的准备了,当添加一个任务,队列就会安排好它的进度。
如果想要以同步方式添加到队列中,调用dispatch_sync_f函数。
如果想要暂停队列,调用dispatch_suspend()函数并传递队列名称。
dispatch_suspend(_serial_queue);
调用dispatch_resume()函数来重新启用。这些都是程序员自己来掌握的。
dispatch_resume(_serial_queue);
操作队列:
有一些被称为操作(operation)的API,可以让队列在Objective-C层级上使用起来更加简单。
想要使用操作,首先需要创建一个操作对象,然后将其指派给操作队列,并让队列执行它,3种创建操作的方式:
1、NSInvocationOperation:如果你已经拥有一个可以完成工作的类,并且想要在队列上执行,可以尝试使用这种方法。
2、NSBlockOperation:这有些像包含了需要执行代码块的dispatch_async函数。
3、自定义的操作:如果你需要更灵活的操作类型,可以创建自己的自定义类型。你必须通过NSOperation子类来定义你的操作。
创建调用操作(invocation operation)
NSInvocationOperation会为执行任务的类调用选择器。如果拥有一个包含需要的方法的类,使用这种方式来创建会非常方便:
@implementation MyCustomClass
- (NSOperation *) operationWithData:(id)data
{
return [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(myWorkerMethod:) object:data];
}
// This is the method that does actual work
-(void) myWorkerMethod: (id)data
{
NSLog(@"My Worker Method %@", data)
}
@end
一旦向队列添加了操作,任务即将执行时便会调用类里面的myWorkerMethod:方法。
创建代码块操作(block operation)
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock: ^{
// Do your business
}];
这些操作的代码块与我们在调度队列中使用的是同一种类型。
通过addExecutionBlock:方法继续添加更多的代码块。
代码块会根据队列的类型的方式运行。
[blockOperation addExecutionBlock: ^{
// do your stuff
}];
向队列中添加操作:
一旦创建了操作,就需要向队列中添加代码块。
使用NSOperationQueue来取代之前使用的dispatch_queue_t函数,NSOperationQueue一般会并发执行操作。它具有相关性,因此如果某操作是基于其他操作的,它们会相应地执行。
如果要确保你的操作时连续执行的,可以设置最大并发操作数为1,这样任务将会按照先入先出的规范执行。
在向队列添加操作之前,需要某个方法来引用到那个队列。
现在运行的队列:
NSOperationQueue *currentQueue = [NSOperationQueue currentQueue];
或主队列:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
创建自己队列的代码:
NSOperationQueue *_operationQueue = [[NSOperationQueue alloc] init];
使用addOperation:来添加操作:
[theQueue addOperation:blockOperation];
也可以添加需要执行的代码块来代替操作对象:
[theQueue addOperationWithBlock: ^{
NSLog(@"My Block");
}];
一旦向队列添加了操作,它就会被安排进度并执行。
AppKit简介
构建项目:
创建委托文件的@interface部分
使用Xcode中的Interface Builder编辑器来布局窗口内容,并在MSCDelegate与用户界面之间创建各种连接。
Interface Builder也适用于布局iOS应用程序,因此无论你要为哪一个平台开发应用程序,都需要在Interface Builder上进行很多工作。
MSCAppDelegate类委托的头文件:
#import <Cocoa/Cocoa.h>
@interface MSCDelegate : NSObject <NSApplicationDelegate>
@property (assign) IBOutlet NSWindow *window;
@end
IBOutlet 不是一个真正的Objective-C关键字。但通常会在Interface Builder中使用它。在IBOutlet 那行代码的左边边栏有一个点。点击那个点就会直接跳转到Interface Builder中与其相关的对象上。
另一个像关键字的IBAction.IBAction被定义为void的作用,这意味着这个在文件中声明的方法返回的类型将是void(也就是什么都不返回)。
Interface Builder编辑器,双击MainMenu.xib文件的Interface Builder编辑器。有人会称其为IB。
虽
然文件的扩展名为.xib,当我们仍称之为nib文件。nib是NeXT Interface
Builder的首字母缩写,是Cocoa从NeXT公司沿袭下来的技术结晶。nib文件是包含了压缩(freeze-dried)对象的二进制文件,
而.xib文件是XML格式的nib文件。在编译时,.xib文件将会编辑为nib格式。
通过库面板来编辑下面的画面:
创建连接:
连接输出口(IBOutlet)
首先要告诉NSTextField类型的实例变量textField以及resultsField分别指向哪个控件。
将会用到辅助编辑器,在Xcode窗口的右上角有三个标记为Editor的按钮。点击中间的按钮,就会将编辑器从中间垂直分成两部分,这就是辅助编辑器。
连接操作(IBAction)
应用程序委托的实现
首先了解一下IBOutlet是如何工作的:
加
载nib文件时(MainMenu.nib会在应用程序启动时自动加载,
你可以创建自己的nib文件并手动加载它们),存储在nib文件中的任何对象都会被重新创建。这意味着会在后台执行alloc和init方法。所以,当程
序启动时,会分配并初始化一个MSCAppDelegate实例。在执行init方法期间,所有IBAction实例变量都是nil。只有在生成了nib
文件中的所有对象(包括窗口、文本框以及按钮)后,所有连接才算完成。
一旦建立了所有连接(也就是将NSTextField等对象的地址添加到MSCAppDelegate的实例变量中),就会向创建的每个对象发送消息awakeFromNib。需要注意,对象的创建和awakeFromNib消息的发送没有任何既定的顺序。
一个常见的错误是试图在init方法中使用IBOutlet执行一些操作。由于所有实例变量都为nil,所有发送给它们的消息都不执行任何操作,所以在init中尝试任何操作都会无疾而终。
init方法:
-(id) init
{
if (nil != (self = [super init]))
{
NSLog(@"init: text %@ / results %@", _textField, _resultsField);
}
return self;
}
在文本框中输入默认值为Enter text here,并将结果标签的文本内容预设为Result。
awakeFromNib方法:
- (void) awakeFromNib
{
NSLog (@"awake: text: %@ / results %@", _textField, _resultsField);
[_textField setStringValue: @"Enter text here"];
[_resultsField setStringValue: @"Results"];
}
uppercase:
-(IBAction) uppercase: (id) sender
{
NSString *original = [_textField stringValue];
NSString *uppercase = [original uppercaseString];
[_resultesField setStringValue: uppercase];
} // uppercase
内存管理检查:是否每句代码都是正确的,是的。创建的两个新对象(原始字符串和大写形式的字符串)都来自与除alloc、copy和new以外的方法,所以它们都位于自动释放池中,并且到时将会被清除。
用stringValue消息从_textField获取最初的字符串。
setStringValue:负责复制或保留传入的字符串。该方法内部的操作我们无从知道,但是我们知道代码是符合内存管理的。
lowercase:
-(IBAction) lowercase: (id) sender {
NSString *original = [_textField stringValue];
NSString *lowercase = [original lowercaseString];
[_resultesField setStringValue:lowercase ];
}
运行项目后的界面: