(经典推荐) Objective-C 的语法与Cocoa 框架----02

来源:互联网 发布:迷恋网络怎么办 编辑:程序博客网 时间:2024/06/04 18:10

14. 内存管理:

JAVA 使用GC 机制自动管理内存的,Objective-C 支持手动管理内存,也支持GC 机制,但是GC 机制对于iOS 设备无效,也就是仅对Mac OS X 电脑有效。这是合理的,因为iPhone、iPod、iPad 等的内存、CPU肯定要比电脑低很多,你必须谨慎对待内存的使用,而不能肆无忌惮的等着GC 帮你去收拾烂摊子。

 

Objective-C 在使用alloc、new、copy 的时候为对象分配内存,然后返回分配的内存的首地址存入指针变量,使用dealloc 释放内存。

new 是alloc 和init 的合写形式,也就是[[Fraction alloc]init]与[Fraction new]是相同的,

copy 我们会在第16 节讲解,

 

我们知道Objective-C 中的对象都是使用指针引用的,也就是在方法调用的时候传递的都是

指针,那么一个对象的引用在一次调用过程中,可能被传递到了多处,也就是有多个地方在

引用这个对象。

例如:

main(){

A *a=[A new];

B *b=[B new];

C *c=[C new];

[b m: a];

[c m: a];

}

上面的代码就将a 传递给了b、c 两个实例,而a 实例本身又是在main 函数中定义的,因此一共有main 函数、b、c 三个地方引用了a 实例。那么由上面的代码引出了这样的问题:你在alloc 一个对象之后,什么时候dealloc 它呢?回收早了可能导致有些还在引用的地方会报错,不回收自然会导致内存泄漏。

Objective-C 的解决办法是采用一个引用计数器retainCount 来表示还有多少个地方在引用这个对象。一个对象在被alloc 之后就retainCount 就是1,之后每调用一次调用retain 方法都会使retainCount 加1,调用release都会使retainCount 减1。

当Objective-C 发现一个对象的引用计数器retainCount 为0 时就会立即调用这个对象从NSObject 继承而来的dealloc 方法回收内存,这个调用动作是Objective-C 运行环境完成的,你需要关心的就是把retainCount在恰当的时候减为0 就可以了。

 

int main()

{

Fraction *frac=[[Fraction alloc] initWithNumerator: 3 denominator: 5];

printf("%d\n",[frac retainCount]);//1---alloc 分配内存并使引用计数器从0 变为1

[frac retain];//2---引用计数器加1

printf("%d\n",[frac retainCount]);

[frac retain];//3---引用计数器加1

printf("%d\n",[frac retainCount]);

[frac release];//2---引用计数器减1

printf("%d\n",[frac retainCount]);

[frac release];//1---引用计数器减1

printf("%d\n",[frac retainCount]);

[frac release];//0---引用计数器减1

//此时frac 的dealloc 方法自动被调用,Objective-C 回收frac 对象被回收。你可以在

Fraction 中覆盖-(void) dealloc 方法中加个输出语句观察一下。

 

此时你再去调用frac 的方法都会导致程序崩溃,因为那块内存去被清理了。但是你可以像下面这样做。

frac=nil;

[frac print];//记得前面说过nil 与null 的区别了吗?因此此行为空操作,但是不会报错。

}

这段代码输出了frac 的引用计数器的变化1---2---3---2---1---0,当然这段代码的retain 操作毫无意义,仅仅是演示retain、release 之后引用计数器的变化情况。

 

我们定义一个住址类型的类:

Address.h

#import <Foundation/Foundation.h>

@interface Address: NSObject{

NSString *city;

NSString *street;

}

-(void) setCity: (NSString*) c;

-(void) setStreet: (NSString*) s;

-(void)setCity: (NSString*) c andStreet: (NSString*) s;

-(NSString*) city;

-(NSString*) street;

@end

与前面的示例不同的是Address 的成员变量city、street 都是NSString 的对象类型,不是基本数据类型。

 

Address.m

#import "Address.h"

@implementation Address

-(void) setCity: (NSString*) c

{

[c retain];

[city release];

city=c;

}

 

-(void) setStreet: (NSString*) s

{

[s retain];

[street release];

street=s;

}

-(void)setCity: (NSString*) c andStreet: (NSString*) s{

[self setCity: c];

[self setStreet: s];

}

-(NSString*) city{

return city;

}

-(NSString*) street{

return street;

}

-(void) dealloc

{

[city release];

[street release];

[super dealloc];

}

@end

 

你可以先不理会这里的一堆retain、release 操作,一会儿会做讲解。

main 函数

 

// initWithString 是NSString 的一个使用NSString 字面值初始化的方法。与直接使用下面的C语言字符序列初始化相比,@” ”支持Unicode 字符集。

NSString *city=[[NSString alloc] initWithString: @"Beijing"];

 

// initWithCString 是NSString 的一个使用C 语言的字符序列初始化的方法。

NSString *street=[[NSString alloc] initWithCString: "Jinsongzhongjie"];

 

Address *address=[[Address alloc] init];

[address setCity: city andStreet: street];

[city release];

[street release];

[address release];

 

我们在main 函数中创建了city、street 两个NSString 的实例,然后setter 到address 实例,

由于city、street 你使用了alloc 分配内存,你势必就要release 它们。首先要确定的是在main函数里release还是在address 里release 呢?因为你确实把他们两个传递给Address 的实例address 了。我们一般按照谁创建的就谁回收的原则(对象的拥有权),那么自然你要在main方法里release,我们恰当的选择了在调用完address 的setter 方法之后release,因为city、street 在main 函数中的任务(给address 里的两个成员变量赋值)就此结束。

此时,新的问题来了,我们在main 函数中setter 完之后release 对象city、street,因此city、

street 的retainCount 会由1 变为0,这就会导致你传递到address 里的city、street 指针指向的对象被清理掉,而使address 出错。很显然,你需要在address 的setter 方法里retain 一下,增加city、street 的引用计数为2,这样即便在main 函数release 之后,city、street 指向的内存空间也不会被dealloc,因为2-1=1,还有一个引用计数。因此就有了Address 中的setter的写法,我们拿setCity 方法为例:

 

-(void) setCity: (NSString*) c

{

[c retain];//---1

[city release];//---2

city=c;//---3

}

在JAVA 这种使用GC 机制的语言中,我们只需要写第三条语句。其实Objective-C 中你也可

以只写第三条语句,我们管这种方式获得的city 叫做弱引用,也就是city 只是通过赋值操作,把自己指向了c指向的对象,但是对象本身的引用计数器没有任何变化,此时city 就要承担[c release]之后所带来的风险,不过有些情况下,你可能确实需要这种弱引用。

当然,大多数情况下,我们使用的都是强引用,也就是使用第一行代码首先retain 一下,使引用计数器加1,再进行赋值操作,再进一步说就是先通过retain 方法拿到对象的拥有权,

再安全的使用对象。

 

第二行代码又是为什么呢?其实这是为了防止city 可能已经指向了一个对象,如果不先对

city 进行一次release,而直接把city 指向c 指向的对象,那么city 原来指向的对象可能会出现内存泄漏,因为city 在改变指向的时候,没有将原来指向的对象的引用计数器减1,违反了你retain 对象之后,要在恰当的时刻release 对象的要求。

 

第三行代码毋庸置疑的要在最后一行出现,但按照前面的阐述,貌似第一行、第二行的代码

是没有先后顺序的,也就是可以先[city release],再[c retain],但真的是这样吗?有一种较为

