这节课的主要内容是Core Data、NSNotificationCenter和Objective-C Categories。

Core Data

它是一个完全面向对象的API,负责在数据库中存储数据,底层也是由类似于SQL的技术来实现的。

在高级语言这一层,如何使用Core Data?在xcode中,有个工具可以建立对象之间的映射,这些对象会存储在你的数据库里,它们是NSObject的子类,实际上是NSManagedObject的子类,然后Core Data负责管理这些对象之间的关系。一旦在xcode中建立了visual map,你就可以新建对象,并存到数据库里或在数据库里删除、查询,实际起作用的是底层的SQL。可以用property访问数据库中对象内部的数据。Core Data负责管理底层的通信。

如何建立visual map?打开New File界面,在左边找到Core Data,这里选择Data Model,然后点Next,这样就建立了一个数据库的图形化model。通常会给visual map一个和应用相同的名字。

map的内部结构是怎样的?由3个不同的部分组成:一是entities,它们将映射到class;还有attributes,它映射到properties上;然后是relationship,这个属性用来指向数据库中的其他对象。

新建两个entity,分别是photo和photographer,它们之间会有一个明显的relationship。在代码中,entity实际上是一个NSManagedObject。

接下来要做的是如何创建NSManagedObject的子类,有了这些子类就可以调用数据库中的entity了。即使创建了子类,管理这些对象的底层机制仍然是NSManagedObject。

记住,所有的attribute都是对象,Core Data只知道在数据库中如何读写对象,所有的attribute都是各种不同类型的对象。有几种方法可以获取这些对象的值,一种方法是可以用NSKeyValueCoding协议,valueForKey和setValueForKey是这个协议的一部分,所有对象都可以使用它们,用valueForKey和setValueForKey设置property;另一种访问attribute的方法是新建一个NSManagedObject的子类,数据库的所有对象在代码中都是NSManagedObject。

不仅可以以表格的形式查看entity和attribute,还可以用图的方式。点击右下角的Editor Style,看到的内容与刚才一样,但是是以图的方式。可以在entities之间按住control拖动,来建立它们之间的relationship。一旦建立了关系,可以双击它,然后在inspector里改变它的名字,有个开关叫To-Many Relationship,就是设置两者间一对多的关系,注意其中的Delete Rule,意思是如果删除其中一个,那么会对这个relationship指向的东西有什么影响?其实就是把指针设为空。relationship的property类型:whoTook这个property的类型是NSManagedObject *;photos的类型是NSSet,它是一个内部数据类型为NSManagedObject *的NSSet。NSSet就是一堆对象的集合,它是无序的。

怎么在代码中使用visual map的数据呢?要获得数据,最重要的一点是,需要使用一个NSManagedObjectContext的东西,这是一个类,需要实例化。可以给这个实例发消息,比如查询之类。

怎么得到NSManagedObjectContext呢?需要它来往数据库里添加数据或进行查询操作,有两种基本方法可以获得NSManagedObjectContext:其一是创建UIManagedDocument,它有个属性叫managedObjectContext,获取它并使用就好了;第二种方法是在你新建一个工程的时候,有个复选框Use Core Data,选中它,就会在AppDelegate中生成一些代码,添加一个managedObjectContext的property。

UIManagedDocument

UIManagedDocument类继承自UIDocument,UIDocument有一套机制来管理一个或一组与磁盘相关的文件。UIManagedDocument实际上是一个装载Core Data数据库的容器,而且这个容器提供一些功能,比如写入、打开数据库。

怎么创建UIManagedDocument呢?它只有一个intializer,叫做initWithFileURL:

UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url];

这个url几乎总是在文档目录下。现在还不可以用,还需要打开它,或者是创建,来使用。alloc init之后,它实际上并没有在磁盘上打开或创建。怎么打开或创建document?要调用以下方法来打开它:

- (void)openWithCompletionHandler:(void (^)(BOOL success))completionHandler;

CompletionHandler就是一个简单的block,这是一个没有返回值的block,它只处理一个表明是否成功打开文件的布尔值。如果文件不存在,不得不检查一下,必须调用fileExistsAtPath来检查这个文件是否存在:

[[NSFileManager defaultManager] fileExistsAtPath:[url path]]

如果这个文件存在,就可以用openWithCompletionHandler。但是如果不存在,需要创建它,需要调用UIManagedDocument里的这个方法来创建:

- (void)saveToURL:(NSURL *)url
 forSaveOperation:(UIDocumentSaveOperation)operation 
