iOS开发技巧系列---使用泛型类来实现数据持久化

以前在用C#开发程序的时侯,只要用到数组,必然离不开泛型。再配合集合的扩展方法和LINQ,对于集合数据的操作真是得心应手,非常爽快。后来我转到iOS开发,学会了Objective语言。发现在Objective-C里,一般最常用的数组容器就是NSArray和字典NSDictionary了。可惜这两者都不支持泛型。数据都是以NSObject的类型添加进来,理论上可以保存保存任何的引用类型,这就需要让开发者来确保所有添加的数据类型的一至性。同样的,当从数组容器里取值时,需要将其转化成对应的Model。有时侯还需要进行类型判断。这样的操作不仅增加了代码的复杂性,也很容易出错。而Swift作为一门后起之秀,必然添加了对泛型的支持。弥补了Objective-C对数组操作的安全性,复杂性等的不足,而本篇文章向大家介绍了如果来定义一个泛型类,通过它来作为一种可以永久保存任意数据类型的数据容器,来实现iOS的数据持久化。

什么是泛型

为了更直观一点,首先让我们来看看不支持泛型的Objective-C语言是怎么利用数据容器的

    @interface demo:NSObject//自定义对象
    @property (nonatomic,copy) NSString* demoString;
    @end
    @implementation demo

    @end

    NSMutableArray* arr = [NSMutableArray new];
    [arr addObject:[NSNumber numberWithBool:YES]];          //添加Bool类型
    [arr addObject:@"111"];                                 //可以字符串
    demo* dm = [demo new];
    dm.demoString = @"String";
    [arr addObject:dm];                                     //添加自定义对象
    NSLog(@"%@",arr);
    BOOL a = [arr[0] boolValue];                            //需要转成Bool
    NSString* b = arr[1] ;                                  //直接将id类型赋到NSString类型
    demo* dm1 = arr[2];                                     //直接将id赋值给demo类型
    NSLog(@"a:%hhd b:%@.dm:%@",a,b,dm1);                    //可以正确地打印出来
    NSString* dm2 = arr[2];                                 //也可以将本身是demo类型的赋值到String
    NSLog(@"%@",dm2);                                       //不会报错.运行时dm2本身还是demo类型
    dm2.length;                                             //调用length方法就会出错,

    打印结果:
    2016-03-31 15:29:28.437 DemoObjc[1108:32945] (
    1,
    111,
    "<demo: 0x1001025f0>"
    )
    2016-03-31 15:29:28.438 DemoObjc[1108:32945] a:1 b:111.dm:<demo: 0x1001025f0>
    2016-03-31 15:29:28.438 DemoObjc[1108:32945] <demo: 0x1001025f0>//虽然在代码阶段是NSString类型,但是运行时是demo类型
    2016-03-31 15:29:28.438 DemoObjc[1108:32945] -[demo length]: unrecognized selector sent to instance 0x1001025f0
    2016-03-31 15:29:28.439 DemoObjc[1108:32945] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[demo length]: unrecognized selector sent to instance 0x1001025f0'
    )

从上面的示例代码可以很清楚地看到,因为Objective-C不支持泛型,所以编译器没有做任何限制,可以在NSMutableArray里添加任何引用类型的数据,这些数据都以id类型保存。在取值时也一样,默认取出来的数据都是id类型,需要将其转化成原先保存的类型才行。这些操作都是需要开发者来确保正确性。如果不小心写错了,编译器也不会给出任何提示或者警告,只有在运行程序时才能触发错误。所以很容易出现隐藏的Bug,同时类型转换也增加了代码的复杂性。

而泛型的出现刚好完全解决了上面的问题:

var arrInt = [Int]()    //定义了一个数据类型是Int类型的数组,其实就是一个只能添加Int类型数据的NSMutableArray。
arrInt.append(1)        //添加1
arrInt.append(2)        //添加2
arrInt.append("123")    //添加字符串“123”
//编译器报错,Cannot convert value of type 'String' to expected argument type 'Int'
//不能将String转换成Int
print(arrInt[0].dynamicType)  //取第一个值 ,打印出类型是Int
let str:String = arrInt[1]    //将第二个值赋值给String
//编译器报错, Cannot convert value of type 'Int' to specified type 'String'
//不能将转Int换成String

