NSKeyedArchiver缓存app用户账号数据

/ 0

某些app用户只需要登录一次,退出app再次打开后还是保持登录状态。实现这个需求,用脚趾头都能想到用户账号数据已经做了持久化处理,那么怎么做的呢?今天我就写一篇笔记,给没有经验的朋友参考。

iOS的存储方式

NSKeyedArchiver:使用归档保存数据需要遵守NSCoding协议,并且该对象对应的类必须提供encodeWithCoder:initWithCoder:方法。前一个方法告诉系统怎么对对象进行编码,而后一个方法则是告诉系统怎么对对象进行解码。

NSUserDefaults:偏好设置用于保存一些简单数据,比如app里一些设置数据、状态。NSUserDefaults可以存储的数据类型包括:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要存储其他类型,则需要转换为前面的类型,才能用NSUserDefaults存储。

Write:直接将对象永久写入到本地磁盘中,一些类自带写入属性。比如UIImage、NSString等,或者使用NSFileManager进行文件操作。

SQLite:采用数据库的形式存储数据,iOS中使用SQLite来存储一些数据量比较大的数据。比如新闻资讯类的app实现本地数据缓存,大都是采用数据库形式存储。数据库存储相比前面三种存储方式来说,相对比较复杂一些,但对大量数据进行缓存管理还是得需要数据库的。

需求分析

首先我们要明确一点,我们实现用户数据缓存的目的是保持用户的登录状态和数据(由于本人垃圾,所以这里不考虑用户数据隐私)。

一般app里我们都会定义一个用户账号类来管理用户数据,然后让这个类的对象能够一直保存在内存中,被整个app共享,并且修改了用户数据也要及时更新(更新本地数据、异步上传更新服务端的用户数据)。然后提供登录后的初始化操作方法和注销登录后的清理方法,在登录后就将数据缓存,退出后清除用户数据。

并且,一般后台会提供一个token字段和token有效期字段,用于判断用户登录状态是否有效。如果token失效,则清理用户数据。

代码实现

这里我就以最近写的一个 六阿哥客户端 app为例,创建账号类JFAccountModel,并声明一堆用户相关的属性,并提供一个转模型的构造方法:

/// 令牌
var token: String?
/// 用户id
var id: Int = 0
/// 用户名
var username: String?
/// 昵称
var nickname: String?
/// 注册时间
var registerTime: String?
/// 邮箱
var email: String?
/// 头像路径
var avatarUrl: String?
/// 用户组
var groupName: String?
/// 积分
var points: String?
/// 个性签名
var saytext: String?
/// 电话号码
var phone: String?
/// qq号码
var qq: String?
// KVC 字典转模型
init(dict: [String: AnyObject]) {
    super.init()
    setValuesForKeysWithDictionary(dict)
}
override func setValue(value: AnyObject?, forUndefinedKey key: String) {}

遵守协议实现归档和解档方法:

// MARK: - 归档和解档
func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(token, forKey: "token")
    aCoder.encodeInt(Int32(id), forKey: "id")
    aCoder.encodeObject(username, forKey: "username")
    aCoder.encodeObject(registerTime, forKey: "registerTime")
    aCoder.encodeObject(email, forKey: "email")
    aCoder.encodeObject(avatarUrl, forKey: "avatarUrl")
    aCoder.encodeObject(groupName, forKey: "groupName")
    aCoder.encodeObject(points, forKey: "points")
    aCoder.encodeObject(saytext, forKey: "saytext")
    aCoder.encodeObject(phone, forKey: "phone")
    aCoder.encodeObject(qq, forKey: "qq")
    aCoder.encodeObject(nickname, forKey: "nickname")
}

required init?(coder aDecoder: NSCoder) {
    token = aDecoder.decodeObjectForKey("token") as? String
    id = Int(aDecoder.decodeIntForKey("id"))
    username = aDecoder.decodeObjectForKey("username") as? String
    registerTime = aDecoder.decodeObjectForKey("registerTime") as? String
    email = aDecoder.decodeObjectForKey("email") as? String
    avatarUrl = aDecoder.decodeObjectForKey("avatarUrl") as? String
    groupName = aDecoder.decodeObjectForKey("groupName") as? String
    points = aDecoder.decodeObjectForKey("points") as? String
    saytext = aDecoder.decodeObjectForKey("saytext") as? String
    phone = aDecoder.decodeObjectForKey("phone") as? String
    qq = aDecoder.decodeObjectForKey("qq") as? String
    nickname = aDecoder.decodeObjectForKey("nickname") as? String
}

定义一个私有的静态属性用于持久保存JFAccountModel对象在内存中:

// 持久保存到内存中
private static var userAccount: JFAccountModel?

创建归档JFAccountModel对象的路径,一般都是保存在沙盒中的Document目录下:

/// 归档账号的路径
static let accountPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! + "/Account.plist"

归档JFAccountModel的方法:

func saveAccount() {
    NSKeyedArchiver.archiveRootObject(self, toFile: JFAccountModel.accountPath)
}

登录成功后保存JFAccountModel的方法:

func updateUserInfo() {
    // 保存到内存中
    JFAccountModel.userAccount = self
    // 归档用户信息
    saveAccount()
}

这里我们来模拟登录,发送登录请求:

var parameters: [String : AnyObject]
parameters = [
    "username" : self.usernameField.text!,
    "password" : self.passwordField.text!
]

// 发送登录请求
JFNetworkTool.shareNetworkTool.post(LOGIN, parameters: parameters) { (success, result, error) in
    if success {
        if let successResult = result {
           let account = JFAccountModel(dict: successResult["data"].dictionaryObject!)
            // 更新用户本地数据
            account.updateUserInfo()
        }
    } else if result != nil {
        JFProgressHUD.showInfoWithStatus(result!["data"]["info"].string!)
    }

}

在登录成功后调用上面的 updateUserinfo() 方法进行更新本地缓存数据。

退出登录后的清理工作,因为我这里也使用了第三方登录,所以在退出登录的同时取消授权:

class func logout() {
    ShareSDK.cancelAuthorize(SSDKPlatformType.TypeQQ)
    ShareSDK.cancelAuthorize(SSDKPlatformType.TypeSinaWeibo)
    
    // 清除内存中的账号对象和归档
    JFAccountModel.userAccount = nil
    do {
        try NSFileManager.defaultManager().removeItemAtPath(JFAccountModel.accountPath)
    } catch {
        print("退出异常")
    }
}

至此,从登录成功并归档账号数据,到退出后清理归档和内存中的账号数据就做完了。

补充完善

上面只是实现了基本的用户数据归档和清理,但是这样的还不够。比如,开头我们说到的让JFAccountModel对象能够给整个app共享,我们什么时候去登录有效期处理,在进行一些用户操作的时候判断当前用户是否是登录状态。这些我们都可以封装在JFAccountModel用户类里面的。

我这里的做法是,在app启动的时候去判断是否已经登录,首先我们需要提供一个方法来让JFAccountModel对象能够被app共享:

static func shareAccount() -> JFAccountModel? {
    if userAccount == nil {
        userAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? JFAccountModel
    }
    if userAccount == nil {
        // 没有则说明没有登录
        return nil
    } else {
        // 有则说明已经登录
        return userAccount
    }
}

调用上面的方法,如果返回不为nil,则说明已经登录,并获取到了JFAccountModel对象。我们再提供一个便捷的方法直接判断用户是否已经登录:

class func isLogin() -> Bool {
    return JFAccountModel.shareAccount() != nil
}

app启动时在appDelegatedidFinishLaunchingWithOptions方法中去判断当前用户信息是否有效:

class func checkUserInfo(finished: () -> ()) {
    if isLogin() {
        // 已经登录并保存过信息,验证信息是否有效
        let parameters: [String : AnyObject] = [
            "username" : JFAccountModel.shareAccount()!.username!,
            "userid" : JFAccountModel.shareAccount()!.id,
            "token" : JFAccountModel.shareAccount()!.token!
        ]
        
        JFNetworkTool.shareNetworkTool.post(GET_USERINFO, parameters: parameters, finished: { (success, result, error) in

            guard let successResult = result where success == true else {
                JFAccountModel.logout()
                print("登录信息无效")
                return
            }
            
            print("登录信息有效")
            let account = JFAccountModel(dict: successResult["data"].dictionaryObject!)
            // 更新用户信息
            account.updateUserInfo()
            
            // 更新完成回调
            finished()
        })
    }
}

这个方法也可以作为更新本地用户信息的方法,比如用户在其他手机上的app登录过后的一些数据改变,我们需要去更新同步这些数据。当然,还有一种情况,也就是没有网络或者网络差的情况,直接去调用这个方法会导致清空本地用户数据,这个可以根据自己的需求去变通。比如判断请求结果,根据不同情况去处理,或者判断当前网络状态,无网络情况不去更新用户信息。

这只是举一个栗子,真正做项目的时候还是得根据后端api返回的具体数据去具体分析的,比如我们这里将用户登录有效期和更新用户信息给写成一个方法了,因为我对用户是否是真的登录,要求并不是很高。就算登录失效,本地用户的数据也是可以用的。只是在调用一些接口的时候,需要传递token,这个时候可能传递错误的token,后台就会返回对应错误码给你,你再去针对处理就行。一些对用户安全性要求比较低的app,可能根本就没有token字段,或者有token字段但没有token有效期。

下一篇继续讲解缓存,使用SQLite缓存本地数据。