competionHandler:(
void (^)(BOOL success))completionHandler;

创建完之后,如果要保存需要调用UIDocumentSaveForCreating。这边也有一个CompletionHandler。

为什么会有一个CompletionHandler呢?open和save方法是异步的,这些操作要花费一些时间,它们会立刻返回,但文件此时还没打开或创建好,只有在之后CompletionHandler被调用的时候,才能用这个document。异步的意思是这些操作需要花费一些时间,当这些操作完成之后调用你的block。

这是一个典型的例子:

self.document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url]; 
if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
    [document openWithCompletionHandler:^(BOOL success) { 
          if (success) [self documentIsReady]; 
          if (!success) NSLog(@“couldn’t open document at %@”, url);
    }]; 
} else {
    [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating
      completionHandler:^(BOOL success) { 
          if (success) [self documentIsReady]; 
          if (!success) NSLog(@“couldn’t create document at %@”, url);
    }];
}

在这还不能对文档进行任何操作,因为这两个调用是异步的,必须等block被调用后,并激活一些条件才可以。

如果document打开了或创建好了,documentIsReady被调用了,你就可以使用它了:

- (void)documentIsReady {
     if (self.document.documentState == UIDocumentStateNormal) {               
NSManagedObjectContext *context = self.document.managedObjectContext; // do something with the Core Data context } }

document中有一个documentState的东西,通常在使用它之前都会检查这个documentState,最重要的状态就是UIDocumentStateNormal,意思就是已经打开好了,可以用了。如果状态是normal的话,我要做的是获得document context,然后就可以做Core Data的操作了,创建对象,查询,或从数据库在读取一些东西等等。

其他一些状态:UIDocumentStateClosed,这是document开始时的状态,当alloc initWithFileURL时,它的状态就是closed的;UIDocumentStateSavingError,这是指当保存文件时调用CompletionHandler出现了success等于NO,就会出现这种状态;UIDocumentStateEditingDisabled,这个状态是一个瞬时的状态,或许document正在重置,重置回以前保存的状态,或者保存操作正在进行,不能进行编辑;UIDocumentStateInConflict,这是处理iCloud时可能遇到的情况。

documentState的状态通常处于observed(监听)中,这是指,在ios中有一种方法,当documentState改变时,就告诉我,或者当有一个冲突出现了,马上告诉我,我好立刻解决问题。这个observed怎么用,它由NSNotification这个机制来管理。

NSNotification

有一种通信方式是广播站模式的,这种模式有点像广播,其他人可以接进这个广播站来并收听消息,这就是NSNotification。有一种办法可以让一个对象注册成为radio station,然后其他对象收听这个radio station。

需要一个NSNotificationCenter,就像交换中心似的,也可以把它想象成一个广播站注册机构。最简单的方式是调用[NSNotificationCenter defaultCenter],然后给NSNotification传递一个方法:

- (void)addObserver:(id)observer    // you (the object to get notified)
           selector:(SEL)methodToSendIfSomethingHappens 
               name:(NSString *)name // what you’re observing (a constant somewhere)
             object:(id)sender;    // whose changes you’re interested in (nil is anyone’s)

addObserver就是你自己,你把自己设置为observer。selector是指当广播站广播时,会被调用的方法。name是指radio station的名字,是一个常量字符串,几乎总是常量类型的,一些类会告诉你它们广播站的名字,好让你注册。object是指你想收听的对象,你可以注册收听广播站上的任何广播,或者只收听某个特定的广播,如果是nil,就是收听所有的广播。

必须指定selector的名字,它的参数总是NSNotification *。NSNotification有三个property,一个是name,就是radio station的name,和上面一样;object,就是给你发送通知的那个对象,和上面一样。然后是userInfo,它就是个ID,可以是任何东西,由广播员负责告诉你现在正在播放什么内容,通常它会像一个词典或者某种容器来保存数据。

- (void)methodToSendIfSomethingHappens:(NSNotification *)notification {
       notification.name     // the name passed above 
       notification.object   // the object sending you the notification     
notification.
userInfo// notification-specific information about what happened }

下面来看一个例子,是关于documentState的:

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self 
           selector:@selector(documentChanged:)
               name:UIDocumentStateChangedNotification  
             object:self.document];

把自己添加成observer。这边要注册的广播站是UIDocumentStateChangedNotification,这是在UIManagedDocument中定义的一个NSString,其实是在UIDocument.h中。object是我想收听的对象,所以在这写self.document。只要把这个消息传递给center,只要documentState有变化,我就会得到一个documentChanged的消息,这个消息会有一个NSNotification *参数。

当你不再需要监听广播时,要删除自己的observer身份。原因是,NSNotification不会维护一个指向你的weak指针,它维护一个unsafe或者是unretained的指针。这并不安全,如果被指向的对象消失,unsafe或者unretained指针会指向堆上的一块无用的内存,必须要确保在对象消失之前解除你的observer身份。

[center removeObserver:self];
or
[center removeObserver:self name:UIDocumentStateChangedNotification object:self.document];

很有可能,会在viewDidAppear或者viewWillDisappear中传递add或者remove消息,这边有一个例子:

- (void)viewDidAppear:(BOOL)animated{
     [super viewDidAppear:animated];
     [center addObserver:self
                selector:@selector(contextChanged:)   
                    name:NSManagedObjectContextObjectsDidChangeNotification
                  object:self.document.managedObjectContext];
} 
- (void)viewWillDisappear:(BOOL)animated{
      [center removeObserver:self  
                        name:NSManagedObjectContextObjectsDidChangeNotification
                      object:self.document.managedObjectContext]; 
      [super viewWillDisappear:animated];
}

这里监听Core Data数据库是否有变化。记住,可以由多个不同的managedObjectContext改变数据库,这样会造成混淆,如果多线程就容易解决。广播者是managedObjectContext,如果数据库中添加,删除,或者有一些更改,它就会向你广播。广播站叫NSManagedObjectContextObjectsDidChangeNotification。

contextChanged是这个样子的:

- (void)contextChanged:(NSNotification *)notification {
        The notification.userInfo object is an NSDictionary with the following keys: 
        NSInsertedObjectsKey // an array of objects which were inserted 
        NSUpdatedObjectsKey //anarrayofobjectswhoseattributeschanged 
        NSDeletedObjectsKey //anarrayofobjectswhichweredeleted
}

userInfo是一个词典,这个词典有三个键,这些键是否存在取决于NSManagedObjectContext中出现了什么变化,这些键的值是NSArray,它的内部数据类型为一个有过更改的NSManagedObject,你可以获得context中所发生的更改的完整描述。

UIManagedDocument

打开或者创建document,获取它的context,对数据库做了很多更改,怎么保存这些更改呢?UIManagedDocument是自动保存的,但不会依赖这种自动保存机制,可以用以下这个方法来保存数据:

[self.document saveToURL:self.document.fileURL
        forSaveOperation:UIDocumentSaveForOverwriting
       completionHandler:^(BOOL success) { 
     if (!success) NSLog(@“failed to save document %@”, self.document.localizedName);
}];

关闭document同样是异步的,什么时候需要关闭document呢?在完成更改后都需要关闭它,同时撤销所有指向UIManagedDocument的strong指针。如果没有strong指针指向UIManagedDocument时,它会自动关闭。

[self.document closeWithCompletionHandler:^(BOOL success) {
    if (!success) NSLog(@“failed to close document %@”, self.document.localizedName);
}];

它是异步的,得等到block执行了,它才会关闭。

可以有UIManagedDocument的多个实例指向磁盘上的同一个document吗?完全可以,但要小心,这些实例是没有关系的。

Core Data

现在从document中获得了一个NSManagedObjectContext,就可以进行插入和删除操作,可以进行查询。

通过调用NSEntityDescription中的方法来插入数据,这是一个类方法:

NSManagedObject *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo”
                                                       inManagedObjectContext:(NSManagedObjectContext *)context];

数据库中的所有对象都是由NSManagedObject表示的,NSEntityDescription insert的返回值是一个NSManagedObject *,它返回一个指向新创建对象的指针。

现在有这个对象了,需要设置它的attribute,怎么访问这些attribute呢?可以用NSKeyValueObserving协议,注意NSKeyValueObserving协议中的Observing,可以观察任何支持这个协议的对象的setting和geting这两个property,你希望观察这些property,这看起来和NSNotificationCenter很相似,可以说添加一个观察者,来观察这个对象的某个property,只要这个对象为这个property实现了这个协议。

- (id)valueForKey:(NSString *)key; 
- (void)setValue:(id)value forKey:(NSString *)key;

如果使用valueForKeyPath:/setValue:forKeyPath:方法,它就会跟踪那个relationship。key是attribute的名字,而value是所存的内容。

对UIManagedDocument做的所有修改都是在内存中进行的,直到做了save操作。

但是调用valueForKey:/setValueForKey:会使代码变得很乱,这么做没有任何的类型检查,所以通常不用这种方法。用property,但是如何给NSManagedObject添加一个property,并且它的类型是Photographer *,而不是NSManagedObject *,而且是在NSManagedObject不认识这些东西的情况下。方法是创建NSManagedObject的子类,比如创建一个名为Photo的NSManagedObject的子类来表示photo entity,它在头文件里生成的就是@property,这个@property对应着所有的attribute,在实现文件中采用的不是@synthesize,因为@synthesize是给它生成一个实例变量,但这些property并不是以实例变量存储的,它是存储在SQL数据库里的。

怎么生成NSManagedObject的子类呢?只需到xcode中的model file,选中它们,然后到Editor菜单,点击下面的Create NSManagedObject subclasses。生成后可以看到Photographer.h和.m文件,还有Photo.h和.m文件。

它创建了一个category,可以用来设置NSSet中的值。怎么往photos relationship中添加图片呢?有两种方法:一种是可以用它自动生成的add;另一种是用photos这个set,调用mutableCopy,这样就有一个mutable set了,然后往里面加东西,然后把photos设置回来就行了,通过调用这个property的setter。

在Photo.h中可以看到whoTook,它的类型是NSManagedObject *,应该是Photographer *才对。这是xcode的问题,在xcode生成代码时,它先生成Photo,然后生成Photographer。怎么修改这个错误呢?回到xcode,再生成一下就行了。

再看.m文件,很简洁,它所做的就是在所有property前面加上@dynamic,@dynamic的作用是告诉编译器我清楚我不需要对这个property进行@synthesize,请不要发出警告。如果这些子类不实现这些property,会有什么后果?这就不确定了。NSManagedObject的做法是,如果你传递一个property,它就会查找自己是否有个相同名字的属性,如果有,它就调用valueForKey:,或者setValueForKey:。如果添加一些额外的property,会出现错误。

有了Photographer.h、Photographer.m文件、Photo.h和Photo.m文件,那如何访问property呢?用“.”的方式调用就可以。

Photo *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObj...]; 
NSString *myThumbnail = photo.thumbnailURL; 
photo.thumbnailData = [FlickrFetcher urlForPhoto:photoDictionary format:FlickrPhotoFormat...]; 
photo.whoTook = ...; // a Photographer object we created or got by querying
photo.whoTook.name = @“CS193p Instructor”; // yes, multiple dots will follow relationships

