iOS开发技巧系列---打造强大的BaseModel(篇二:让Model实现自动归档)

本文是iOS开发技巧系列---打造强大的BaseModel中的篇二,第一篇文章请见此:
让Model自我描述 ,相对于让Model实现自我描述,让Model实现自动归档的难度大得多。我相信能够好好看完这几篇文章的人,绝对是有大收获的。另外,些文章不适合新手,只适合有一定有Swift开发经验的人。

2018年Swift4已经发布,现在需要更新这些文章了,里面的代码可能都跑不起了。所以我要修正这些代码让其跑起来。我把这些代码都放在iOSDemo项目里
https://github.com/DuckDeck/iOSDemo

什么是iOS的归档

归档--NSKeyedArchiver,是iOS开发中基本的数据存储方式之一,和其他的数据存储方式相比,归档不仅能够存储任意类型的数据,而且使用起来也很简单。归档能将数据处理成NSData的形式,所以很容易以文件的形式保存在APP的沙盒中,而解归和归档相反,它是将保存在APP沙盒的归档文件逆归档,转换成归档前的状态。

传统的iOS归档方式

要想让一个自定义对象可以使用归档,必须要让其符合NSCoding协议,

public protocol NSCoding {
    public func encodeWithCoder(aCoder: NSCoder)
    public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}
@end

上面的代码是iOS中NSCoding协议的定义。里面包含两个方法,其中一个是构造器。第一个方法

public func encodeWithCoder(aCoder: NSCoder)

就是归档方法,它是为了告诉NSKeyedArchiver对象如何将数据归档成文件的。第二个方法(构造器)

public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER

就是解档方法了。它是告诉NSKeyedUnArchiver是如何将归档好的对象解档成原来的数据的
下面来看看传统的iOS归档方式,先定义一个类,让其符合NSCoding协议

 @objcMembers class DemoArchiver:GrandModel,NSCoding {
    var demoString:String?
    var demoInt = 100
    var demoFloat:Float = 0.0
    var demoDate = Date()
    override init() { }
    
    func encode(with aCoder: NSCoder) {//归档需要实现的方法
        aCoder.encode(demoString, forKey: "demoString")
        aCoder.encode(demoInt, forKey: "demoInt")
        aCoder.encode(demoFloat, forKey: "demoFloat")
        aCoder.encode(demoDate, forKey: "demoDate")
    }
    
    @objc required init?(coder aDecoder: NSCoder) {//解档需要实现的构造器
        demoString = aDecoder.decodeObject(forKey: "demoString") as? String
        demoInt = aDecoder.decodeInteger(forKey: "demoInt")
        demoFloat = aDecoder.decodeFloat(forKey: "demoFloat")
        demoDate = aDecoder.decodeObject(forKey: "demoDate") as! Date //存在强制转换情况
    }
}

我们需要在正确地重写这两个方法。这里面最需要注意的点有两个,一是不要把数据类型搞错。二是key名不要弄错了。然后下面开始测试

     let demoTest = DemoArchiver()
     demoTest.demoString = "ABCDEFG"
     demoTest.demoFloat = 11.11
     print(demoTest)
     let a = NSKeyedArchiver.archivedDataWithRootObject(demoTest)
     let b = NSKeyedUnarchiver.unarchiveObjectWithData(a)
     print(b)
     //打印结果
     DemoArchiver:["demoInt": 100, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-09 13:03:17 +0000]
Optional(DemoArchiver:["demoInt": 100, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-09 13:03:17 +0000])

可见经过归档再解档后的数据又恢复了原样。这里需要说明一下的是,一般是需要把归档后的文件保存在APP的沙盒目录内的,需要使用时再取出来解档。这里为了测试方便就不这么做了。

传统的iOS归档方式的弊端

