這節課的主要內容是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是空的,是指沒有查詢到符合條件的對象。
所有的數據並不會一次返回,它會有選擇的存儲你想要的對象。