分类: C/C++
2015-03-16 14:24:18
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
@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
Object obj=每一行的Excel 记录对应的JAVA 对象;
session.save(obj);
}
Transaction.commit();
由于Hibernate 的一级缓存是在你提交事务的时候才清空,并且刷新到数据库上的,因此在
循环结束前,你的一级缓存里会积累大量的obj 对象,使得内存占用越来越大。
解决办法与Objective-C 的自动回收池差不多,就是如下的做法:
Session session=获取Hibernate 的JDBC 连接对象
for(int i=0;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
{
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
@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---Integer、boolean---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
@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,就是前面所说的alloc、new、copy 操作创建
的对象,要手工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
里的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 一下,也就是说lock、unlock 之间的代码是加锁的。
另外,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 会夹杂在线程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
//定义一个任务对象,它要实现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
//犯人类型
@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 :
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*) 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");
}