少见的情况,那就是把自己作为参数赋给自己(这听起来很怪),也就是说参数c 和成员变

city 是一个指针变量,那么此时如果你先调用[city release],就会导致对象的retainCount 归0,对象被dealloc 了,那么[c retain]就会报错,因为对象没有了。

 

综上所述,上面所示的三行代码是比较好的组织方式。但其实你可能也会看到有些人用其他

的方式书写setter 方法,这也没什么好奇怪的,只要保证对象正常的使用、回收就是没有问题的代码。

 

最后我们再看一下Address 中的dealloc 方法,因为你在setter 中持有了city、street 指向的对象的拥有权,那么你势必要和main 函数一样,负责在恰当的时刻release 你的拥有权,由于你在address 实例中可能一直要用到city、street,因此release 的最佳时刻就是address 要被回收的时候。另外,我们知道对象被dealloc 之后,原来指向它的指针们依然指向这块内存区域,Objective-C 并不会把这些指着垃圾的指针们都指向nil,因此你如果还调用这些指针们的方法,就会报错。因此有些人喜欢在dealloc 里把对象release 之后紧接着指向nil,

防止它会被意外调用而出现空指针异常。

但我认为你这样可能屏蔽了错误,不是一个好办法。

 

只要实例是alloc、new、copy 出来的,你就需要选择完全手动的管理内存,也就是你要负责release。(谁申请,谁释放)

Objective-C 的手动管理内存还提供了一种半自动的方式,就是第八节用到的NSAutoreleasePool 类型。

 

第八节的DenominatorNotZeroException 异常我们是通过父类 NSException 的类方法exceptionWithName:reason:userInfo 创建的,我们并没有使用到alloc、new、copy 关键字,这种情况下,你只需要定义一个NSAutoreleasePool 就可以了。为什么是这样呢?

 

我们先看下面的代码:

NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

Fraction *frac=[[Fraction alloc] initWithNumerator: 3 denominator: 5];

printf("frac 的引用次数 %d\n",[frac retainCount]);//1

[frac retain];

printf("frac 的引用次数 %d\n",[frac retainCount]);//2

[frac autorelease];

printf("frac 的引用次数 %d\n",[frac retainCount]);//2

[frac autorelease];

printf("frac 的引用次数 %d\n",[frac retainCount]);//2

[pool release];

这里我们对frac 用的是autorelease 方法,不是release 方法,autorelease 方法并不对引用计数器减1,而是将对象压入离它最近的NSAutoreleasePool 的栈顶(所谓离得最近就表示多个NSAutoreleasePool 是可以嵌套存在的),等到NSAutoreleasePool 的release 方法被调用后,NSAutoreleasePool 会将栈内存放的指针指向的对象的release 方法。

 

其实exceptionWithName:reason:userInfo方法的内部实现就是把创建好的N***ception的实例

的指针返回之前,首先调用了指针的autorelease 方法,把它放入了自动回收池。

其实在iOS 开发中,苹果也是不建议你使用半自动的内存管理方式,但不像GC 机制一样被

禁用,原因是这种半自动的内存管理,容易在某些情况下导致内存溢出。我们看一下如下的

代码:

NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];

for(int i=0;i<1000000;i++){

NSString *s=@"...";

if(i%1000==0){//执行代码}

}

[pool release];

这里我们在循环100 万次的代码中每次都创建一个NSString 实例,由于NSString 没有使用

alloc 创建实例,因此我们使用了自动回收池。但这段代码的问题是这100 万个NSString 

for 循环完毕,最后的pool release 方法调用后才会回收,这就会造成在for 循环调用过程

中,内存占用一直上涨。当然,你可以改进上面的代码如下所示:

NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];

for(int i=0;i<1000000;i++){

NSString *s=@"...";

     if(i%1000==0)

    {

          [pool release]; 

           pool=[[NSAutoreleasePool alloc] init];

      }

}

上面的代码每循环1000 次,就回收自动回收池,然后再紧接着重新创建一个自动回收池给

下1000 个NSString 对象使用。其实这个问题很像Hibernate 的一个问题:给你一张1000 万

行记录的Excel,让你导入到数据库,你该怎么做呢?直接的做法如下所示:

Session session=获取Hibernate 的JDBC 连接对象

for(int i=0;i<Excel 的行数;i++){

Object obj=每一行的Excel 记录对应的JAVA 对象;

session.save(obj);

}

Transaction.commit();

由于Hibernate 的一级缓存是在你提交事务的时候才清空,并且刷新到数据库上的,因此在

循环结束前,你的一级缓存里会积累大量的obj 对象,使得内存占用越来越大。

解决办法与Objective-C 的自动回收池差不多,就是如下的做法:

Session session=获取Hibernate 的JDBC 连接对象

for(int i=0;i<Excel 的行数;i++){

Object obj=每一行的Excel 记录对应的JAVA 对象;

session.save(obj);

if(i%1000==0){

session.flush();

}

}

Transaction.commit();

我们看到每隔1000 次就刷新一级缓存,它的作用就是清理一级缓存,把数据都发送到数据

库上面去,但是我并没有提交事务,也就是数据都从JVM 转移到数据库的缓冲区累积了,

数据库依然会等待循环结束后的commit()操作才会提交事务。

 

15. 常用的类型:


(1.) 字符串:

Cocoa 中使用NSString 表示字符串,字面值使用@” ”的形式除了前面看到initWithString

initWithCString 的初始化方法外,NSString 还有其他的初始化方法和类方法创建NSString 的

实例,例如:

 

+(NSString*) stringWithFormat: @””,…

与NSLog 一样,使用格式化模版格式化字符串。

NSString 也有一些其他的成员方法:

-(BOOL) isEqualToString: (NSString*) s

比较两个字符串是否相等,与JAVA 一致的地方是

比较指针相等用 ==,比较对象是否相同要用equal 方法。

 

-(NSComparisonResult) compare: (NSString*) s options: (NSStringCompareOptions) options

这个方法用于详细的比较两个字符串是否相等,譬如:是否忽略大小写等。

返回值NSComparisonResult 为C 语言的enum 类型,常用的枚举值为NSOrderedSame,表示比较结果相等。第二个参数NSStringCompareOptions 也是个enum 类型, 常用的枚举值为NSCaseInsensitiveSearch、NSNumericSearch,分别表示忽略大小写、字数是否相等。由于

NSStringCompareOptions 的枚举值都是2 的指数,所以你可以使用位或运算表示同时具备两个条件,例如:NSCaseInsensitiveSearch|NSNumericSearch 表示忽略大小写并且字数相同。

 

-(BOOL) hasPrefix: (NSString) s

表示字符串是否以参数s 作为前缀,当然也有hasSufix方法判断后缀。

-(NSRange) rangeOfString: (NSString*) s

判断是否包含参数s,这里返回前面提到过的结构体NSRange。如果包含字符串s,则NSRange

的location 为包含的字符串s 所在的起始位置,length 为长度。如果不包含字符串s,则location为NSNotFound,length 为0。

 

例:

NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

NSString *s1=[NSString stringWithFormat: @"You height is %d weight is %d!",168,68];

NSLog(s1);

//长度

NSLog(@"The str length is %d!",[s1 length]);

NSString *s2=@"You Height is 168 Weight is 68!";

//比较

if([s2 isEqualToString: s1]==YES)

NSLog(@"Equal YES");

else

NSLog(@"Equal NO");

//详细比较

NSComparisonResult cr=[s2 compare: s1

options: NSCaseInsensitiveSearch|NSNumericSearch];

