第十七章、文件加載與保存

文件加載與保存

  • 大多數(shù)計算機程序(應(yīng)用程序)在關(guān)閉時都會為用戶的當前成果創(chuàng)建一個臨時的(非永久的)文件,可能是小說的某個章節(jié),或是某個樂隊專輯的封面。但無論是那種情形,用戶都會有個文件保存在磁盤上。
  • 標準C函數(shù)庫提供了函數(shù)調(diào)用來創(chuàng)建,讀取和寫入文件,例如open(),read(),write(),fopen()和fread()。而Cocoa提供了Core Data,它能在后臺進行文件的以上所有操作。
  • Cocoa提供了兩個處理文件的通用類:屬性對象和對象編碼。

1.屬性列表

在Cocoa中,有一類名為屬性列表(property list)的對象,通常簡寫為plist。這些屬性列表類是NSArray,NSDictionary,NSString,NSNumber,NSDate和NSData(前面四個已經(jīng)講過),以及它們的可修改形態(tài)(只要它們能擁有前綴為Mutable的類)

1.1NSDate

  • 程序中經(jīng)常要處理時間和日期。NSDate類是Cocoa中用于處理日期和時間的基礎(chǔ)(Foundation)類。
  • 可以使用[NSDate data]來獲取當前的日期和時間,它會返回一個能自動釋放的對象。
NSDate *date = [NSDate date];
NSLog(@"today is %@",date);
//輸出結(jié)果
today is 2018-12-01 20:21:02 -0400
  • 你可以使用一些方法比較兩個日期,從而對列表進行排序,還可以獲取與當前時間間隔一定時差的日期。
//獲取24小時之前的確切時間
NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow:-(24*60*60)];
NSLog(@"yesterday is %@",yesterday);
//輸出結(jié)果
yesterday is 2018-11-30 20:21:02 -0400
  • +dateWithTimeIntervalSinceNow:接受一個NSTimeInterval參數(shù),該參數(shù)是一個雙精度值,表示以妙為單位計算的時間間隔。通過該參數(shù)可以指定時間偏移的方式:對于將來的時間,使用時間間隔的整數(shù);對于過去的時間,使用時間間隔的負數(shù)。
  • 如果你想要設(shè)定輸出結(jié)果的時間格式,蘋果公司提供了一個叫做NSDateFormatter的類,它符合35號Unicode技術(shù)標準,能為用戶提供多種時間的顯示格式。

1.2NSData

  • 將緩沖區(qū)(buffer)的數(shù)據(jù)傳遞給函數(shù)是C語言中常見的操作。通常是將緩沖區(qū)的指針和長度傳遞給某個函數(shù)。另外,C語言中可能會出現(xiàn)內(nèi)存管理問題。例如,如果緩沖區(qū)已經(jīng)被動態(tài)分配,那么當它不在使用時,由誰負責將其清除?
  • Cocoa提供了NSData類,該類可以包含大量字節(jié)。你可以獲取數(shù)據(jù)的長度和指向字節(jié)和起始位置的指針。因為NSData是一個對象,所以常規(guī)的內(nèi)存管理對它是有效的。因此,如果想將數(shù)據(jù)塊傳遞給一個函數(shù)或方法,可以通過傳遞一個支持自動釋放的NSData來實現(xiàn),而無需擔心內(nèi)存清理的問題。下面的NSData對象將保存一個普通的C字符串(一個字節(jié)序列),然后輸出數(shù)據(jù)。
const char *string = "Hi there,this is a C string!";
NSData *data = [NSData dataWithBytes: length: strlen(string) + 1];
NSLog(@"data is %@",data);
//運行結(jié)果
data is <48692074 68657265 2c207468 69732069 73206120 43207374 72696e67 2100>
  • 上面的輸出有點特別,但是如果你有ASCII表(打開終端,并鍵入命令man ascii就可以找到該表),就可以看到,這個十六進制數(shù)據(jù)塊就是我們的字符串,0×48表示字符H,0×69表示字符i等。-length方法給出字節(jié)數(shù)量,-byte方法給出指向字符串起始位置的指針。注意到+dataWiithBytes:調(diào)用中的+1了嗎?它用于C語言字符串所需的尾部的零字節(jié)。還需注意NSlog輸出結(jié)果末尾的00。通過包含零字節(jié),就可以使用%s格式的說明符輸出字符串。
