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

iOS开发技巧系列---使用泛型类来实现数据持久化_第1张图片

以前在用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,
    ""
    )
    2016-03-31 15:29:28.438 DemoObjc[1108:32945] a:1 b:111.dm:
    2016-03-31 15:29:28.438 DemoObjc[1108:32945] //虽然在代码阶段是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* arrString  = [NSMutableArray new]; //可以在NSMutableArray后面加上数据类型,这样就声明了一个泛型的NSMutableArray

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

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


iOS开发技巧系列---使用泛型类来实现数据持久化_第2张图片

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


iOS开发技巧系列---使用泛型类来实现数据持久化_第3张图片

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

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

iOS数据持久化方案的问题

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

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

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

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

  • Plist
  • 归档&NSUserDefault
  • SQLite3
  • CoreData

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

使用泛型类来存储数据

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

public class GrandStore {            //在类名后面加个表示这是一个泛型类
    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,掌握它,你会发现另一片天地。

你可能感兴趣的:(iOS开发技巧系列---使用泛型类来实现数据持久化)