if(cr==NSOrderedSame){

NSLog(@"Compare YES");

}

else{

NSLog(@"Compare NO");

}

//判断前缀

if([s1 hasPrefix: @"The"]){

NSLog(@"hasPrefix YES");

}

else{

NSLog(@"hasPrefix NO");

}

//判断s1 中是否含有字符串height

NSRange range=[s1 rangeOfString: @"height"];

if(!(range.location==NSNotFound)){

NSLog(@"The \"height\" is located in %u!",range.location);

NSLog(@"The \"height\" length is %u!",range.length);

}

[pool release];

NSString 是长度不可变的字符串,NSMutableString 继承自NSString,实现了可变字符串。

NSMutableString 一般使用类方法

+(NSMutableString*) stringWithCapacity: (int) capacity 进行创建,capacity 指定预先分配的字

符串长度。

-(NSMutableString*) appendString: (NSString*) s

这与JAVA 的StringBuffer 的append 没什么区别。

-(void) deleteCharactersInRange: (NSRange) range

这个方法删除指定范围的字符串,常与rangeOfString 方法联用。

例:

NSMutableString *ms1=[NSMutableString stringWithCapacity: 100];

[ms1 appendString: @"You height is "];

[ms1 appendFormat: @"%d weight is %d!", 168, 68];

NSLog(@"%@",ms1);

NSRange range=[ms1 rangeOfString: @" weight is 68"];

[ms1 deleteCharactersInRange: range];

NSLog(@"%@",ms1);


(2.) 数组:

Cocoa 使用NSArray 表示数组,但是它不能存储基本数据类型、enum、struct、nil,只能存

Objective-C 的对象。

例:

NSArray *array=[NSArray arrayWithObjects: @"One", @"Two", @"Three", nil];

//从这个类方法arrayWithObjects 的定义可以看出,它使用nil 表示数组元素结束,这也是nil 不能存储在NSArray 中的原因。

int count=[array count];//获取数组元素的格式,本例中为3。

int i;

for(i=0; i<count;i++)

{

NSLog(@"%@",[array objectAtIndex: i]);

//遍历数组元素,objectAtIndex 方法获取指定索引位置的元素。

}

NSString *s=@"iPhone,Android,Windows Phone 7";

array=[s componentsSeparatedByString: @","];//按照一个字符串将字符串拆分为数组。

s=[array componentsJoinedByString: @" "];//按照一个字符串将数组连接为字符串。

NSLog(s);

同理,NSMutableArray 为长度可变的数组,相当于JAVA 中的List。

例:

NSMutableArray *mArray=[NSMutableArray arrayWithCapacity: 10];

[mArray addObject: @"Apple"];//添加数组元素

[mArray addObject: @"Google"];

[mArray addObject: @"MicroSoft"];

[mArray removeObjectAtIndex: 2];//移除指定索引位置的数组元素

s=[mArray componentsJoinedByString: @","];//拼接数组元素为字符串

NSLog(s);

NSEnumerator *e = [mArray objectEnumerator];//获取数组的迭代器,相当于JAVA 中的Iterator,reserveObjectEnumerator 用于获取反转

之后的数组迭代器。与JAVA 一致的地方是你在使用迭代器时,不能对数组进行添加、删除

操作。

id obj;

while(obj=[e nextObject])

{

NSLog(@"%@",obj);

}

printf("------------------------------\n");

/*

for(NSString *ms in mArray){

NSLog(@"%@",ms);

}

*/

//for-each 快速枚举为Objective-C 2.0 的新特性,Windows 上的GNUStep 并不支持。

(3.) 字典(哈希表):

NSDictionary 用于存储key-value 的数据结构,与JAVA 中的Map 类似。

例:

NSDictionary *dic=[NSDictionary dictionaryWithObjectsAndKeys: @"Apple", @"A", @"Google",@"G", nil];

//dictionaryWithObjectAndKeys 后的可变参数,每两个为一个value-key,以nil 表示结束。

NSLog(@"%@",[dic objectForKey: @"A"]); //按照指定的key 查询value

同样的有NSMutableDictionary 表示长度可变的字典。

例:

NSMutableDictionary *mDic=[NSMutableDictionary dictionaryWithCapacity: 10];

[mDic setObject: @"Apple" forKey: @"A"];//添加value-key 对

[mDic setObject: @"Google" forKey: @"G"];

[mDic setObject: @"Windows Phone 7" forKey: @"W"];

[mDic removeObjectForKey: @"W"];//移除指定key 的value

/*

for(id key in mDic)

{

NSLog(@"%@ : %@",key,[mDic objectForKey: key]);

}

*/

//快速迭代的for-each 循环

NSEnumerator *keyEnum=[mDic keyEnumerator];//获得key 的枚举器

id key;

while(key=[keyEnum nextObject])

{

NSLog(@"%@ : %@",key,[mDic objectForKey: key]);

}


(4.) 哈希Set:

NSSet 表示以hash 方式计算存储位置的集合,与JAVA 中的HashSet 是一致的。在NSSet 中

的每个对象都有一个唯一的hash 值,重复的对象将只能保留一个。因此,这引出了Objective-C

中的对象比较问题,这需要你实现从NSObject 继承而来的如下两个方法:

- (BOOL) isEqual: (id) anObject;

- (NSUInteger) hash;

这与JAVA 的对象比较没有什么区别,两个相等的对象必须有相同的hashCode,所以这两个

方法必须同时实现。下面我们看一个示例。

#import <Foundation/Foundation.h>

@interface Person: NSObject{

int pid;

NSString *name;

}

-(void) setPid: (int) pid;

-(void) setName: (NSString*) name;

-(int) pid;

-(NSString*) name;

@end

@implementation Person

-(void) setPid: (int) p{

pid=p;

}

-(void) setName: (NSString*) n{

[n retain];

[name release];

name=n;

}

-(int) pid{

return pid;

}

-(NSString*) name{

return name;

}

- (NSUInteger) hash

{

return pid+[name hash];

}

-(BOOL) isEqual: (id) p

{

if(pid==[p pid]&&[name isEqualToString: [p name]])

{

return YES;

}

else

{

return NO;

}

}

-(void) dealloc

{

[name release];

[super dealloc];

}

@end


int main (int argc , const char * argv[])

{

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

Person *person1=[[Person alloc] init];

[person1 setPid: 1];

[person1 setName: @"Name1"];

Person *person2=[[Person alloc] init];

[person2 setPid: 1];

[person2 setName: @"Name1"];

Person *person3=[[Person alloc] init];

[person3 setPid: 3];

[person3 setName: @"Name3"];

NSSet *set=[NSSet setWithObjects: person1, person2, person3, nil];

NSEnumerator *e=[set objectEnumerator];

Person *person;

while(person=[e nextObject])

{

NSLog(@"%d",[person pid]);

}

[pool release];

return 0;

}

我们看到person1 与person2 实例相同,所以最后输出两个Person 的pid,__________而不是三个。

同样的,NSMutableSet 表示长度可变的哈希Set。

(5.) 封装类:

前面的几个容器类的对象都不能存放基本数据结构、enum、struct、nil,怎么办呢?JAVA

中我们知道所有的基本数据类型都有对应的封装类,例如:int---Integerboolean---Boolean

使用封装类可以把基本数据类型包装为对象,而封装类本身又有方法可以返回原来的基本数

据类型。

Cocoa 使用NSValue 作为封装类。

例:

NSRect rect=NSMakeRect(1,2,30,50);//这个是在前面提到过的一个表示矩形的结构体