NSLog(@"%d byte string is "%s",[data length],[data bytes]);
//輸出結(jié)果
30 byte is "Hi there,this is a C string!"
  • NSData對象是不可變更的,創(chuàng)建后就不可以改變。你可以使用它們,但不能更改其中的內(nèi)容。不過NSMutableData支持在數(shù)據(jù)內(nèi)容中添加和刪除字節(jié)。

1.3寫入和讀取屬性列表

  • 集合屬性列表類(NSArray和NSDictionary)具有一個-writeToFile:atomically:方法,用于將屬性列表的內(nèi)容寫入文件。
  • NSString和NSData也具有-writeToFile:atomically:方法,不過只能寫出字符串或者數(shù)據(jù)塊。因此,我們可以將字符串存入一個數(shù)組,然后保存它。

相關(guān)例子代碼:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    NSArray *phrase;
    phrase = [NSArray arrayWithObjects:@"I",@"seem",@"to",@"be",@"a",@"verb",nil];
    [phrase writeToFile:@"/Users/xulei/Desktop/file.txt" atomically:YES];
    return 0;
}

運行結(jié)果:

  • 以上代碼雖然有些繁瑣,但正是我們要保存的內(nèi)容:一個字符串數(shù)組。如果保存的數(shù)組是包含了各種字符串數(shù)組,數(shù)字和數(shù)據(jù)的字典,那么這些屬性列表文件的內(nèi)容將相當復雜。Xcode也包含了一個屬性列表編輯器,所以你可以查看plist文件并進行編輯。

  • 有些屬性列表文件,特別是首選項文件,是以壓縮的二進制格式存儲的。通過使用plutil命令:plutil -convert xml1文件名.plist,可以將這些文件轉(zhuǎn)化成人可以理解的字面形式。

  • 現(xiàn)在我們的磁盤中已經(jīng)有了verbiage.txt文件,可以使用+arrayWithContentsOfFile:方法讀取該文件。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    NSArray *phrase1;
    phrase1 = [NSArray arrayWithObjects:@"I",@"seem",@"to",@"be",@"a",@"verb",nil];
    [phrase1 writeToFile:@"/Users/xulei/Desktop/file.txt" atomically:YES];
    NSArray *phrase2 = [NSArray arrayWithContentsOfFile:@"/Users/xulei/Desktop/file.txt"];
    NSLog(@"%@",phrase2);
    return 0;
}
//運行結(jié)果
2018-12-18 12:47:02.350581-0800 OC文件[632:19018] (
    I,
    seem,
    to,
    be,
    a,
    verb
)
Program ended with exit code: 0
  • 注意到writeToFile:方法中的atomically了嗎?atomically:參數(shù)的值為BOOL類型,它會告訴Cocoa是否應(yīng)該首先將文件內(nèi)容保存在臨時文件中,再將該臨時文件和原始文件交換。
  • 這是一種安全機制:如果在保存過程中出現(xiàn)意外,不會破壞原始文件。但這種安全機制需要付出一定的代價:在保存過程中,由于原始文件仍然保存在磁盤中,所以需要使用雙倍的磁盤空間。你應(yīng)該盡量使用atomically的方式保存文件,除非保存的文件容量非常大,會占用用戶大量的硬盤空間。
  • 如果能將數(shù)據(jù)歸結(jié)為屬性列表類型,則可以使用這些非常便捷的方法調(diào)用來將內(nèi)容保存到磁盤中,供以后讀取。
  • 這些函數(shù)的一個缺點是不會返回任何錯誤信息。如果無法加載文件,你只能從方法中獲得一個nil指針,但無法得知出現(xiàn)錯誤的具體原因。