相信大家很容易看出使用传统的iOS归档方式的不足之处,还是和以前一样,需要写太多的重复啰嗦代码了。目前对于Objc语言来说,有一个代码生成器(Accessorizer,见(http://www.kevincallahan.org/software/accessorizer.html))可以使用,只需要把所有属性放进去,就可以生成所有属性的归档解档方法。遗憾的是Swift目前还没有这种工具可以用(或者有了但是我不知道),2018年应该有了,只是我不太想用这东西。只有老实的让每个Model符合NSCoding协议,再写出每个属性的归档&解档方法。其中最让人疼的是有些属性还需要强制转换。而一般情况下一个项目的Model数都超过了两位数,虽然不一定每个Model都需要归档功能,但是如果一个类里面属性太多的话,写起来会让人很郁闷的。

使用RunTime实现自动归档

如果读者看了我先前的两篇--打造强大的BaseModel文章,脑子了应该可以很快构思出使用RunTime和KVC来实现自动归档的思路。先用RunTime获取Model中所有属性名,再用KVC获取每一个属性的值。再调用encodeWithCoder就能实现归档了。嗯,这种想法不错,下面直接写代码吧。
还是和以前一样,先写一个返回该类所有属性名的方法

   func getSelfProperty()->[String]{  //和description属性一样
        var selfProperties = [String]()
        var count:UInt32 =  0
        let vars = class_copyIvarList(type(of: self), &count)
        for i in 0..<count {
            let t = ivar_getName((vars?[Int(i)])!)
            if let n = NSString(cString: t!, encoding: String.Encoding.utf8.rawValue) as String?
            {
                selfProperties.append(n)
            }
        }
        free(vars)
        return selfProperties
    }

和先前一样,利用Objc运行时的一系列方法可以从该类获取所有的属性名,下面是测试

@objcMembers class DemoArchiver:GrandModel {
    var demoString:String?
    var demoInt = 0
    var demoFloat:Float = 0.0
    var demoDate = NSDate()
    
    override init(){}
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
print(DemoArchiver().getSelfProperty())
//打印出**["demoString", "demoInt", "demoFloat", "demoDate"]**

下面来让GrandModel实现NSCoding协议,注意,实现NSCoding协议不能使用extension,因为指定构造器不能声明在extension中

class GrandModel:NSObject,NSCoding{
    //归档方法
    func encode(with aCoder: NSCoder) {
        let item = type(of: self).init()
        let properties = item.getSelfProperty()
        for propertyName in properties{
            let value = self.value(forKey: propertyName)
            aCoder.encode(value, forKey: propertyName)
        }
    }
    
    //解档方法
    required init?(coder aDecoder: NSCoder) {
        super.init()
        let item = type(of: self).init()
        let properties = item.getSelfProperty()
        for propertyName in properties{
            let value = aDecoder.decodeObject(forKey: propertyName)
            self.setValue(value, forKey: propertyName)
        }
    }
 }

没想到这么快就写好了,看起来也不难嘛,但是实际上这里这里存在一个显而易见的问题,就是归档方法中需要根据属性的类型调用不同的encode(属性类型)方法,本文的第一个例子里很清楚,对于Int类型的属性,需要调用aCoder.encodeInteger方法,Float和Double也不一样。如果统一使用 aCoder.encodeObject方法,就会造成数据类型丢失,

测试使用RunTime实现自动归档是否有效

这里可以测试一下。还是用文章开头的例子的哪个类,只不过需要去掉里面其他所有的方法只保留属性,并且添加了一些属性用来测试

class DemoArchiver:GrandModel {
    var demoString:String?
    var demoInt = 10
    var demoFloat:Float = 11.0
    var demoDouble:Double = 22.0
    var demoDate = NSDate()
    var demoRect = CGRect(x: 1, y: 1, width: 1, height: 1)
}
   let demoTest = DemoArchiver()
   demoTest.demoString = "ABCDEFG"
   demoTest.demoFloat = 11.11
   print(demoTest)
   let a = NSKeyedArchiver.archivedDataWithRootObject(demoTest)
   let b = NSKeyedUnarchiver.unarchiveObjectWithData(a)
   print(b)
   //打印结果为
   **DemoArchiver:["demoDouble": 22, "demoInt": 10, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-12 07:57:57 +0000]
Optional(DemoArchiver:["demoDouble": 22, "demoInt": 10, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-12 07:57:57 +0000])**

实际上测试结果出乎我意料之外,非常完美,所有属性都成功地归档保存下来,解档后数据没有出现丢失的情况。对此我的分析是:这一切都是KVC的功劳。因为KVC取出的属性都是为AnyObject?类型,那么归档也就可以很方便地调用aCoder.encodeObject这个方法,所以数据以AnyObject类型保存。取出来时正好相反,用aDecoder.decodeObjectForKey这个角档方法取出来的数据类型都是AnyObject?类型的。然后KVC在组属性赋值并不需要知道每个属性是什么样的数据类型,都可以正确地赋值。难道事情就这样解决了吗?我们来看看下个测试用例

@objcMembers class DemoArchiver:GrandModel {
    var demoString:String? = ""
    var demoInt = 0
    var demoFloat:Float = 0.0
    var demoDate = NSDate()
    var demoRect = CGRect(x: 1, y: 1, width: 1, height: 1)
}
let demoTest = DemoArchiver()
demoTest.demoString = "ABCDEFG"
demoTest.demoFloat = 11.11
print(demoTest)
let a = NSKeyedArchiver.archivedData(withRootObject: demoTest)
let b = NSKeyedUnarchiver.unarchiveObject(with: a)
print("------------归档后的数据------------")
print(b)
   //打印结果为
   **DemoArchiver:["demoFloat": 11.11, "demoString": ABCDEFG, "demoInt": 0, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:14:29 +0000]
------------归档后的数据------------
Optional(DemoArchiver:["demoFloat": 11.11, "demoString": ABCDEFG, "demoInt": 0, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:14:29 +0000])**

结果比预料中好了很多,nil的属性都可以正确打印出来。但是和以前一样,demoFloat:Float?这个属性又丢失了,这是很正常的,因为Objc不支持这种数据类型。读过我这系列文章的读者都可以明白。

从打印结果可以看出,归档后的数据Unarchiver后和原来的是一样的,说明GrandModel起到作用了。

那么如果属性类型是其他对象,或者是Array和字典类型呢?自动归档还能正常工作吗?答案是肯定的,只要该对象(Array或者Dict里保存的对象)都继承于GrandModel,都可以实现自动归档解档。

@objcMembers class DemoArchiver:GrandModel {
    var demoString:String? = ""
    var demoInt = 0
    var demoFloat:Float = 0.0
    var demoDate = NSDate()
    var demoRect = CGRect(x: 1, y: 1, width: 1, height: 1)
    var demoClass:demoArc?
    var demoArray:[demoArc]?
    var demoDict:[String:demoArc]?
}


@objcMembers class demoArc:GrandModel {
    var daString:String? = "default"
    var daInt:Int = 0
}

//下面测试
    let demoTest = DemoArchiver()
    demoTest.demoFloat = 11.11
    demoTest.demoClass = demoArc()
    demoTest.demoClass?.daInt = 8
    demoTest.demoClass?.daString = "demoArc"
    let a1 = demoArc()
    let a2 = demoArc()
    a1.daString = "a1"
    a1.daInt = 1
    a2.daInt = 2
    a2.daString = "a2"
    demoTest.demoArray = [a1,a2]
    demoTest.demoDict  = ["demo1":a1,"demo2":a2]
    print(demoTest)
    let a = NSKeyedArchiver.archivedData(withRootObject: demoTest)
    let b = NSKeyedUnarchiver.unarchiveObject(with: a)
    print("------------归档后的数据------------")
    print(b)
        
        //打印结果
        **DemoArchiver:["demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:31:59 +0000, "demoFloat": 11.11, "demoString": , "demoInt": 0, "demoArray": <_TtGCs23_ContiguousArrayStorageC12ConsoleSwift7demoArc_ 0x100f617a0>(
demoArc:["daInt": 1, "daString": a1],
demoArc:["daInt": 2, "daString": a2]
)
, "demoClass": demoArc:["daInt": 8, "daString": demoArc], "demoDict": {
    demo1 = "demoArc:[\"daInt\": 1, \"daString\": a1]";
    demo2 = "demoArc:[\"daInt\": 2, \"daString\": a2]";
}]
------------归档后的数据------------
Optional(DemoArchiver:["demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:31:59 +0000, "demoFloat": 11.11, "demoString": , "demoInt": 0, "demoArray": <__NSArrayI 0x101851250>(
demoArc:["daInt": 1, "daString": a1],
demoArc:["daInt": 2, "daString": a2]
)
, "demoClass": demoArc:["daInt": 8, "daString": demoArc], "demoDict": {
    demo1 = "demoArc:[\"daInt\": 1, \"daString\": a1]";
    demo2 = "demoArc:[\"daInt\": 2, \"daString\": a2]";
}])**

结果完全符合预期。

总结

让Model自动归档是iOS Runtime和KVC强大威力的又一次体现。这个组合就像一把锋利的尖刀,可以准确高效地解决问题,避免写很多重复的代码。缺点就是效率比正常代码要低一点,但是我认为这完全是可以接受的。这三篇文章所有的相关代码都可以在我的Github里面找到(https://github.com/DuckDeck/iOSDemo),你们读者能给个Star.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,107评论 29 470
  • 时光匆匆,岁月如梭。又是新年了,今年的的新年还是和往年一样热闹。从腊月28开始全家团聚祭拜灶王爷,吃饺子和麻糖...
    一颗小苗阅读 448评论 0 0
  • 12月2日,周三,景芳,两节古诗词教学,万千思绪感悟。留给我印象最深的就是以下三点: 一、谈“涵咏、推敲” 谈涵咏...
    晚起画蛾眉阅读 1,820评论 0 1
  • 每当大地沉睡,人们鼾声似起的时候,我总喜欢去淘宝浏览一番才能睡觉,就跟大多数女人爱逛商场一样,有瘾,总也忍不住。平...
    自由和人生阅读 1,510评论 0 0