NSValue *v=[NSValue valueWithBytes: &rect objCType: @encode(NSRect)];

//valueWithBytes 要求传入包装数据的地址,也就是变量中存储的数据的首地址,我们依然

使用C 语言的&作为地址运算符,计算指针存储的首地址。

//objCType 要求传入用于描述数据的类型、大小的字符串,使用@encode 指令包装数据所

属的类型。

NSMutableArray *mArray=[NSMutableArray arrayWithCapacity: 3];

[mArray addObject: v];

NSRect rect2;

[[mArray objectAtIndex: 0] getValue: &rect2];

//NSValue 的-(void) getValue: (void*) value 方法可以将NSValue 中的数据内容取出,要求

传入的参数是一个指针,也就是说getValue 方法将你传入的指针指向存储的数据。

//一般Cocoa 中的getXXX 方法都是这个作用,这也是为什么前面的getter 方法不要以get作为方法前缀的原因。

printf("%f\n",rect2.size.width);


对于基本数据类型,你可以使用简便的NSNumber 来封装,它是NSValue 的子类

例:

NSNumber *n1=[NSNumber numberWithChar: 'A'];

NSNumber *n2=[NSNumber numberWithInt: 100];

NSNumber *n3=[NSNumber numberWithFloat: 99.9F];

NSNumber *n5=[NSNumber numberWithBool: YES];

printf("%d\n",[n2 intValue]);

如果你想存储控制到集合类,可以使用NSNull,它使用NSNull *n =[NSNull null];的方式创

建。

(6.) 日期类型:

Cocoa 中使用NSDate 类型表示日期。

例:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];


//获取当前时间

NSDate *date=[NSDate date];

NSLog(@"Today is %@!",date);


//格式化一个指定的字符串为日期类型

NSCalendarDate *date2=[NSCalendarDate dateWithString:@"26 Apr 2008"

calendarFormat:@"%d %b %Y"];

NSLog(@"Today is %@!",date2);


//获取当前时间的前一天的时间

NSDate *date3=[NSDate dateWithTimeIntervalSinceNow: -24*60*60];

NSLog(@"Yestoday is %@!",date3);


[pool release];


你会看到Shell 窗口输出如下内容:

2011-03-31 16:18:43.101 Date[2540] Today is 2011-03-31 16:18:43 +0800!

2011-03-31 16:33:51.624 Date[488] Today is 26 Apr 2008!

2011-03-31 16:38:17.720 Date[4008] Yestoday is 2011-03-30 16:38:17 +0800!

常用的日期格式化字符串如下所示:

%Y 四位数的年

%y 两位数的年

%B 月份的英文全写,如January

%b 月份的英文简写,如Jan

%m 两位数的月份

%A 星期的英文全写,如Friday

%a 星期的英文简写,如Fri

%d 两位数的日期

%H 24 小时制的两位数小时

%I 12 小时制的两位数小时

%p 显示A.M 或者P.M

%M 两位数的分钟

%S 两位数的秒数

%F 三位数的毫秒

%Z 显示时区的名字

%z 显示时区与标准时区的偏移时间HHMM


(7.) 数据缓冲区:

Cocoa 中使用NSData 类型来实现缓冲区,其实你可以把NSData 当作JAVA 中的字节数组,用于存储二进制的数据类型,譬如:从网络下载回来的文件等。

NSData 的创建使用如下的类方法:

+(NSData*) dataWithBytes: (const void*) bytes length: (NSUInteger) length;

第一参数指定你要缓冲的数据的起始位置,也就是指针里存储的首地址

第二个参数指定数据的长度。

例:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

const char *cs="He is very heigh!";

NSData *data=[NSData dataWithBytes:cs  length:strlen(cs)+1 ];

NSLog(@"%@",data);

[pool release];

首先我们定义了一个C 语言的字符序列,然后把它的指针、长度赋给了NSData 的创建方法。

这里需要注意的是由于C 语言的字符序列的指针地址本身就是数组的首地址,所以不需要再

加 &计算指针的地址了。另外,长度要再加1 也是C 语言的特性。C 语言里的字符串以\0

结尾。你会看到Shell 窗口输出一组16 进制的数字,也就是缓冲区中存储的内容。

NSData 是长度不可变的数据缓冲区,还有一个NSMutableData 用来存储长度可变的数据缓冲区。


16. 写入和读取属性:

在iPhone 的*.ipa 文件中,你经常可以看到*.plist 文件,它保存了程序的相关属性,叫做属

性列表。其实它就是NSArray、NSDictionary、NSString、NSData 持久化之后的文件。

这几个类型都有一个成员方法writeToFile: (NSString*) file atomically: BOOL 用于将自己写入

到一个文件。atomically YES 表示文件先存储到临时文件区,如果文件保存成功,再替换

掉原始文件,这样的好处是可以防止在保存文件过程中出错,但是仅适用于小文件,因为你

相当于是在临时文件存储区也放了一份,再加上原始文件,就是两份文件,如果文件比较大,

将会占用较多的用户的磁盘空间。

例:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSArray *array=[NSArray arrayWithObjects: @"Apple", @"Google" , @"Microsoft" ,nil];

[array writeToFile: @"plist.txt" atomically: NO];

[pool release];

你会在当前文件夹下面找到plist.txt,打开之后内容如下所示:

(

Apple,

Google,

Microsoft

)

因为我们用的是Windows 上的GNUStep,你在Mac 电脑上看到的会是XML 的格式。

这几个对象还提供了相应的方法,将文件还原为对象。

例:

NSArray *array2=[NSArray arrayWithContentsOfFile: @"plist.txt"];

NSLog(@"%@",[array2 objectAtIndex: 1]);

这个还原文件为对象的方法有个陷阱,就是即便加载文件失败,也不会报告任何的错误,仅

仅是返回的指针指向nil

如果你要持久化的类型不是上述的数组、字典、缓冲区,那该怎么办呢?Objective-C 中也有

和JAVA 一样的序列化、反序列化支持,使用NSCoding 协议。NSCoding 协议定义了如下两个

方法:

-(void) encodeWithCoder: (NSCoder*) coder;

-(id) initWithCoder: (NSCoder*) decoder;

第一个方法相当于编码对象,第二个方法是解码回对象,也就相当于给类型定义了一个新的

init 方法而已。

例:

#import <Foundation/Foundation.h>

@interface Person: NSObject <NSCoding>{

int pid;

NSString *name;

float height;

}

-(void) setPid: (int) pid;

-(void) setName: (NSString*) name;

-(void) setHeight: (float) height;

-(int) pid;

-(NSString*) name;

-(float) height;

@end

@implementation Person

-(void) setPid: (int) p{

pid=p;

}

-(void) setName: (NSString*) n{

[n retain];

[name release];

name=n;

}

-(void) setHeight: (float) h{

height=h;

}

-(int) pid{

return pid;

}

-(NSString*) name{

return name;

}

-(float) height{

return height;

}

-(void) encodeWithCoder: (NSCoder*) coder

{

[coder encodeObject: name forKey: @"name"];

[coder encodeInt: pid forKey: @"pid"];

[coder encodeFloat: height forKey: @"height"];

}

-(id) initWithCoder: (NSCoder*) decoder

{

self=[super init];

if(self)

{

[self setName: [decoder decodeObjectForKey: @"name"]];

//name=[decoder decodeObjectForKey: @"name"]; 

//这里千万不能写成name=[decoder decodeObjectForKey: @"name"],因为name 是对象,你需要调用setter方法,运行里面的retain 操作。


pid=[decoder decodeIntForKey: @"pid"];

height=[decoder decodeFloatForKey: @"height"];

}

return self;

}