1.4修改對象類型

  • 需要注意,當使用集合類從某文件讀取數(shù)據(jù)時,你無法修改數(shù)據(jù)的類型。一種解決方法是強制轉(zhuǎn)換,遍歷plist文件的內(nèi)容并創(chuàng)建一個平行結(jié)構(gòu)的可修改對象。不過還有一種方法,需要用到類NSPropertyListSerialization,正如名字所提示的,它可以為存儲和加載屬性列表的行為添加很多你需要的設(shè)定項。
  • 尤其要注意propertyListFromData:mutabilityOption:format:errorDescription:方法。它能把plist數(shù)據(jù)返回給你,并且能在出現(xiàn)異常的時候提供錯誤信息。
  • 以下是將plist數(shù)據(jù)內(nèi)容以二進制形式寫入文件的代碼:
NSString *error = nil;
    NSData *encodedArray = [NSPropertyListSerialization dataFromPropertyList:capitols format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
    [encodedArray writeToFile:@"/tmp/capitols.txt" atomically:YES];
  • 如你所見,我們將數(shù)組數(shù)據(jù)轉(zhuǎn)化成了NSData類型并寫入了文件中。
  • 將數(shù)據(jù)讀取回內(nèi)存要多執(zhí)行一步,即指定文件的類型。我們創(chuàng)建一個指針,如果文件格式與指定的類型不同,可以換用原格式類型的指針,也可以將讀取的內(nèi)容轉(zhuǎn)化成信的格式。
 NSPropertyListFormat propertyListFormat = NSPropertyListXMLFormat_v1_0;
    NSString *error = nil;
    NSMutableArray *capitols = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:&propertyListFormat error:&error];
  • 其中一個選項就是以什么方式來讀取數(shù)據(jù):我們是否想要能夠修改plist文件的類型?我們是想獲取列表結(jié)構(gòu)還是僅獲取二進制數(shù)據(jù)?
  • 以下是將屬性列表文件plist數(shù)據(jù)內(nèi)容以二進制形式寫入文件的代碼:
void writeCapitols (void)

{

    NSMutableArray *capitols = [NSMutableArrayarrayWithCapacity:10];

    NSMutableDictionary *capitol = [NSMutableDictionarydictionaryWithObject:@"Canada"forKey:@"country"];

    [capitol setObject:@"Ottawa"forKey:@"capitol"];

    [capitols addObject:capitol];

    capitol = [NSMutableDictionarydictionaryWithObject:@"Norway"forKey:@"country"];

    [capitol setObject:@"Oslo"forKey:@"capitol"];

    [capitols addObject:capitol];

    

    NSString *error =nil;

    NSData *encodedArray = [NSPropertyListSerializationdataFromPropertyList:capitols 

                                                                     format:NSPropertyListBinaryFormat_v1_0 

                                                           errorDescription:&error];

    [encodedArray writeToFile:@"/tmp/capitols.txt"atomically:YES];

}
  • 如你所見,我們將數(shù)組數(shù)據(jù)轉(zhuǎn)換成了NSData類型并寫入了文件中。

  • 將數(shù)據(jù)讀取回內(nèi)存要多執(zhí)行一步,即指定文件的類型。我們創(chuàng)建了一個指針,如果文件格式與指定的類型不同,可以換用原格式類型的指針,也可以將讀取的內(nèi)容轉(zhuǎn)換成新的格式。

static void modifyCapitols(void)

{

    NSData *data = [NSDatadataWithContentsOfFile:@"/tmp/capitols.txt"];

    NSPropertyListFormat propertyListFormat =NSPropertyListXMLFormat_v1_0;

    NSString *error =nil;

    NSMutableArray *capitols = [NSPropertyListSerializationpropertyListFromData:data

                                                               mutabilityOption:NSPropertyListMutableContainersAndLeaves

                                                                         format:&propertyListFormat

                                                               errorDescription:&error];

    NSLog(@"capitols %@", capitols);

}
  • 在main函數(shù)中,我們調(diào)用writeCapitols();和modifyCapitols();的輸出結(jié)果如下:
capitols (

        {

        capitol = Ottawa;

        country = Canada;

    },

        {

        capitol = Oslo;

        country = Norway;

    }

)

  • 可以使用[NSDate date]來獲取當前的日期和時間,它會返回一個能自動釋放的對象。
NSDate *date = [NSDatedate];

NSLog (@"today is %@", date);

將輸出的結(jié)果為:
today is 2013-12-4 19:58:06 -0400。

  • 有些屬性列表文件,特別是首選項文件,是以壓縮的二進制格式存儲的。通過使用plutil命令:plutil - convert xml1文件名.plist,可以將這些文件轉(zhuǎn)換成人可以理解的字面形式。

2.編碼對象

  • 不幸的是,你無法總是將對象信息表示為屬性列表類。
  • Cocoa具備一種將對象轉(zhuǎn)換成某種格式并保存到磁盤中的機制。對象可以將它們的實例變量和其他數(shù)據(jù)編碼為數(shù)據(jù)塊,然后保存到磁盤中。這些數(shù)據(jù)塊以后還可以讀回內(nèi)存中,并且還能基于保存的數(shù)據(jù)創(chuàng)建新對象。這個過程被稱為編碼與解碼(encoding and decoding),也可以叫做序列化與反序列化(serialization and deserialization)。
  • 前面說Interface Builder時,我們從庫中將對象拖到窗口,這些對象會被存儲到nib文件中。換句話說,NSWindow和NSTextField對象都被序列化并保存到磁盤中。當程序運行時,會將nib文件加載到內(nèi)存中,對象會被反序列化,新的NSWindow與NSTextField對象會被創(chuàng)建并建立關(guān)系。
  • 通過采用NSCoding協(xié)議,可以使自己的對象實現(xiàn)相同的功能。該協(xié)議與下面的代碼類似。
@protocol NSCoding
- (void) encodeWithCoder:(NSCoder *) encoder;
- (id) initWithCoder:(NSCoder *) decoder;
@end
  • 通過采用該協(xié)議,可以實現(xiàn)兩種方法。當對象需要保存自身時,就會調(diào)用-encodeWithCoder:方法;當對象需要加載自身時,就會調(diào)用-initWithCoder:方法。
  • NSCoder是一個抽象類,它定義了一些有用的方法,便于對象與NSData之間的轉(zhuǎn)換,你完全沒必要創(chuàng)建一個新的NSCoder對象,因為實際上并不會起作用。不過有一些具體實現(xiàn)的NSCoder子類可以用來編碼和解碼對象,我們使用其中的兩個子類NSKeyedArchiver和NSKeyedUnarchiver。
#import <Foundation/Foundation.h>

//包含一些實例變量的簡單類
@interface Thingie : NSObject <NSCoding>{
    NSString *name;
    int magicNumber;
    float shoeSize;
    NSMutableArray *subThingies;
}
@property (copy) NSString *name;
@property  int magicNumber;
@property  float shoeSize;
@property (retain) NSMutableArray *subThingies;
-(id)initWithName:(NSString *) n magicNumber:(int) mn shoeSize:(float) ss;
@end