如果更改schema,得重新生成子类。要是往其中加入一些代码呢,这么做就得修改Photo.m,那下次改变schema并在xcode中重新生成时,代码就没了。怎么解决这个问题呢?用一个Objective-C语言的一个新特性,叫category。

Categories

Categories可以让你在不使用子类的情况下往一个类中添加方法或者属性,语法是这样的:

@interface Photo (AddOn) 
- (UIImage *)image; 
@property (readonly) BOOL isOld; 
@end

这就是@interface,它会在Photo+AddOn.h中。不仅需要声明这些方法,还要实现它们,这里是一个.m文件可能的写法:

@implementation Photo (AddOn) 
-(UIImage*)image //imageisnotanattributeinthedatabase,butphotoURLis 
{
     NSData *imageData = [NSData dataWithContentsOfURL:self.photoURL]; 
     return [UIImage imageWithData:imageData];
} 
-(BOOL)isOld //whetherthisphotowasuploadedmorethanadayago 
{
     return [self.uploadDate timeIntervalSinceNow] < -24*60*60;
} 
@end

把它们加入到Photo类,isOld是只读的,只添加isOld的getter方法,self就是Photo。

使用category有一个很大的限制就是,它自己是不能添加实例变量的。所以在实现一个category时,内部是不能有@synthesize。