-(void) dealloc{

[name release];

[super dealloc];

}

@end

int main (int argc , const char * argv[]){

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

Person *person=[Person new];

[person setName: @"张明"];

[person setPid: 1];

[person setHeight: 185.5];

//归档对象

NSData *data=[NSKeyedArchiver archivedDataWithRootObject: person];

[data writeToFile: @"person.db" atomically: NO];

[person release];

//还原数据为对象

Person *person2=[NSKeyedUnarchiver unarchiveObjectWithData: data];

NSLog(@"%@",[person2 name]);

[pool release];

return 0;

}

红色部分的方法是编码对象的实现,其实就是用参数NSCoder 的encodeXXX:forKey:方法给你

要编码的字段取个名字。绿色部分的方法是解码对象的实现,就是用参数NSCoder 的

decodeObjectForKey:解码指定的字段。在main 函数中使用NSKeyedArchiver 的类方法

archivedDataWithRootObject 将要序列化的对象转换为NSData 的数据缓冲区类型,然后再通

过前面提到过的writeToFile:atomically:方法将数据写入到硬盘。我们还原数据为对象使用的

是NSKeyedUnarchiver 的类方法unarchiveObjectWithData。


17. 对象的复制:

对象的复制就相当于JAVA 中的clone()方法,也就是对象的深度复制,所谓深度复制就是重

新分配一个存储空间,并将原对象的内容都复制过来,从这些描述可以看出,复制也会分配

空间,那就是你要对复制出来的对象release,就是前面所说的alloc、new、copy 操作创建

的对象,要手工release。

Objective-C 中的一个对象是否可以被复制,要看它的类型是否遵循NSCopying 协议,这个协

议中有个复制方法-(id) copyWithZone: (*NSZone) zone 需要我们去实现。

我们这里使用Car(汽车)对象进行演示,Car 里有Engine(引擎)、Tire(轮胎)两个成员

变量。

Engine.h

#import <Foundation/Foundation.h>

@interface Engine: NSObject <NSCopying>

@end

Engine.m

#import "Engine.h"

@implementation Engine

-(id) copyWithZone: (NSZone*) zone

{

Engine *engine=[[[self class] allocWithZone: zone] init];

return engine;

}

@end


这里我们看一下NSCopying 的copyWithZone 方法这个方法的调用是Objective-C 的运行环

境来完成的,我们的程序在想复制一个对象的时候,调用的是copy 方法,这个方法是从

NSObject 继承而来的。copyWithZone 只有一个参数,就是系统传进来的一个表示内存空间

的NSZone 对象,也就是在这个空间里复制原来的对象。

复制一个对象需要经过如下几个步骤:

(1.) 首先调用类方法allocWithZone 传入NSZone 参数,为即将产生的新对象分配空间。你要

注意这个类方法必须用[self class]来调用,而不能用Engine 来调用。因为可能Engine 会

有子类,子类会有新的成员变量。那么子类在调用它的copyWithZone 的时候,由于

allocWithZone 用的是Engine 来调用的,那么也就是分配的空间也是按照Engine 的结构

来分配的(分配空间前面说过,主要就是给成员变量确定在内存中的位置),这样根本

就不会考虑子类新增加的成员变量,从而导致内存溢出,因为子类的新的成员变量根本

就没有存储位置[self class]的意思是获取当前实例的Class 对象,也就是运行时动态获

取的,这样就可以保证子类复制的时候,这个地方返回的是子类的Class 对象,而不是

父类Engine。

(2.) 然后调用初始化方法,完成新实例的创建。

(3.) 最后是要把原有实例中的内容都setter 到新实例。

Tire.h

#import <Foundation/Foundation.h>

@interface Tire: NSObject <NSCopying>{

int count;//轮胎的数量

}

-(Tire*) initWithCount: (int) count;

-(void) setCount: (int) count;

-(int) count;

@end

Tire.m

#import "Tire.h"

@implementation Tire

-(Tire*) initWithCount: (int) c

{

self=[super init];

if(self){

count=c;

}

return self;

}

-(void) setCount: (int) c

{

count=c;

}

-(int) count{

return count;

}

-(id) copyWithZone: (NSZone*) zone{

Tire *tire=[[[self class] allocWithZone: zone] initWithCount: count];

return tire;

}

@end

Car.h

#import "Engine.h"

#import "Tire.h"

@interface Car: NSObject <NSCopying>{

Engine *engine;

Tire *tire;

}

-(void) setEngine: (Engine*) engine;

-(void) setTire: (Tire*) tire;

-(Engine*) engine;

-(Tire*) tire;

@end

Car.m

#import "Car.h"

@implementation Car

-(void) setEngine: (Engine*) e

{

[e retain];

[engine release];

engine=e;

}

-(void) setTire: (Tire*) t{

[t retain];

[tire release];

tire=t;

}

-(Engine*) engine{

return engine;

}

-(Tire*) tire{

return tire;

}

-(id) copyWithZone: (NSZone*) zone{

Car *car=[[[self class] allocWithZone: zone] init];

Engine *engineCopy=[engine copy];

Tire *tireCopy=[tire copy];

[car setEngine: engineCopy];

[car setTire: tireCopy];

[engineCopy release];

[tireCopy release];

return car;

}

-(void) dealloc{

[engine release];

[tire release];

[super dealloc];

}

@end

这里的copyWithZone 稍显麻烦一些,因为Car 的成员变量都是对象类型,所以copyWithZone

里的engineCopy、tireCopy 的创建都使用了copy 方法,这是因为Engine、Tire 本身都支持复

制操作。

main.m

#import "Car.h"

int main(int argc,const char *argv[])

{

Engine *engine=[Engine new];

Tire *tire=[[Tire alloc] initWithCount: 4];

Car *car=[Car new];

[car setEngine: engine];

[car setTire: tire];

[engine release];

[tire release];

Car *newCar=[car copy];//复制car 对象

[car release];

//至此,原有的对象car 已经被内存回收,如果下面的代码正常运行并输出,就说

明我们的复制操作成功。

printf("The car has %d tires!",[[newCar tire] count]);

[newCar release];

return 0;

}

第三种赋值策略:

我们前面在对象的内存管理中讲解了setter 方法有强、弱两种类型的赋值策略,但他们的本

质都还是对象在参数传递中传址(对象的指针),也就是setter 方法的参数、赋值后的成员

变量都指向同一个对象,只要任何一方把对象中的内容改变了,另一方都会看到。如果我们

想让对象类型的参数传值呢?很显然是把传递给setter 方法的参数copy 一份,把复制后的

克隆对象传递给成员变量,这样参数、成员变量都会指向自己的对象,可以各自独立使用。

18. 多线程:

Objective-C 的多线程编程与JAVA 语言极其类似分为原始的线程操作、线程池两种,后者其

实就是使用队列等机制对前者的封装。JAVA 也一样,原始的线程操作使用Thread 类,线程

池的操作使用java.util.concurrent.*中的类库。

(1.) NSThread:

Objective-C 中NSThread 类型表示线程,线程对象的创建主要使用以下几个方法:

A.- (id) init;

B.- (id) initWithTarget: (id) target selector: (SEL)selector object: (id) argument;

第一个参数指定目标对象,一般情况下就是当前的实例self。第二个参数指定要运行的方法,

相当于JAVA 中的run 方法。第三个参数一般为nil。

C.+(void) detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