从上面的代码很容易看出,对于数据类型是Int的数组,只能添加Int类型的数据,其他的数据类型编译器都会报错。取值也一样,取出来的数直接就是Int类型了,不需要再转型了。
而且比较方便的是Swift的数据容器是是可以直接保存值类型的,不需要像Objective-C一样需要将其转成NSNumber了。同样,你也可以声明一个类型为String类型的数组([String]),
或者自定义类的的泛型数组,编译器都会帮你在编码时告诉你正确的数据类型,防止开发者添加错误的数据类型。

使用泛型的优点

使用泛型的优点有很多:

  • 泛型提供了一个强类型的编程模型
  • 编译时的类型检查减少了运行时发生数据类型转换异常的几率
  • 简化了代码,缓解了代码膨胀。
  • 性能得到了提升,不需要在运行时再做类型检查。
  • 代码的可读性更好,并且有更好的代码智能提示。

其实在最新的XCode 7.X中,苹果也悄悄地加入了Objective-C语言的弱泛型支持,见下面代码。

    NSMutableArray<NSString *>* arrString  = [NSMutableArray new]; //可以在NSMutableArray后面加上数据类型,这样就声明了一个泛型的NSMutableArray

    [arrString addObject:[NSNumber numberWithUnsignedInteger:1]]; //这里编译器会警告你添加了错误的数据类型,但是不会强制报错。

可以在声明NSMutableArray时添加一个弱泛型约束,之所以是弱泛型,是因为编译器会帮你检查数据类型是否正确,如果不正确会有一个警告,但是不会强制报错,代码还是可以编译过的。



写代码时XCode会自动提示它你应该添加什么类型的数据



取值也会告诉你里面保存了什么类型的数据

如果你把错误的数据类型存进去,编译器会警告,但不是强制报错。

iOS数据持久化方案的问题

数据存储在APP开发中起着至关重要的作用。相信各位开发者在开发过程中都有碰到如下情况:

  • 需要一些全局变量,来记录APP的一些设置或者是频繁变动的数据
  • 页面之间或者各种View之间的传值,
  • 需要临时缓存一些数据。

这些数据存储的处理在开发过程极为常见,而且有一个共同点就是处理的各种数据类型完全不一样,有时是各种数字(NSNumber),也有很常用的字符串,当然各种数组或者字典也是不可少的。
所以由此带来的一个问题就是这些数据需要以什么的格式保存,保存后取出来又要如何转化成原先的数据类型,这些都是要手写代码去处理。前面已经说过,泛型正好是解决此类问题而生。但在这里并不是使用泛型数据容器,而是使用泛型类。那么怎么使用泛型类来写一个通用的数据存储框架,可以解决以上问题呢?

首先iOS的数据存储离不开iOS存储本身的机制,也就是那如下几种:

  • Plist
  • 归档&NSUserDefault
  • SQLite3
  • CoreData

关于这些数据持久化的介绍和使用方式网络上有很多的文章讲解,在这里我就不详述了。而且目前市面上还有不少对这些数据持久化API的封装开源库,比如说著名的FMDB,但是都没有完全解决上面的问题。我们需要一个轻量级,可以在代码文件的任何位置读写,支持缓存,临时存储(APP退出后数据丢失)和数据发生变化时的监视,同时不需要做数据类型的转换且可以保存任何类型数据的数据存储方案。而Swift泛型的出现,使得这种存储方案成为可能。

使用泛型类来存储数据

和泛型数据容器相比,自定义泛型类用得并不多。可能许多初级开发者不好理解泛型类,更不明白泛型类有什么作用。关于这个,可以去参考Swift的Array或者是NSDictionry的定义,并且练下手会更好一点。下面我们来定义一个泛型类

public class GrandStore<T> {            //在类名后面加个<T>表示这是一个泛型类
    private var name:String!            //这个变量极为重要,相当于是一个Key,表示这个存储的名称。
    private var value:T?                //私有内部存储值,是个泛型字段
    private var defaultValue:T?         //默认值,也是个泛型字段
    private var hasValue:Bool = false   //Bool类型,用来判断有没有值
    private var timeout:Int = 0         //缓存时间,如果你设定一个带缓存的存储数据,需要设定这个值
    private var storeLevel:Int = 0      //存储等级,用来判断保存在什么地方
    private var isTemp = false          //如果设为true ,那么只是放到内存里临时保存       
    private var timeoutDate:NSDate?     //过期的时间,用于带缓存的存储
}

