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..

和先前一样,利用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.

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