使用plist保存和加载数据

2,500 阅读6分钟

每一个iOS app都运行在一个被"隔离"的环境里,这个环境,就是我们经常提及的沙箱。在这个沙箱里,每一个app都认为自己拥有一套独立完整的文件系统可以访问。但实际上,它们只能在自己的沙箱里活动,而对其他app的环境一无所知。

在沙箱里,每一个app都有一个叫做"Documents"的目录,用来保存App的数据。当你通过iTunes或者iCloud备份的时候,每一个App的"Documents"目录都会被备份;而当你更新App的时候,"Documents"目录的内容,也会被完整保留下。总之,无论你想保存App运行中的任何内容,使用"Documents"目录,就对了。


访问Documents目录

首先,我们来看如何访问Documents目录,在EpisodeListViewController里,添加下面的代码:


class EpisodeListViewController: UITableViewController {
    // ...

    func documentsDirectory() -> NSURL {

        let urls = NSFileManager
            .defaultManager()
            .URLsForDirectory(.DocumentDirectory, 
                inDomains: .UserDomainMask)

        return urls[0]
    }
}

首先,我们使用NSFileManager.defaultManager读取了系统默认的文件管理器对象,然后在URLsForDirectory方法里,.DocumentDirectory表示我们要获取Document目录的URL,.UserDomainMask表示在当前用户的Home目录中查找。

尽管URLsForDirectory会返回一个NSURL数组,但是,由于iOS App都运行在自己的沙箱里,因此,Document目录肯定存在并且也只有一个,我们直接返回url[0]就可以了。

当我们要在Document目录里创建文件的时候,我们可以用下面的代码生成一个文件的NSURL:


class EpisodeListViewController: UITableViewController {
    // ...

    func fileUrl(fileName: String) -> NSURL {
        let documentUrl = self.documentsDirectory()
            .URLByAppendingPathComponent(fileName)

        return documentUrl
    }
}

"尽量避免直接使用字符串来拼接路径,使用这些方法会避免很多小错误(例如,你不确定是否需要在文件名前或者URL结尾添加/等)"。

特别提示

然后,我们可以在viewDidLoad方法里,把这些路径打印在控制台上:


class EpisodeListViewController: UITableViewController {
    // ...
    override func viewDidLoad() {
        super.viewDidLoad()

        let docUrl = documentsDirectory()
        let file = fileUrl("EpisodeList.plist")

        print("Document url: \(docUrl)");
        print("Data file url: \(file)");
    }
}

然后 Command + R编译执行,在控制台里,我们可以看到类似下面的结果:

image

我们复制URL中路径的部分,然后在Finder里按Command + Shift + G,把路径粘贴进去,打开之后,我们可以看到每一个App都用一个随机字符串来表示,并且,每一个App都有自己的Document / Library / tmp目录。

image

回到Xcode,接下来,我们来了解什么是plist文件。


什么是plist文件?

所谓plist,就是Property List的缩写,本质上,它是一个XML文件。每一个App都有一个叫做Info.plist的文件,在项目文件列表里,我们选中它,就可以看到类似这样的内容:

image

其中每一行都表示了一个App的配置信息,如果我们右键选中Info.plist,然后选择"Open as / Source code",就可以在Xcode里,发现它就是一个XML格式的文本文件了:

image

接下来,我们就在App的Document目录下,创建一个EpisodeList.plist文件,用来保存获取的视频列表。


保存EpisodeListItem到plist文件

为了把视频列表保存在plist文件里,首先我们要对EpisodeListItem进行一些修改,让它从NSObject派生,并且遵从NSCoding protocol:


class EpisodeListItem : NSObject, NSCoding {
    var title = ""
    var finished = false
}

然后,我们来实现NSCoding中约定的方法,首先,我们来告诉iOS在保存EpisodeListItem对象的时候,如何"编码"它的属性:


class EpisodeListItem : NSObject, NSCoding {
    // ...

    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(title, forKey: "Title")
        aCoder.encodeBool(finished, forKey: "Finished")
    }
}

其中NSCoder用来把各种类型的值"编码"成适合保存在文件里的形式。于是,上面的方法意思是说,当保存一个EpisodeListItem对象的时候,把它的title属性用键值"Title"保存成一个对象,把它的finished属性用键值"Finished"保存成一个Bool值。

然后,我们要添加两个init方法:


class EpisodeListItem : NSObject, NSCoding {
    // ...

    required init?(coder aDecoder: NSCoder) {
        super.init()
    }

    override init() {
        super.init()
    }
}

虽然它们没什么实际用途,为了让编译器通过编译通过,我们必须这样定义它们。

这样,iOS就知道如何把我们的EpisodeListItem对象序列化到一个plist文件了,接下来,我们回到EpisodeListViewController,把获取的视频列表写到EpisodeList.plist里:


class EpisodeListViewController: UITableViewController {
    // ...

    func saveEpisodeListItems() {
        // 1\. Create a concrete archiver
        let data = NSMutableData()
        let archiver = NSKeyedArchiver(forWritingWithMutableData: data)

        // 2\. Serialize object into archiver
        archiver.encodeObject(episodeListItems, forKey: "EpisodeListItems")
        archiver.finishEncoding()

        // 3\. Create the file
        data.writeToURL(fileUrl("EpisodeList.plist"), atomically: true)
    }
}

从上面的代码可以看到,把一个对象写入到plist大体分成三步:

首先,我们创建一个NSMutableData对象,并且用它创建一个NSKeyedArchiver,这个archiver用来把我们要保存的内容编码到NSMutableData对象里;

其次,我们把要保存的数组,用一个键值"EpisodeListItems"通过archiver进行编码,在这里,由于我们之前让EpisodeListItem遵从了NSCoding protocol,因此archiver会按照我们的要求,对数组里的内容进行编码;编码完成后,我们用finishedEncoding()通知archiver;

此时,data里就包含编码过的我们要保存的内容了,我们使用writeToURL方法,直接把data写到我们指定的plist文件里;

完成之后,按Command + R编译执行:

image

在控制台里,我们用同样的方法复制Documents目录到Finder,我们就可以在里面看到EpisodeList.plist文件了,用Xcode打开它,可以看到类似这样的内容:

image

也许,这并不想你想象的那样易读,实际上,NSKeyedArchiver编码过的内容,它并不是用来给我们读的。接下来,我们就来看,如何从文件加载保存过的内容。


加载plist中的内容

就像我们要告诉iOS如何编码EpisodeListItem对象一样,为了读取它,我们也要告诉iOS如何解码它。打开EpisodeListItem.swift文件,添加下面的代码:


class EpisodeListItem : NSObject, NSCoding {
    // ...

    required init?(coder aDecoder: NSCoder) {
        title = aDecoder.decodeObjectForKey("Title") as! String
        finished = aDecoder.decodeBoolForKey("Finished")

        super.init(coder: aCoder)
    }
}

当我们通过加载EpisodeList.plist创建EpisodeLiteItem对象的时候,执行的代码和保存文件的时候是相反的。我们通过decodeObjectForKey,把plist文件中和Key对应的值读取出来。对于title来说,由于它被编码成了一个Object,因此解码出来的对象类型是AnyObject,我们要把它转换成一个String类型。

定义了解码EpisodeListItem的方式之后,回到EpisodeListViewController。我们应该把通过EpisodeList.plist初始化EpisodeListItems的代码放在哪呢?你可能会很自然的想到viewDidLoad啊。当然,这不会造成什么问题,但是,作为一个和任何UI显示都不相关的代码,我们应该把它放到EpisodeListViewController的init方法里。

当我们通过storyboard加载一个view controller时,iOS会调用一个和EpisodeListItem中类似的init方法,而这是我们初始化EpisodeListItems的绝佳机会:


class EpisodeListViewController: UITableViewController {
    // ...

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        loadEpisodeListItems()
    }
}

然后,我们来实现这个loadEpisodeListItems方法:


class EpisodeListViewController: UITableViewController {
    // ...

    func loadEpisodeListItems() {
        let episodeUrl = fileUrl("EpisodeList.plist");

        var error: NSError?
        let fileExist = 
            episodeUrl.checkResourceIsReachableAndReturnError(&error)

        if fileExist {
            if let data = NSData(contentsOfURL: episodeUrl) {
                let unarchiver = 
                    NSKeyedUnarchiver(forReadingWithData: data)
                episodeListItems =
                    unarchiver.decodeObjectForKey(
                        "EpisodeListItems") as! [EpisodeListItem]

                unarchiver.finishDecoding()
            }
        }
        else {
            getEpisodeListItemData()
            saveEpisodeListItems()
        }
    }

}

首先,我们使用了checkResourceIsReachableAndReturnError方法,判断文件是否存在。如果存在,我们用EpisodeList.plist文件的内容生成一个NSData对象,然后,我们基于这个NSData对象生成一个NSKeyedUnarchiver对象,并且,针对其中键值为EpisodeListItems的对象进行解码。

由于我们之前在EpisodeLitemItem类中,定义了解码方法,因此,我们就可以自动获得一个解码后的EpisodeListItem数组了。

否则,如果EpisodeList.plist文件不存在,我们就手动初始化EpisodeListItems数组,然后,新建一个EpisodeList.plist文件。

最后,我们删掉原来在viewDidLoad()中的初始化代码,Command + R编译执行,这次,我们就可以从EpisodeList.plist中,加载UITableView要显示的内容了。