//Thingie類采用了NSCoding協(xié)議這意味著我們將要實現(xiàn)encodeWithCoder:方法和initWithCoder:方法
//先將這兩個方法的內(nèi)容置空
//該代碼初始化了一個新的對象,清除了我們創(chuàng)建的所有東西,并且為NSCodering協(xié)議的方法創(chuàng)建了結(jié)構(gòu)框架以避免編譯器報錯,最后返回了描述信息的方法。
@implementation Thingie
@synthesize name;
@synthesize magicNumber;
@synthesize shoeSize;
@synthesize subThingies;
- (id)initWithName:(NSString *)n magicNumber:(int)mn shoeSize:(float)ss{
    if (self = [super init]) {
        self.name = n;
        self.magicNumber = mn;
        self.shoeSize = ss;
        self.subThingies = [NSMutableArray array];
    }
    return self;
}
-(void)dealloc{
    
}
//現(xiàn)在,我們來將這個對象歸檔
//我們使用NSKeyedArchiver把對象歸檔到NSData中,KeyedArchiver使用鍵/值來保存對象的信息
//Thingie的encodeWithCoder方法會使用與每個實例變量名稱匹配的鍵對其進行編碼
//也可以使用無規(guī)則的文字作為鍵來編碼,保證鍵的名稱與實例變量的名稱相似有助于識別它們之間的映射關(guān)系
//可以使用上面的字面量字符串作為編碼建
//可以使用#define kSubthingiesKey @"subThingies"來定義常量
//可以使用文件的局部變量,比如static NSString *kSubthingiesKey = @"subThingies";所定義的靜態(tài)變量
//注意,每種類型的encodeSomething:forKey:方法都不同。需要確保你的類型使用的是正確的編碼方法。
//對于所有的Objective-C對象類型,都要使用encodeObjective:forKey:方法。
-(void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:name forKey:@"name"];
    [aCoder encodeInt:magicNumber forKey:@"magicNumber"];
    [aCoder encodeFloat:shoeSize forKey:@"shoeSize"];
    [aCoder encodeObject:subThingies forKey:@"subThingies"];
}
//如果需要恢復某個歸檔的對象,可以使用decodeSomethingForKey方法
//initWithCoder:和其它的init:方法一樣,在為對象執(zhí)行操作之前,需要使用超類進行初始化
//可以采用兩種方式,具體取決于父類
//如果父類采用了NSCoding協(xié)議,則應(yīng)該調(diào)用[super initWithCoder:aDecoder],否則只需要調(diào)用[super init]
//當你使用decodeIntForKey:方法時,會把一個init值從aDecoder中取出
//當你使用decodeObjectForKey:方法時,會把一個對象從aDecoder中取出
//如果里面還有嵌入的對象,就會對其遞歸調(diào)用initWithCoder:方法。
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self =[super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.magicNumber = [aDecoder decodeIntForKey:@"magicNumber"];
        self.shoeSize = [aDecoder decodeFloatForKey:@"shoeSize"];
        self.subThingies = [aDecoder decodeObjectForKey:@"subThingies"];
    }
    return self;
}
-(NSString *)description{
    NSString *description = [NSString stringWithFormat:@"%@: %d/%.1f %@",name,magicNumber,shoeSize,subThingies];
    return description;
}
@end
//你將會注意到,編碼和解碼的順序和示例對象的順序完全相同。當然,你也可以不這么做,這只是一種簡便的習慣
//目的在于確保每個對象都進行了編碼和解碼操作,沒有對象被漏掉
//使用鍵進行調(diào)節(jié)的原因之一就是可以將示例對象按任意順序放入或取出
int main(int argc, const char * argv[]) {
    Thingie *thing;
    thing = [[Thingie alloc] initWithName:@"thing1" magicNumber:42 shoeSize:10.5];
    NSLog(@"some thing:%@",thing);
    //使用thing1對象,并將其歸檔
    NSData *freezeDried;
    //類方法+archivedDataWithRootObject:對這個對象進行了編碼,比如字符串、數(shù)組及放入數(shù)組中的任何對象
    //當所有的對象完成了鍵值編碼后,會被放入一個NSData并返回
    freezeDried = [NSKeyedArchiver archivedDataWithRootObject:thing];
    Thingie *thing1;
    thing1 = [NSKeyedUnarchiver unarchiveObjectWithData:freezeDried];
    NSLog(@"%@",thing1);
    //我們可以將對象放到subThing的可變數(shù)組中,當數(shù)組被編碼時,這些對象將被自動編碼
    //NSArray中encodeWithCoder:方法的實現(xiàn)會對所有的對象調(diào)用encodeWithCoder方法,直到所有對象都被編碼
    Thingie *anotherThing;
    anotherThing = [[Thingie alloc] initWithName:@"thing2" magicNumber:23 shoeSize:13.0];
    [thing1.subThingies addObject:anotherThing];
    anotherThing = [[Thingie alloc] initWithName:@"thing3" magicNumber:17 shoeSize:9.0];
    [thing1.subThingies addObject:anotherThing];
    NSLog(@"%@",thing1);
    //編碼解碼的工作機制是一樣的
    freezeDried = [NSKeyedArchiver archivedDataWithRootObject:thing1];
    thing1 = [NSKeyedUnarchiver unarchiveObjectWithData:freezeDried];
    NSLog(@"%@",thing1);
    return 0;
}
//運行結(jié)果
2018-12-21 06:26:32.998091-0800 編碼對象[1126:102536] some thing:thing1: 42/10.5 (
)
2018-12-21 06:26:32.999011-0800 編碼對象[1126:102536] thing1: 42/10.5 (
)
2018-12-21 06:26:32.999227-0800 編碼對象[1126:102536] thing1: 42/10.5 (
    "thing2: 23/13.0 (\n)",
    "thing3: 17/9.0 (\n)"
)
2018-12-21 06:26:32.999503-0800 編碼對象[1126:102536] thing1: 42/10.5 (
    "thing2: 23/13.0 (\n)",
    "thing3: 17/9.0 (\n)"
)
Program ended with exit code: 0
  • 我們使用NSKeyedArchiver把對象歸檔到NSData中,KeyedArchiver使用鍵/值來保存對象的信息。

  • Thingie的encodeWithCoder方法會使用與每個實例變量名稱匹配的鍵對其進行編碼。

  • 也可以使用無規(guī)則的文字作為鍵來編碼,保證鍵的名稱與實例變量的名稱相似有助于識別它們之間的映射關(guān)系。

  • 可以使用上面的字面量字符串作為編碼建。

  • 可以使用#define kSubthingiesKey @"subThingies"來定義常量。

  • 可以使用文件的局部變量,比如static NSString *kSubthingiesKey = @"subThingies";所定義的靜態(tài)變量。

  • 注意,每種類型的encodeSomething:forKey:方法都不同。需要確保你的類型使用的是正確的編碼方法。

  • 對于所有的Objective-C對象類型,都要使用encodeObjective:forKey:方法。

  • 如果需要恢復某個歸檔的對象,可以使用decodeSomethingForKey方法
    initWithCoder:和其它的init:方法一樣,在為對象執(zhí)行操作之前,需要使用超類進行初始化。

  • 可以采用兩種方式,具體取決于父類.如果父類采用了NSCoding協(xié)議,則應(yīng)該調(diào)用[super initWithCoder:aDecoder],否則只需要調(diào)用[super init]。

  • 當你使用decodeIntForKey:方法時,會把一個init值從aDecoder中取出;當你使用decodeObjectForKey:方法時,會把一個對象從aDecoder中取出,如果里面還有嵌入的對象,就會對其遞歸調(diào)用initWithCoder:方法。

  • 你將會注意到,編碼和解碼的順序和示例對象的順序完全相同。當然,你也可以不這么做,這只是一種簡便的習慣。目的在于確保每個對象都進行了編碼和解碼操作,沒有對象被漏掉。使用鍵進行調(diào)節(jié)的原因之一就是可以將示例對象按任意順序放入或取出。

  • 如果編碼的數(shù)據(jù)中含有循環(huán)會怎么樣?例如thing1就在自己的subThingies數(shù)組中會怎么樣?會不停的重復編碼嗎?Cocoa的歸檔實現(xiàn)非常智能,對象循環(huán)也可以進行保存或恢復。

  • 如果要進行實驗的話,可以將thing1放入其自身的subThingies數(shù)組中。但是不要嘗試在thing1中使用NSLog,NSLog不夠智能,不能檢測對象循環(huán)。因此,它將會陷入一個無限的遞歸,試圖創(chuàng)建日志信息,最終會導致成千上萬的-description調(diào)用進入調(diào)試器中。不過,如果對thing1進行編碼和解碼,它可以完美的運行,也不會陷入混亂中。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容