向NSManagedObject的子类,添加的最常用的category是Create:

@implementation Photo (Create) 
+ (Photo *)photoWithFlickrData:(NSDictionary *)flickrData
        inManagedObjectContext:(NSManagedObjectContext *)context
{
      Photo *photo = ...; // see if a Photo for that Flickr data is already in the database
      if (!photo) {
          photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObjectContext:context];
          // initialize the photo from the Flickr data 
          // perhaps even create other database objects (like the Photographer)
      }
      return photo;
} 
@end

要使用这个方法,只需import Photo+Create.h。

Core Data

如何在数据库上删除对象,只要调用以下方法:

[self.document.managedObjectContext deleteObject:photo];

必须要保证如果删除数据库中的某个对象时,数据要维持在一个稳定的状态。

有一个prepareForDeletion方法,而且可以在category中实现它,这个方法必须由一个NSManagedObject的子类来实现,才可以调用。在将要进行删除操作的时候,就会调用它。就是说,如果有谁调用了deletePhoto,这个过程的前期就是调用这个prepareForDeletion。

@implementation Photo (Deletion) 
- (void)prepareForDeletion 
{
     // we don’t need to set our whoTook to nil or anything here (that will happen automatically) 
     // but if Photographer had, for example, a “number of photos taken” attribute, 
     //       we might adjust it down by one here (e.g. self.whoTook.photoCount--).
} 
@end

