iPhone開發(fā)應用之Archiving NSCoder教程
iPhone開發(fā)應用之Archiving NSCoder教程是本文要介紹的內容,一個面向對象程序在運行的時候,一般都創(chuàng)建了一個復雜的對象關系圖,經常需要把這樣一個復雜的對象關系圖表示成字節(jié)流.這樣的過程我們叫做Archiving 如圖10.1,
這個字節(jié)流可以在網絡中傳送,也可以寫入到文件中. 例如,我們創(chuàng)建保存一個nib文件,Interface Builder把對象寫入到nib文件就是這樣的arching過程(對于Java,這個過程叫serialization)。
而當從字節(jié)流中重新恢復對象關系圖的過程叫做unarchive. 例如,當程序啟動是,將會從nib文件中unarchive對象雖然對象包含成員變量和方法.但是只有成員變量和類名會被archive. 換句話說,data會被archive,而code不會. 所以,如果程序A archive對象,而程序B unarchive對象.那么程序A和B都要保證包含了class所連接的code. 舉個例子,在nib文件中,你使用到了Appkit framework 的NSWindow和NSButton對象.那么如果我們的程序沒有連接Appkit framework,那么我們就沒有辦法生成NSWindow和NSButton對象,因為archive中只包含了data,而沒有code
有一個洗發(fā)水的廣告是這樣說得:"我告訴了我的兩個朋友,而他們各自又告訴了自己的兩個朋友,這樣一傳十,十傳百.."寓意就是,你告訴了你的朋友,最后所有的人都開始使用這個洗發(fā)水了. 對象archiving的工作方式和這差不多. 你archiving一個root對象. 它archiving自己相關聯的對象,那些相關聯的對象也會archiving自己相關聯的對象,依次類推,所有相關的對象都被archiving了
archiving由2步來完成. 1,我們需要告知我們的對象要怎么樣來archive. 2. 我們需要激發(fā)archiving動作發(fā)生
Objective-C語言有一個機制叫protocol, 就像java中的interface一樣. 一個protocol聲明了一系列方法.但你的類實現一個protocol,那么就預定了,你的類需要實現protocol中聲明的所有方法
NSCoder 和NSCoding
NSCoding是一個protocol. 如果你的類實現了NSCoding.那么就要實現這些方法
- - (id)initWithCoder:(NSCoder *)coder;
- - (void)encodeWithCoder:(NSCoder *)coder;
NSCoder是archivie 字節(jié)流的抽象類.我們可以實現把數據寫入一個coder,也可以從coder中讀取我們寫入的數據. 我們對象的方法initWithCoder:就是從一個coder從讀取數據,然后把數據賦給成員變量. 方法encodeWithCoder: 則是把成員變量的值寫入到coder中. 在這一章中,我們會在Person類中實現這兩個方法
NSCoder是一個抽象類,我們不會直接使用它來創(chuàng)建對象. 相反,我們會使用從它繼承來的子類. 也就是我們使用 NSKeyedUnarchiver類來從字節(jié)流中讀取數據,而使用NSKeyedArchiver類來把對象寫入到字節(jié)流
Encoding
NSCoder包含了很多方法, 不過大部分人會發(fā)現只會使用到其中很少的一部分. 下面是當要archivie數據時用到的一些常用方法
- - (void)encodeObject:(id)anObject forKey:(NSString *)aKey
這個方法把anObject對象寫入到coder中,并把它和aKey關聯起來[下次使用aKey從coder中可以再把anObject讀取出來] 這會是anObject的方法encodeWithCodr得到調用(還記得上面那個洗發(fā)水廣告把.就是這樣傳下去的)
對于C的基本類型(如int float).NSCoder使用下面方法
- - (void)encodeBool:(BOOL)boolv forKey:(NSString *)key
- - (void)encodeDouble:(double)realv forKey:(NSString *)key
- - (void)encodeFloat:(float)realv forKey:(NSString *)key
- - (void)encodeInt:(int)intv forKey:(NSString *)key
添加encoing方法到Person類中.
- - (void)encodeWithCoder:(NSCoder *)coder
- {
- [super encodeWithCoder:coder];
- [coder encodeObject:personName forKey:@"personName"];
- [coder encodeFloat:expectedRaise forKey:@"expectedRaise"];
- }
這里調用了父類的encodeWithCoder,使得父類有機會把自己的變量寫入到coder中. 因此,類繼承樹中的類只會把自己的成員變量寫入到coder-不會包含父類的成員變量
Decoding
從coder中decoding數據,我們使用這些方法
- - (id)decodeObjectForKey:(NSString *)aKey
- - (BOOL)decodeBoolForKey:(NSString *)key
- - (double)decodeDoubleForKey:(NSString *)key
- - (float)decodeFloatForKey:(NSString *)key
- - (int)decodeIntForKey:(NSString *)key
如果因為某些原因, 字節(jié)流中沒有和aKey關聯的數據,那么我們會得到0值. 例如,對象沒有把key foo 關聯一個float數據寫入coder,那么在使用foo key來讀取這個float數據,coder會返回0.0 . 如果key foo關聯的是一個對象數據[使用方法encodeWithCoder 寫入],那么讀取時coder返回nil
添加decoding到Person類中
- - (id)initWithCoder:(NSCoder *)coder
- {
- [super init];
- personName = [[coder decodeObjectForKey:@"personName"] retain];
- expectedRaise = [coder decodeFloatForKey:@"expectedRaise"];
- return self;
- }
我們沒有調用父類的initWithCoder, 那是因為NSObject沒有實現它. 如過Person類的父類實現了NSCoding協議,那么這個方法應該這樣寫
- - (id)initWithCoder:(NSCoder *)coder
- {
- [super initWithCoder:coder];
- personName = [[coder decodeObjectForKey:@"personName"] retain];
- expectedRaise = [coder decodeFloatForKey:@"expectedRaise"];
- return self;
- }
你可以會說"在第3章中, designated initializer會完成所有的init工作然后在調用父類的 designated initializer, 也就是說類的其他initializer 方法都會調用designated initializer,Person類有designated initializer- init. 可以這個新加入的initializer方法并沒有調用init方法阿?" 不錯, 你是對的, initWithCoer: 是這個規(guī)則的一個特例.
好了.我們實現了NSCoding協議的方法.現在讓Person類實現NSCoding protocol. 我們來編輯Person.h文件.
- @interface Person : NSObject <NSCoding> {
現在編譯我們的工程. 你也可以運行程序看看.雖然Person類可以encode自己了.不過我們沒有地方讓它這么做.所以程序看上去沒什么變化.
#p#
Document Architecture
多文檔程序有很多的共同性. 比如都可以創(chuàng)建新的document, 打開document,保存或打印打開的document, 當關閉document窗口或退出程序時提醒用戶保存編輯好得document. Apple提供3個類- NSDocumentController,NSDocument,NSWindowController-來完成這些工作. 它們一起組成了document architecture
創(chuàng)建document architecture的意圖是和我們第8章討論的Model-View-Controller設計模式相關的. 在RaiseMan工程中. 我們的NSDocument子類-使用了NSArrayController類-就是其中的Controller. 它包含了指向model對象的指針. 負責下面所列的職責 [這里的model 數據就是值employyess-person 對象]
將model 數據保存為一個文件
從一個文件中加載model數據
在view中顯示model數據
響應用戶通過view的輸入,并更新model
Info.plist 和 NSDocumentController
XCode在編譯創(chuàng)建一個程序時會使用到一個文件 Info.plist(本章后面,我們會修改這個文件). 當程序啟動時,它會讀取Info.plisst的信息. 告知工作的文件類型是什么. 如果它發(fā)現是一個document-base 程序. 那么會創(chuàng)建一個NSDocumentController對象(圖10.2). 我們很少去直接使用這個document controller. NSDocumentController對象在后面會為我們做一些工作.例如,當選擇New 或是 Save All菜單時, document controller會處理這些請求. 如果你有給document controller發(fā)送消息,你可以這樣做
- NSDocumentController *dc;
- dc = [NSDocumentController sharedDocumentController];
document controller保存了一個document 對象的array - 每一個document對象就是一個打開的document.
NSDocument
document對象是NSDocument子類的一個實例. 在我們的RaiseMan程序中,它就是MyDocument的實例. 對于大部分程序,一般我們只有簡單的擴展NSDocument來完成想要的功能而不需要過多關系NSDocumentcontroller或是NSWindowController
saving
菜單項Save,Save As...,Save All,和Close雖然不相同.但是它們都面向同一個問題:把mdoel保存為一個文件或是文件包(文件包是一個文件目錄,不過對于用戶就象是一個文件一樣). 對于這些菜單項. 我們的NSDocument子類需要實現下面3個方法中的一個
- - (NSData *)dataOfType:(NSString *)aType
- error:(NSError *)e
你的document對象將model生成一個NSData寫入文件.[這個方法中,我們只有把model壓成一個NSData返回,然后Cocoa會把NSData在寫入文件了] NSData就是字節(jié)buffer. 是簡單也是通用的實現saving的方法.如果不能生成一個NSData 對象,那么就返回nil,而用戶會得到一個alert提示save失敗. 注意到參數aType, 它可以容許你將document保存為一個或多個類型格式. 例如,你編寫了一個圖像程序,你可能容許用戶將圖像保存為gif或是jpg格式.所以當你生成data對象時, aType就指定了用戶請求保存的格式.如果你的程序只處理單一類型,那么可以忽略aType. 為了說明你不能保存,可以返回nil并創(chuàng)建一個NSError對象來說明出來什么樣得錯誤
- - (NSFileWrapper *)fileWrapperOfType:(NSString *)aType
- error:(NSError *)e
你的document對象生成一個文件包返回. 文件包將被創(chuàng)建在用戶指定的位置
- - (BOOL)writeToURL:(NSURL *)absoluteURL
- ofType:(NSString *)typeName
- error:(NSError **)outError;
你的docuemnt對象以指定的type把model數據保存在指定的URL(URL就是文件系統上的文件路徑)[這個方法應該在NSDocument類中實現了,里面估計就是調用了dataOfType:error: . 得到NSData后將其寫入指定URL. 當然你也可以從中這個方法] 如果能夠保存成功返回YES,否則返回NO. 如果返回NO,那么你你應該生成一個NSError對象來描述錯誤是什么
來解釋下NSError.它的觀念是,因為某些原因,某個方法沒有辦法完成這個功能.那么它就會生成一個NSError對象,并把NSError對象的指針放到指定的位置. 例如,如果我希望從一個文件中讀取到一個NSData,那么我會提供一個地址,當出錯時,我可以從這個地址中得到錯誤信息
NSError *e;
- NSData *d = [NSData dataWithContentsOfFile:@"/tmp/x.txt"
- options:0
- error:&error];
- // Did the read fail?
- if (d == nil) {
- NSLog(@"Read failed: %@", [error localizedDescription];
- }
所以NSData類即會返回一個data對象,同時可能會創(chuàng)建一個error對象
在save和load方法中,我們將有負責在失敗的時候創(chuàng)建NSError對象
Loading
Open...,Open Recent,和Revert To Saved 菜單項也是一樣,它們都面向同一個問題:從一個文件或是文件包中得到model. 為了響應它們,NSDocuement子類需要實現下面3個方法中的一個
- - (BOOL)readFromData:(NSData *)data
- ofType:(NSString *)typeName
- error:(NSError **)outError
包含了用戶要打開的文件內容的NSData對象被傳進來. 如果能夠從這個NSData對象中生成model那么就返回YES. 如果返回NO,那么用戶會得到一個Alert提示為什么個不能成功打開文件. Alert的內容由這個方法生成的NSError對象來指定
- - (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper
- ofType:(NSString *)typeName
- error:(NSError **)outError;
從一個NSFileWrapper對象讀取model數據
- - (BOOL)readFromURL:(NSURL *)absoluteURL
- ofType:(NSString *)typeName
- error:(NSError **)outError;
#p#
從指定文件中讀取model數據
在實現了一個save和一個load方法后,我們的程序就知道怎么樣讀寫文件了.在打開一個文件時, document對象會在讀取nib文件前讀取document文件[你需要讀取nib文件來顯示一個document阿] . 這樣的結果是,我們不能在loading一個document文件后馬上去給用戶界面發(fā)送消息(它們還不存在)[注意load nib文件是document 架構為我們做的,這里說的立馬調用是指在 load方法中-這個是我們在NSDocument子類中實現的調用-給UI發(fā)送消息]. [那如果我們想要馬上給UI發(fā)送消息怎么辦?]-為了解決這個問題,我們可以實現一個方法-它會在nib文件被調用UI創(chuàng)建好了后發(fā)送
- - (void)windowControllerDidLoadNib:(NSWindowController *)x;
[想想 當點擊Open菜單,代碼執(zhí)行的過程是怎么樣 - 有些代碼是cocoa里面實現的,有些是我們自己實現的]
在我們的NSDocuemnt 子類中,實現這個方法刷新UI
NSWindowController
在document architecture中最后要介紹的一個類是NSWindowcontroller .每打開一個Document都會產生一個窗口-生成一個NSWindowController實例. 對于大部分程序,每一個document對于一個window, window controller的默認實現已經夠用了.所以一般我們只有在下面幾種情況下才會生成一個NSWindowController的子類
對于同一個document,需要使用多個window. 例如,CAD程序, 你可能需要一個text窗口來描述一個立體,而另外一個窗口來顯示這個立體
你需要把UI controller 和 model controller 放到不同的類中
你需要創(chuàng)建不和NSDocument 對象對應的窗口.我們會在12章來做這樣的事
Saving 和 NSKeyedArchiver
現在我們知道了怎樣encode和decode我們自己的類,現在開始給我們的程序添加saving和loading功能了. 當我們要保存person到一個文件,MyDocument類會被請求生成一個NSData實例. 一旦創(chuàng)建了NSData實例并返回,它會自動保存到文件中
為了生成一個NSData實例[encode了model數據] , 我們使用NSKeyedArchiver類. 它有這樣一個方法
- + (NSData *)archivedDataWithRootObject:(id)rootObject
這個方法將對象archive成NSData對象的字節(jié)buffer [字節(jié)buffe-看看NSData的說明吧]
再一次回到那個廣告"我告訴了兩朋友,他們也告訴了自己的朋友...."當你encode一個對象是, 這個對象會encode它自己連接的對象,那些對象也會encode它們連接的對象..等等. 這里我們要encode那個對象呢?就是array employees了. 它又會encode所有包含的Person對象. 而我們在Peron類中實現了encodeWithCoder:,所以每個Perosn對象開始encode自己了-encode personName字串和expectedRaise float
編輯方法dataOfType:error:. 添加saving功能
- - (NSData *)dataOfType:(NSString *)aType
- error:(NSError **)outError
- {
- // End editing
- [[tableView window] endEditingFor:nil];
- // Create an NSData object from the employees array
- return [NSKeyedArchiver archivedDataWithRootObject:employees];
- }
這里我們忽略了error參數.將沒有error產生
- Loading和NSKeyedUnarchiver
現在開始添加load文件功能, 再一次說明,NSDocument已經大部分細節(jié)
我們會使用到NSKeyedUnarchiver類方法
- + (id)unarchiveObjectWithData:(NSData *)data
編輯MyDocument類的readFromData:ofType:error:方法
- ofType:(NSString *)typeName
- error:(NSError **)outError
- {
- NSLog(@"About to read data of type %@", typeName);
- NSMutableArray *newArray = nil;
- @try {
- newArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
- }
- @catch (NSException *e) {
- if (outError) {
- NSDictionary *d = [NSDictionary
- dictionaryWithObject:@"The data is corrupted."
- forKey:NSLocalizedFailureReasonErrorKey];
- *outError = [NSError errorWithDomain:NSOSStatusErrorDomain
- code:unimpErr
- userInfo:d];
- }
- return NO;
- }
- [self setEmployees:newArray];
- return YES;
- }
在nib文件加載后,你需要刷新UI.不過NSArrayController為你完成了這個功能.我們不需要在windowControllerDidLoadNib:方法中多做什么. 我們在13章將會修改這個方法
- - (void)windowControllerDidLoadNib:(NSWindowController *)aController
- {
- [super windowControllerDidLoadNib:aController];
- }
注意,在打開或創(chuàng)建一個document時,會詢問我們的document類:需要load那個nib文件.現在我們也不需要修改這個方法
- - (NSString *)windowNibName
- {
- return @"MyDocument";
- }
因為我們激活了undo 機制,所以在編輯了document后, window會自動標注為編輯過.
現在,我們的程序能夠讀寫文件了.編譯運行程序,試試看吧,看上去都能工作正常. 不過我們保存的文件的后綴名為.???? ,我們需要在Info.plist中給它定義一個后綴名
#p#
設置后綴名和圖標
我們將為RaiseMan 文件添加后綴.rsmn 和一個圖標. 首先找到一個.icns文件并拷貝到我們的工程中. 就使用
- /Developer/Examples/Appkit/CompositeLab/BBall.icns
吧.把他從Finder中拖到XCode的Resources組中.如圖10.3
XCode會彈出一個頁面,確保勾選Copy items into destination group's folder 如圖10.4.這樣將會包icon文件拷貝到我們的工程目錄中
在XCode中選定RaiseMan Target, 從File菜單中選擇Get Info, 來設置document-type屬性. 在Properties頁中,設置identifier為com.bignerdranch.RaiseMan. 設置Icon file 為BBall. 在document-types中,設置name為RaiseMan Doc. Extensions為rsmn. icon file為BBall.參考圖10.5
編譯運行程序.我們再次試試保存和打開功能. 在Finder中, 我們的.rsmn文件的圖標變成了BBall.icns
一個程序其實是一個目錄. 包含了程序用到的nib 文件, 圖像,聲音和可執(zhí)行代碼. 在Terminal,試試輸入
- cd /Applications/TextEdit.app/Contents
- ls
可以看到3個有趣的東西
Info.plist文件. 包含了該程序的信息, 文件類型和相關的圖標. Finder會使用這些信息
MacOS/目錄. 這里包含了可執(zhí)行代碼
Resources/目錄. 這里包含了程序用到的圖像,聲音和nib文件,你還可以看到不同語言的本地化資源
#p#
思考:避免死循環(huán)
聰明的讀者可能會懷疑:""如果對象A使對象B進行encode,對象B使對象C進行encode,而對象C又使得對象A進行encode. 這樣不是會產生無窮循環(huán)嗎?"" 沒錯,確實會發(fā)生這種情況,好在NSKeyedArchiver類設計好了避免這種情況發(fā)送.
當encode一個對象的時候,會將一個唯一標識同時放到流中.并建立一個表,一旦archive對象,就會把該對象和它的唯一標識聯系起來. 如果下次又要encode同一個對象,NSKeyedArchiver會先瀏覽這個表,看是否已經encode過,并只會把唯一標識放置到流中.
當從流中decode出對象時, NSKeyedUnarchiver同樣會生成一個表,把encode對象和唯一標識關聯起來.如果發(fā)現流中只有唯一標識[說明之前有encode這個對象],unarchiver就會在表中來查找這個對象,而不是再生成一個新的對象.
NSCoder有一個方法容易使讀者和上面的思想產生混淆
- - (void)encodeConditionalObject:(id)anObject forKey:(NSString *)aKey
當對象A有一個指針指向對象B, 但是對象A不需要知道對象B是否被archive[是否存在]. 不過如果另外一個對象已經archive了B,對象A又希望將對象B的唯一標識在encode的時候能夠放置到流中. [也就是說對象A不會主動encode B, 如果存在對象B ,那么就指向它,否則就指向空]
舉個例子,我們需要給Engine對象編寫它的encodeWithCoder:方法. 它有一個成員變量為car,是一個指向Car對象的指針(發(fā)動機是汽車的一部分). 我們在archiving Engine對象時,不希望整個Car對象被archived. 不過如果該Car對象之前在其他地方archived過, 我們又希望 Engine對象的car指針指向它. 在這種情況下,我們就要要求Engine對象有條件的來encode car指針指向的對象了. 如圖10.6
思考: 創(chuàng)建Protocol
創(chuàng)建自己的Protocol非常簡單.下面的Protocol有兩個方法,它可能在Foo.h文件中
- @protocol Foo
- - (void)fido:(int)x;
- - (float)rex;
- @end
在Objective-C 2.0中,新增了語法@optional. 可以用來指定那些方法是必須那些方法是可選的
- @protocol Foo
- - (void)fido:(int)x;
- - (float)rex;
- @optional
- - (int)rover;
- - (void)spot:(int)x;
- @end
在這個例子中fido: 和rex方法是必須的,而rover和spot:方法是可選的
如果你有一個類要實現Foo protocol和NSCoding protocol. 應該這樣做
- #import "Spunky.h"
- #import "Foo.h"
- @interface ZsaZsa:Spunky <Foo, NSCoding>
- ...etc...
- @end
我們不需要重新聲明父類和protocol中聲明過的方法.所以,在本例中, ZsaZsa類接口文件中不需要再次聲明Spunky和Foo,NSCoding中聲明過的方法
通用類型描述[UTI]
在使用計算機時,一直有這樣一個問題:"數據是怎么樣展現出來的". 對于Mac, 這個問題在不同的幾個地方都會遇到:當從Finder打開一個文件時.當通過剪貼板拷貝數據時,當通過Spotlight索引文件時,當使用Quicklook預覽文件時.這個問題有一些答案: 文件擴展名, creator codes,和MIME類型
Apple選擇的長期解決途徑是通用類型描述(UTIs). 一個UTI是一個描述了文件類型的字符串. UTIs按一定層次關系組織.
我們在Info.plist文件中定義程序可以讀寫的UTIs-包括新建的和自定義的UTIs. Info.plist文件是XML格式,包含了目錄以及key-value. 可以使用一個新key UTExporterTypeDeclarations來export新的UTIs. 例如,如果你想給RaiseMain Document添加一個UTI. 可以在Info.plist文件中添加如下描述:
- <array>
- <dict>
- <key>UTTypeIdentifier</key>
- <string>com.bignerdranch.raiseman-doc</string>
- <key>UTTypeDescription</key>
- <string>RaiseMan Document</string>
- <key>UTTypeConformsTo</key>
- <array>
- <string>public.data</string>
- </array>
- <key>UTTypeTagSpecification</key>
- <dict>
- <key>com.apple.ostype</key>
- <string>rsmn</string>
- <key>public.filename-extension</key>
- <array>
- <string>rsmn</string>
- </array>
- </dict>
- </dict>
- </array>
當然,我們也通過properties inspector來使用UTI.如圖10.7
你可以在Apple的文檔中找到所有的系統定義的UTIs
小結:iPhone開發(fā)應用之Archiving NSCoder教程的內容介紹完了,希望本文對你有所幫助!