上面就是这个泛型存储类的全部私有字段。各有什么作用注释里都有说明,下面上它的两个构造器

   public  init(name:String,defaultValue:T) {    //最常用的构造器,适用于任何需要永久保存的数据
        self.name = name;
        self.defaultValue = defaultValue;
        storeLevel = self.getStoreLevel()       //获取存储级别,
    }
   
 
  public  init(name:String,defaultValue:T,timeout:Int) {  //如果你要保存带缓存的数据,需要调用这个构造器。timeout表示你需要缓存的时间,单位是秒
        self.name = name;
        self.defaultValue = defaultValue;
        self.timeout = timeout
        if self.timeout > 0{
            timeoutDate = NSDate(timeIntervalSinceNow: Double(self.timeout))
        }
        else{                                            //如果timeout<=0话,那么就是临时存储,只保存的内存里面,APP退出后丢失
            isTemp = true
        }
        storeLevel = self.getStoreLevel()               //获取存储级别,
    }

上面是泛型类的两个构造器。因为其参数是T类型,也就是泛型,那么就可以往里在传任何类型。同时这是一个默认值,如果你没有设定值的话,那也是可以取出值的,就是这个默认值。
后面的构造器就是带缓存有的了。如果你传的缓存时间大于0,那么这个时间(秒为单位)就是缓存的时间,如果小于等于0,那就这是一个临时存储。它不会写到硬盘里面,只保存在内存里。
下面上这个泛型类最核心的属性,

    
 public  var Value:T?
 {
     get{
        if isExpire{                                    //判断有没有过期
            self.clear()                                //过期了就清空,再将是否有值设成false
            hasValue = false
        }
        if!hasValue                                     //如果没有值,也就是说过期了或者内存中没有
        {
            if isTemp{                                  //如果是临时保存的,直接将其设成默认值
                if self.value == nil{
                    self.value = defaultValue         
                }
            }
            else{                               
               if !store.hasValue(name){                //判断存储仓库里有没有保存
                    self.value = defaultValue           //如果没有保存,就设成默认值再保存,
                    store.setValue(self.value)forkey(name)
               }
               else{
                    self.value = store.setValueForKey(name) as? T//有的话直接取出来
               }
                
            }
            hasValue = true                             //将是否有值设成true
        }
        return self.value                               //返回取出来的值

    }
    set{
        self.value = newValue
        if !isTemp{
            store.setValue(self.value)forkey(name)      //设值就比较简单了,直接保存就行了、
        }
        hasValue = true
    }
}

这里我用了伪代码,同时省略了一些功能。因为原代码比较长也有点复杂,这里有几个关键点要说明下

  1. 最上面的isExpire是个计算属性,用来判断保存的数据有没有过期,如果是永久存在的,就直接返回false,如果不是,那么根据timeoutDate来判断有没有过期
    private var isExpire:Bool{
        get{
            if timeoutDate == nil{                          //对于永久保存有,直接返回false
                return false
            }
            else{
                return NSDate().compare(timeoutDate!) == NSComparisonResult.OrderedDescending   //对于有缓存的,根据timeoutDate判断有没有过期
            }
        }
    }

如果已经过期,那么需要清空数据,再将hasValue设成false。

  1. 如果hasValue是false,也就是说内存中不存在该值,那么就需要到存储仓库去看了。这里面我省略了一些代码。总体思路如下:
    先判断存储仓库有没有,如果没有,就直接设置成默认值,再保存到存储仓库里。
    如果存在,就从存储仓库里取出,最后再将hasValue设置成true。
    在这里存储仓库是指另一个对iOS数据持久化封装的库,你可以用Github常用的开源库,也可以自己写。
  2. 其实对于取出来的数据上需要转换的,由Object转换成T类型就行了。
  3. 对于设值,首先判断是不是临时保存的,如果不是,那么需要将其保存到存储仓库,最后再将hasValue设成true就行了。