这个方法是类方法,与第二个方法其实在参数上是一致的,区别就是你不需要显示的release

这个线程,因为没有使用alloc 关键字。

通常我们使用后两种方法,因为不但init 完成,还设置好了一个线程所必需的一些元素(例

如:运行的run 方法)。

启动一个线程使用start 方法,结束一个线程使用exit 方法,但要注意如果你使用了exit 

法,首先要将这个线程的引用计数器归0

(2.) NSCondition:

NSCondition 用于执行同步操作,在功能和用法上相当于JAVA 中的java.util.concurrent.*包中

的Lock 对象。一段代码如果需要同步就用NSCondition lock 一下,同步的部分执行完毕再

unlock 一下,也就是说lockunlock 之间的代码是加锁的

另外,Objective-C 也支持@synchronized 指令做代码同步,写法也和JAVA 中的很相似。

@synchronized(要加锁的对象)

{

同步执行的代码

}


(3.) 示例程序:

下面我们依然通过JAVA 中最经典的售票案例演示Objective-C 的多线程代码。

#import <Foundation/Foundation.h>

//定义售票接口

@interface SellTickets: NSObject{

int tickets;//未售出的票数

int count;//售出的票数

NSThread* ticketsThread1;//线程1

NSThread* ticketsThread2;//线程2

NSThread* ticketsThread3;//线程3

NSCondition* ticketsCondition;//同步锁

}

- (void) ticket;//售票方法

@end

//实现售票类

@implementation SellTickets

//这个方法初始化了成员变量,并启动了三个线程,其中前两个线程执行售票方法run,第

三个线程执行用于测试线程之间是否异步执行的asyn 方法。

- (void) ticket

{

tickets = 100;

count = 0;

ticketsCondition = [[NSCondition alloc] init];

ticketsThread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

[ticketsThread1 setName:@"Thread-1"];

[ticketsThread1 start];


ticketsThread2 = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

[ticketsThread2 setName:@"Thread-2"];

[ticketsThread2 start];


ticketsThread3 = [[NSThread alloc] initWithTarget:self selector:@selector(asyn) object:nil];

[ticketsThread3 setName:@"Thread-3"];

[ticketsThread3 start];

[NSThread sleepForTimeInterval:80];  // 让主线程暂停80 秒

}

//测试线程之间是否异步执行。

-(void) asyn

{

[NSThread sleepForTimeInterval:5];

printf("***********************************\n");

}

- (void)run

{

while (YES)

{

     [ticketsCondition lock];   //上锁

     if(tickets > 0)

   {

        [NSThread sleepForTimeInterval:0.5];

        count = 100 - tickets;

        NSLog(@"当前票数是:%d,售出:%d,线程名:%@",tickets,count,

        [[NSThread currentThread] name]);

        tickets--;

   }

   else

   {

        break;

   }

   [ticketsCondition unlock];   //解锁

}

}

- (void)dealloc

{

//回收线程、同步锁的实例

[ticketsThread1 release];

[ticketsThread2 release];

[ticketsThread3 release];

[ticketsCondition release];

[super dealloc];

}

@end

int main(int argc,const char *argv[])

{

SellTickets *st=[[SellTickets alloc] init];

[st ticket];

[st release];

return 0;

}

注意红色部分的代码,我们在这里让主线程暂停80 秒,因为如果你不这么做,由于三个线

程是异步执行的,ticket 方法被主线程执行完毕后,由于主线程的退出,会导致三个线程也

被结束掉,你会发现售票过程根本没有被执行。实际开发中你是不需要这么做的,例如:你

在一个UI 窗体上异步加载图片,由于主线程UI 窗体不会退出,因此你发起的线程也就不会

被结束掉,你就不必加这么一句暂停主线程的垃圾代码了。

另外就是线程3,它没有参与售票,用意就是想看一下三个线程是否异步执行,按照asyn

中的代码,在程序5 秒钟后,线程3 会夹杂在线程1、2 的售票输出语句中输出一串*。

(4.) 与主线程的交互:

如果你想更新UI 上的某一个部件,就需要在发起的新线程里调用UI 所在的主线程上的一个

方法,新线程不能直接访问主线程的方法,需要在run 方法中使用如下的方法:

- (void) performSelectorOnMainThread: (SEL) aSelector withObject: (id) arg waitUntilDone: (BOOL) wait

例如:

- (void) run

{

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

[self performSelectorOnMainThread: @selector(moveText) withObject: nil waitUntilDone: NO];

// 移动窗体上的一个文本标签

[pool release];

}

(5.) 线程池:

Objective-C 使用NSOperation、NSOperationQueue 两个类型实现线程池的操作,NSOpertion

就好比JAVA 中的Runnable,其中的main(名字有点儿奇怪)方法也就是你要执行的操作,

NSOperationQueue 是一个线程池队列,你只需要把NSOperation 添加到NSOperationQueue,

队列就会retain 你的NSOperation,然后执行其中的操作,直到执行完毕。队列中会有多个

操作,队列会按照你添加的顺序逐个执行,也就说队列中的任务是串行的。

例:

#import <Foundation/Foundation.h>

//定义一个任务对象,它要实现NSOperation,这与JAVA 中的任务要实现Runnable 接口是

一样的。

@interface MyOperation: NSOperation{

int num;

}

//我们自定义的init 方法。

-(MyOperation*) initWithNum: (int) num;

@end

@implementation MyOperation

-(MyOperation*) initWithNum: (int) n{

self=[super init];

if(self){

num=n;

}

return self;

}

-(BOOL) isConcurrent{

return YES;

}

//任务方法,从NSOperation 继承

-(void) main{

//[NSThread sleepForTimeInterval: 5];

NSLog(@"%d\n",++num);

}

@end

int main(int argc,const char *argv[]){

NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

NSOperation *operation1=[[MyOperation alloc] initWithNum: 1];

[operation1 autorelease];

NSOperation *operation2=[[MyOperation alloc] initWithNum: 2];

[operation2 autorelease];

NSOperationQueue *queue=[[NSOperationQueue alloc] init];

[queue setMaxConcurrentOperationCount: 2];

[queue addOperation: operation1];//将任务添加到队列

[queue addOperation: operation2];

[NSThread sleepForTimeInterval: 15];

[queue release];

[pool release];

return 0;

}

这里的红色代码不用说明了,与前面的例子一样,为了不让主线程结束。蓝色的方法

isConcurrent 是表明该Operation 可以并发执行,也就是它在Queue 中与其他任务是并行执

行的,而不是一开始说的NSOperationQueue 中的Operation 是串行执行的,默认Operation

串行执行的原因是NSOperation 中的isConcurrent 方法返回NO。绿色的代码表示Queue 中

最多可以并行支持的Operation 数量,如果超过这个值,则放入等待队列。但是GNUStep 似

乎不支持并行执行,只要isConcurrent 方法返回YES,运行时就会报错。

如果线程池可以满足你的业务需求,尽量使用线程池,而不是原始的NSThread,因为使用

线程池屏蔽了许多线程自身需要处理的问题,代码也更加简洁。

19. KVC 与KVO:

KVC 是NSKeyValueCoding 的缩写,它是Foundation Kit 中的一个NSObject 的Category,作用

你可以类比JAVA 中的反射机制,就是动态访问一个对象中的属性。我们以第16 节中的Person

为例进行代码演示。

我们知道正常访问Person 中的name 的方式为:

[person setName: @"豆豆"];//setter

NSLog(@"%@",[person name]);//getter

KVC 中可以这样访问:

[person setValue: @"太狼" forKey: @"name"];

NSLog(@"%@",[person valueForKey: @"name"]);

我们看到KVC 使用了和NSDictionary 差不多的key-value 的操作方法,也就是说它是按照一

个字符串key 在对象中查找属性,然后给它设置值、获取值。如果key 为xxx,则对于获取

属性的值的查找规则如下所示:

A. 首先查找.h 文件中声明的成员方法getXxx 或者xxx;

B. 然后查找.m 文件中声明的变相的私有成员方法_getXxx 或者_xxx

C. 最后查找成员变量_xxx 或者xxx。

KVC 还支持对象导航图的方式访问属性,首先看一段伪代码:

Person{

Address *address;

}

-(id) init{

self=[super init];

if(self){

address=[Address new];

}

return self;

}

@end

Address{

NSString *city;

}

@end

Person person=[[Person alloc] init;

如果我们想访问person 中的address 中的city,你可以如下操作:

[person setValue: @"北京" forKeyPath: @"address.city"];

NSLog(@"%@",[person valueForKeyPath: @"address.city"]);

我们注意到与前面的相比,这里多了个Path,也就是按照路径搜索,所谓的路径就和JAVA

中的EL 表达式差不多,你可以使用.操作符,一级一级的查找属性。这里请注意红色代码

address=[Address new];,如果没有这一句,你会发现上面输出nil,也就是设置值没有成

功,原因是person 的address 是对象类型,初始化的时候指向了nil,还没有分配存储空间,

因此你也就不能用KVC 去给address 里面的city 设置值。

你还要注意的是KVC 有个和EL 表达式不一样的地方,就是你不能对数组使用索引,也就是

Person 里有一个NSArray *schools 的成员变量,你不能使用schools[0]这种形式,只能获取全

部的schools 再自己分拣。

虽然KVC 不支持对数组进行索引操作,但是支持运算符@count、@sum、@avg、@min、@max

操作。假设Person 的NSArray *school 的数组中存放的是School 类型,School 有学校的名字

name、入学时间date、课程数目lessons。那么[person valueForKeyPath: @”schools.@count”]

可以计算数组中有多少个学校,[person valueForKeyPath: @”schools.@sum.lessons”]可以

计算这个人读过的所有学校的课程总数。

KVC 在解析key 的字符串的时候,是会比你正常调用setter、getter 要慢的,而且编译器无法

在编译器对你的方法调用做出检查(因为你的属性名都是字符串,只有运行时才会知道你有

没有写错),出错的几率也会提高,所以请不要随意使用KVC,而省去setter、getter 方法。

KVC 一般用于动态绑定,也就是运行时才能确定谁调用哪个方法,编译期并不确定。

KVO就是NSKeyValueObserving的缩写,它也是Foundation Kit中的一个NSObject的Category,

KVO 基于KVC 实现,基于观察者设计模式(Observer Pattern)实现的一种通知机制,你可以

类比JAVA 中的JMS,通过订阅的方式,实现了两个对象之间的解耦,但又可以让他们相互

调用。

按照观察者模式的订阅机制,KVO 中必然有如下三个方法:

A. 订阅(Subscribe)

- (void) addObserver: (NSObject*) anObserver

forKeyPath: (NSString*) aPath

options: (NSKeyValueObservingOptions) options

context: (void*) aContext;

参数options 为NSKeyValueObservingOptionOld、NSKeyValueObservingOptionNew。

B. 取消订阅(Unsubscribe)

- (void) removeObserver: (NSObject*) anObserver

forKeyPath: (NSString*) aPath;

C. 接收通知(Receive notification)

- (void) observeValueForKeyPath: (NSString*) aPath

ofObject: (id) anObject

change: (NSDictionary*) aChange

context: (void*) aContext;

这三个方法的参数不大容易直接说清楚都是什么意思,下面我们通过实际的代码来理解。这

段代码的业务含义就是警察一直监视犯人的名字是否发生变化,只要发生变化,警察就会收

到通知。

#import <Foundation/Foundation.h>

//犯人类型

@interface Prisoner: NSObject{

int pid;

NSString *name;

}

-(void) setPid: (int) pid;

-(void) setName: (NSString*) name;

-(int) pid;

-(NSString*) name;

@end

@implementation Prisoner

-(void) setPid: (int) p{

pid=p;

}

-(void) setName: (NSString*) n{

[n retain];

[name release];

name=n;

}

-(int) pid{

return pid;

}

-(NSString*) name{

return name;

}

-(void) dealloc{

[name release];

[super dealloc];

}

@end

//警察类型

@interface Police: NSObject

@end

@implementation Police

//接收通知的方法,继承自NSObject 父类。

//请先看main 函数中的addObserver 方法参数的解释再来这个方法的解释。

//第一个参数是你监视的对象上的属性,第二个参数是你监视的对象,第三个参数存放了你

监视的属性的值,最后一个参数我们传递nil。

- (void) observeValueForKeyPath: (NSString*) aPath

ofObject: (id) anObject

change: (NSDictionary*) aChange

context: (void*) aContext{

if([aPath isEqualToString: @"name"]){

NSLog(@"Police : %@",[aChange objectForKey: @"old"]);

NSLog(@"Police : %@",[aChange objectForKey: @"new"]);

//因为main 函数中我们监听name 的新旧两个值,所以aChange 这个字典对

象里就存放了@”old”、@”new”两个key-value 对。

}

}

@end

int main (int argc , const char * argv[]){

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

Prisoner *prisoner=[[Prisoner alloc] init];

Police *police=[[Police alloc] init];

//为犯人添加观察者警察,警察关注犯人的name 是否发生变化,如果发生变化就立即

通知警察,也就是调用Police 中的observeValueForKeyPath 方法。

//换句话说就是警察对犯人的名字很感兴趣,他订阅了对犯人的名字变化的事件,这个

事件只要发生了,警察就会收到通知。

[prisoner addObserver: police

forKeyPath: @"name"

options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld

context: nil];

//addObserver 的调用者是要被监视的对象,第一个参数是谁要监视它,第二个参数是

监视它的哪个属性的变化(使用KVC 机制,也就是前面所说的KVO 基于KVC),第三个参

数是监视属性值改变的类型,我们这里监听Old、New,也就是Cocoa 会把name 属性改变

之前的旧值、改变之后的新值都传递到Police 的处理通知的方法,最后一个参数我们传递

nil。

//这里有一个陷阱,如果你不小心把forKeyPath 的属性名写错了,prisoner 里根本就不

存在, 那么Cocoa 不会报告任何的错误。所以当你发现你的处理通知的

observeValueForKeyPath 没有任何反应的时候,首先看一下是不是这个地方写错了。

[prisoner setName: @"豆豆"];

//警察取消订阅犯人名字变化的事件。

[prisoner removeObserver: police

forKeyPath: @"name"];

[prisoner setName: @"太狼"];

[prisoner release];

[police release];

[pool release];

return 0;

}

运行之后,Shell 窗口输出:

2011-04-01 15:20:33.467 Kvo[2004] Police : <null>//因为name 没有old 值

2011-04-01 15:09:18.479 Kvo[4004] Police : 豆豆

Notification 是Objective-C 中的另一种事件通知机制,我们依然以犯人、警察的示例进行演

示。

#import <Foundation/Foundation.h>

//犯人类型

@interface Prisoner: NSObject{

int pid;

NSString *name;

}

-(void) setPid: (int) pid;

-(void) setName: (NSString*) name;

-(int) pid;

-(NSString*) name;

@end

@implementation Prisoner

-(void) setPid: (int) p{

pid=p;

}

-(void) setName: (NSString*) n{

[n retain];

[name release];

name=n;

}

-(int) pid{

return pid;

}

-(NSString*) name{

return name;

}

-(void) dealloc{

[name release];

[super dealloc];

}

@end

//警察类型

@interface Police: NSObject

-(void) handleNotification:(NSNotification *) notification;

@end

@implementation Police

-(id) init{

self=[super init];

if(self){

//获取通知中心,它是单例的。

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

//向通知中心把自己添加为观察者,第一个参数是观察者,也就是警察自己,

第二个参数是观察的事件的标识符prisioner_name,这就是一个标识性的名字,不是特指哪

一个属性,第三个参数指定handleNotification 为处理通知的方法。

[nc addObserver: self selector:@selector(handleNotification:)

name:@" prisioner_name" object:nil];

}

}

//接收通知,这个方法的名字任意,只有参数是Notification 就可以了。

-(void) handleNotification:(NSNotification *) notification{

Prisoner *prisoner=[notification object];//获得通知中心传递过来的事件源对象

NSLog(@"%@",[prisoner name]);

}

@end

int main (int argc , const char * argv[]){

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

Prisoner *prisoner=[[Prisoner alloc] init];

Police *police=[[Police alloc] init];

[prisoner setName: @"豆豆"];

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

//向通知中心发送通知,告知通知中心有一个prisioner_name 的事件发生了,并把自

己作为事件源传递给通知中心。

//通知中心随后就会查找是谁监听了prisioner_name 事件呢?找到之后就调用观察者

指定的处理方法,也就是Police 中的handleNotification 方法被调用。

[nc postNotificationName: @"prisioner_name" object: prisoner];

//从通知中心移除观察者

[nc removeObserver: police];

[prisoner setName: @"太狼"];

[prisoner release];

[police release];

[pool release];

return 0;

}

Shell 窗口输出如下所示:

2011-04-01 16:20:58.995 Notification[2564] 豆豆

我们看到这种通知方式的实现非常不优雅,观察者Police 中耦合了通知中心的API,而且最

重要的是看到红色的一行代码了吗?通知的发送是需要被监视的对象主动触发的。

20. 谓词NSPredicate:

Cocoa 提供了NSPredicate 用于指定过滤条件,谓词是指在计算机中表示计算真假值的函数,

它使用起来有点儿像SQL 的查询条件,主要用于从集合中分拣出符合条件的对象,也可以

用于字符串的正则匹配。首先我们看一个非常简单的例子,对谓词有一个认知。

#import <Foundation/Foundation.h>

@interface Person: NSObject{

int pid;

NSString *name;

float height;

}

-(void) setPid: (int) pid;

-(void) setName: (NSString*) name;

-(void) setHeight: (float) height;

-(int) pid;

-(NSString*) name;

-(float) height;

@end

@implementation Person

-(void) setPid: (int) p{

pid=p;

}

-(void) setName: (NSString*) n{

[n retain];

[name release];

name=n;

}

-(void) setHeight: (float) h{

height=h;

}

-(int) pid{

return pid;

}

-(NSString*) name{

return name;

}

-(float) height{

return height;

}

-(void) dealloc{

[name release];

[super dealloc];

}

@end

int main (int argc , const char * argv[]){

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

//实例化三个Person,并放入数组。

NSMutableArray *array=[NSMutableArray arrayWithCapacity: 5];

Person *person1=[[Person alloc] init];

[person1 setPid: 1];

[person1 setName: @"Name1"];

[person1 setHeight: 168];

[array addObject: person1];

Person *person2=[[Person alloc] init];

[person2 setPid: 2];

[person2 setName: @"Name2"];

[person2 setHeight: 178];

[array addObject: person2];

Person *person3=[[Person alloc] init];

[person3 setPid: 3];

[person3 setName: @"Name3"];

[person3 setHeight: 188];

[array addObject: person3];

//创建谓词,条件是pid>1 并且height<188.0。其实谓词也是基于KVC 的,也就是如

果pid 在person 的成员变量xxx 中,那么此处要写成xxx.pid>1。

NSPredicate *pre = [NSPredicate predicateWithFormat:

@" pid>1 and height<188.0"];

int i=0;

for(;i<[array count];i++){

Person *person=[array objectAtIndex: i];

//遍历数组,输出符合谓词条件的Person 的name。

if ([pre evaluateWithObject: person]) {

NSLog(@"%@",[person name]);

}

}

[person1 release];

[person2 release];

[person3 release];

[pool release];

return 0;

}

Shell 窗口输出如下所示:

2011-04-01 16:51:18.382 Predicate[2400] Name2

我们看到创建谓词使用类方法predicateWithFormat: (NSString*) format,format 里的东西真的

和SQL 的where 条件差不多。另外,参数format 与NSLog 的格式化模版差不多,如果1 和

188.0 是传递过来的参数,你可以写成如下的形式:

@"pid>%d and height<%f",1,188.0

(1.) 逻辑运算符:AND、OR、NOT

这几个运算符计算并、或、非的结果。

(2.) 范围运算符:BETWEEN、IN

例:

@”pid BETWEEN {1,5}”

@"name IN {'Name1','Name2'}"

(3.) 占位符:

NSPredicate *preTemplate = [NSPredicate predicateWithFormat:@"name==$NAME"];

NSDictionary *dic=[NSDictionary dictionaryWithObjectsAndKeys:

@"Name1", @"NAME",nil];

NSPredicate *pre=[preTemplate predicateWithSubstitutionVariables: dic];

占位符就是字段对象里的key,因此你可以有多个占位符,只要key 不一样就可以了。

(4.) 快速筛选数组:

前面我们都是使用谓词逐个判断数组内的对象是否符合,其实数组本身有更为便捷的方法,

直接筛选出一个符合谓词的新数组。

NSPredicate *pre = [NSPredicate predicateWithFormat:@"pid>1"];

NSMutableArray *arrayPre=[array filteredArrayUsingPredicate: pre];

NSLog(@"%@",[[arrayPre objectAtIndex: 0] name]);

(5.) 字符串运算符:

BEGINSWITH、ENDSWITH、CONTAINS 分别表示是否以某字符串开头、结尾、包含。

他们可以与c、d 连用,表示是否忽略大小写、是否忽略重音字母(字母上方有声调标号)。

例:

@”name BEGINSWITH[cd] ‘He’”

判断name 是否以He 开头,并且忽略大小写、忽略重音字母。

(6.) LIKE 运算符:

LIKE 使用?表示一个字符,*表示多个字符,也可以与c、d 连用。

例:

@”name LIKE ‘???er*’” 与Paper Plane 相匹配。

(7.) SELF:

前面的数组中放的都是对象,如果数组放的都是字符串(或者是其他没有属性的类型),该

怎么写谓词呢?这里我们使用SELF。

例:

NSArray *arrays=[NSArray arrayWithObjects: @"Apple", @"Google", @"MircoSoft", nil];

NSPredicate *pre2 = [NSPredicate predicateWithFormat:@"SELF=='Apple'"];

(8.) 正则表达式:

NSPredicate 使用MATCHES 匹配正则表达式,正则表达式的写法采用international components

for Unicode (ICU)的正则语法。

例:

NSString *regex = @"^A.+e$";//以A 开头,以e 结尾的字符。

NSPredicate *pre= [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];

if([pre evaluateWithObject: @"Apple"]){

printf("YES\n");

}else{

printf("NO\n");

}

原创粉丝点击