Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5520400
  • 博文数量: 763
  • 博客积分: 12108
  • 博客等级: 上将
  • 技术积分: 15717
  • 用 户 组: 普通用户
  • 注册时间: 2007-09-28 21:21
个人简介

业精于勤,荒于嬉

文章分类

全部博文(763)

文章存档

2018年(6)

2017年(15)

2016年(2)

2015年(31)

2014年(14)

2013年(87)

2012年(75)

2011年(94)

2010年(190)

2009年(38)

2008年(183)

2007年(28)

分类: C/C++

2011-08-10 11:17:48

14. 内存管理:

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

 

Objective-C 在使用allocnewcopy 的时候为对象分配内存,然后返回分配的内存的首地址存入指针变量,使用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 传递给了bc 两个实例,而a 实例本身又是在main 函数中定义的,因此一共有main 函数、bc 三个地方引用了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 操作毫无意义,仅仅是演示retainrelease 之后引用计数器的变化情况。

 

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

Address.h

#import

@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 成员变量citystreet 都是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

 

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

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 函数中创建了citystreet 两个NSString 的实例,然后setter address 实例,

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

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

street retainCount 会由1 变为0,这就会导致你传递到address 里的citystreet 指针指向的对象被清理掉,而使address 出错。很显然,你需要在address setter 方法里retain 一下,增加citystreet 的引用计数为2,这样即便在main 函数release 之后,citystreet 指向的内存空间也不会被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 中持有了citystreet 指向的对象的拥有权,那么你势必要和main 函数一样,负责在恰当的时刻release 你的拥有权,由于你在address 实例中可能一直要用到citystreet,因此release 的最佳时刻就是address 要被回收的时候。另外,我们知道对象被dealloc 之后,原来指向它的指针们依然指向这块内存区域,Objective-C 并不会把这些指着垃圾的指针们都指向nil,因此你如果还调用这些指针们的方法,就会报错。因此有些人喜欢在dealloc 里把对象release 之后紧接着指向nil

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

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

 

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

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

 

第八节的DenominatorNotZeroException 异常我们是通过父类 NSException 的类方法exceptionWithName:reason:userInfo 创建的,我们并没有使用到allocnewcopy 关键字,这种情况下,你只需要定义一个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的行数;i++){

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

session.save(obj);

}

Transaction.commit();

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

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

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

Session session=获取Hibernate JDBC 连接对象

for(int i=0;i的行数;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 类型, 常用的枚举值为NSCaseInsensitiveSearchNSNumericSearch,分别表示忽略大小写、字数是否相等。由于

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

 

-(BOOL) hasPrefix: (NSString) s

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

-(NSRange) rangeOfString: (NSString*) s

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

location 为包含的字符串s 所在的起始位置,length 为长度。如果不包含字符串s,则locationNSNotFoundlength 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 表示数组,但是它不能存储基本数据类型、enumstructnil只能存

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

{

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 中的IteratorreserveObjectEnumerator 用于获取反转

之后的数组迭代器。与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

@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.) 封装类:

前面的几个容器类的对象都不能存放基本数据结构、enumstructnil,怎么办呢?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 文件,它保存了程序的相关属性,叫做属

性列表。其实它就是NSArrayNSDictionaryNSStringNSData 持久化之后的文件。

这几个类型都有一个成员方法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

@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) 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,就是前面所说的allocnewcopy 操作创建

的对象,要手工release

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

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

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

变量。

Engine.h

#import

@interface Engine: NSObject

@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

@interface Tire: NSObject {

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 {

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

里的engineCopytireCopy 的创建都使用了copy 方法,这是因为EngineTire 本身都支持复

制操作。

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

//定义售票接口

@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 会夹杂在线程12 的售票输出语句中输出一串*

(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 使用NSOperationNSOperationQueue 两个类型实现线程池的操作,NSOpertion

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

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

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

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

例:

#import

//定义一个任务对象,它要实现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 的字符串的时候,是会比你正常调用settergetter 要慢的,而且编译器无法

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

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

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

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

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

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

调用。

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

A. 订阅(Subscribe

- (void) addObserver: (NSObject*) anObserver

forKeyPath: (NSString*) aPath

options: (NSKeyValueObservingOptions) options

context: (void*) aContext;

参数options NSKeyValueObservingOptionOldNSKeyValueObservingOptionNew

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

//犯人类型

@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),第三个参

数是监视属性值改变的类型,我们这里监听OldNew,也就是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 : //因为name 没有old

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

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

示。

#import

//犯人类型

@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

@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*) formatformat 里的东西真的

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

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

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

(1.) 逻辑运算符:ANDORNOT

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

(2.) 范围运算符:BETWEENIN

例:

@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.) 字符串运算符:

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

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

例:

@”name BEGINSWITH[cd] ‘He’”

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

(6.) LIKE 运算符:

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

例:

@”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");

}

阅读(3211) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~