上面就是这个泛型类最核心的功能的代码实现。这里面其实是对最常见的valueForKey和setValueForKey二次封装,并且再将数据再转换成T类型。因为在用构造器实例化这个类时,
它就会根据传入的默认值得知保存的数据是什么类型的。所以便可以正确地转换成原先的类型。

至于这些细节实现代码,比如数据值变化时的监视,每次设定值时更新缓存过期的时间,怎么将数据保存到存储仓库里,以及清空功能等,读者有兴趣可以去看源码GrandStore。 我在里面是采用归档的形式来将保存各种数据的。并且在我做的所有项目里都用了这个库。不过需要注意的是,因为Objective-C没有泛型支持,所以目前GrandStore可能不支持Objective-C。只能用到Swift环境中。如果觉得不错,请给个Star哦!

使用GrandStore

使用GrandStore十分简单,可以直接写在全局变量里面。所以项目里的任何文件都可以直接访问。
比如你可以用一个值来记录APP是不是第一次开启

let AppFirstLaunch = GrandStore(name: "AppFirstLaunch", defaultValue: true) //默认为true,说明App是第一次开启
//一但app有启用过,那么将其设成false就行了,
AppFirstLaunch.Value = false
//判断APP是不是第一次启动
if AppFirstLaunch.Value!{
//第一次启动,在里面写一些处理的代码。
}

怎么样,用这个来存储一些全局的变量可以是说十分的方便,无论是读取还是写入,都不需要特定的方法,也不需要进行数据类型转换,直接使用就行。
同样的,还可以用它来保存其他的任何类型,比如:

let student = GrandStore(name: "student", defaultValue: Student()) //保存自定义类型
class Student:GrandModel{                                      //这里注意,我让Student继承了GrandModel,因为在我写的GrandStroe中,我是用归档来保存对象的
    var name:String?                                           
    var id:Int = 0
}
let stu = Student()
stu.name = "张三"
stu.id = 1
student.Value = stu              //新建一个Student对象再赋值给GrandStore
let stu1 = student.Value!       //取出来直接就是Student对象,可以直接操作
stu1.name = "李四"
student.Value = stu1            //修改好的值可以再赋回去实现永久保存

因为我是用归档来实现上面所说的存储仓库的。所以如果我想用GrandStore来保存自定义对象,那么要让它实现NSCoding协议,我在前面写了一系列的文章打造强大的BaseModel中有实现自动归档的GrandModel,所以可以在这里直接使用。
无论是数组还是字典都毫无压力

 let arrTest = GrandStore(name: "arrTest", defaultValue: [String]()) //保存数据
 let dictTest = GrandStore(name: "dictTest", defaultValue: [String:String]())  //保存字典 
 var demoCache = GrandStore(name: "CacheTest", defaultValue: "", timeout: 10) //10秒缓存时间的一个字符串
  var demoTemp = GrandStore(name: "demoTemp", defaultValue: "temp", timeout: 0)//只保存在内存里的一个字符串

更多的使用示例请参考我的Github,上面有链接

总结

Swift泛型的出现弥补了Objective-C没有泛型的去缺陷,更是带来了更多现代编程语言诸多的便捷特性。使用Swift的泛型,我们有了一个最好的强类型编程模式。在此基础上,泛型方法,闭包,高阶函数
等有了更好的用武之地,极大的提升了开发效率。我在此建议会Objective-C的开发者尝试去学习Swift,掌握它,你会发现另一片天地。

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 1. 他和她的第一次相遇,是在苏镇的那条小河边。 那天天气晴朗,碧空万里。初秋的傍晚,红色的云霞洒落到小河上面,闪...
    木庭兮阅读 402评论 5 3
  • 上海之行,不虚此行,培训师职业生涯的指路明灯。 作为新入职的培训师,迷茫过,迟疑过,徘徊过,沮丧过,甚至想过放弃。...
    芳宇自然堂培训师小玉阅读 468评论 1 0
  • 简单的数据类型Undefined,Null,Boolean,Number,String。 检测基本数据类型的typ...
    Random_阅读 272评论 0 0
  • 战士通用仍然是百穿铭文,至于蓝色铭文的狩猎和隐匿怎么选择,还是看大家个人使用习惯,到底是喜欢多一点攻速,还是多一点...
    渔夫哥阅读 771评论 3 1