在对象删除后,就不要保留strong指针了。

怎么查询呢?通过创建、执行NSFetchRequest对象来完成。首先要创建,然后请求NSManagedObjectContext替你执行这个fetch。

在建立NSFetchRequest时,有四点很重要:

首先,要指明想获取的那个entity;

还有,NSPredicate,这个指明你想从哪些entities中获取数据,就是查询条件;

再有,NSSortDescriptors,因为fetch会返回一个array,就是一个有序列表,所以要指明排序规则;

最后,可以控制每次查询的返回值的数量,或者每个batch有多少。

这是查找和建立一个fetch请求,大概的写法:

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photo”]; 
request.fetchBatchSize = 20; 
request.fetchLimit = 100; 
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor]; 
request.predicate = ...;

首先是指明entity,当你查询Core Data时,只返回一类entity,从数据库角度讲,只能在一个表上查询,每次只能从一个表中获取数据。NSSortDescriptor,它指明了你在执行这个查询后返回的array的排列顺序,通过以下方法来创建sortDescriptor:

NSSortDescriptor *sortDescriptor =
    [NSSortDescriptor sortDescriptorWithKey:@“thumbnailURL” 
                                  ascending:YES
                                   selector:@selector(localizedCaseInsensitiveCompare:)];

key就是排序时要参照的那个属性,ascending用来指定是升序还是降序,然后是selector,它并非一定得是Objective-C selector。排序是在数据库中进行的,也就是SQL做排序的工作,然后返回排列好的数据。fetch request的sortDescriptor不是只能有一个,可以是一个sortDescriptor的组合。

predicate用来表明你想得到什么样的对象,它看起来就像一个NSString:

NSString *serverName = @“flickr-5”; 
NSPredicate *predicate =
    [NSPredicate predicateWithFormat:@“thumbnailURL contains %@”, serverName];

还有一些例子:

@“uniqueId = %@”, [flickrInfo objectForKey:@“id”] // unique a photo in the database 
@“name contains[c] %@”, (NSString *) // matches name case insensitively 
@“viewed > %@”, (NSDate *) // viewed is a Date attribute in the data mapping 
@“whoTook.name = %@”, (NSString *) // Photo search (by photographer’s name) 
@“any photos.title contains %@”, (NSString *) // Photographer search (not a Photo search)

contain的意思就是是否有子字符串,注意这个[c],意思是区分大小写。

这还有一个例子,如果想查询所有Photographer,查询会在Photographer表上进行:

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photographer”];
... who have taken a photo in the last 24 hours ...
NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow:-24*60*60]; 
request.predicate = [NSPredicate predicateWithFormat:@“any photos.uploadDate > %@”, yesterday]; 
... sorted by the Photographer’s name ... 
NSSortDescriptor *sortByName = [NSSortDescriptor sortDescriptorWithKey:@“name” ascending:YES]; 
request.sortDescriptors = [NSArray arrayWithObject:sortByName];

这个请求建好了,接下来是如何执行这个查询?我向managedObjectContext发送一个消息,这个managedObjectContext是从document中获取的,消息的名字叫executeFetchRequest,发送请求的时候,还跟了一个error指针,这样也能接收到error消息。

NSManagedObjectContext *moc = self.document.managedObjectContext; 
NSError *error; 
NSArray *photographers = [moc executeFetchRequest:request error:&error];

如果返回值是nil,表示出错了,要查看一下这个error。如果返回的array是空的,是指没有查询到符合条件的对象。

所有的数据并不会一次返回,它会有选择的存储你想要的对象。

posted on 2013-03-14 21:13  写下一生的程序  阅读(18964)  评论(1编辑  收藏  举报