010-YapDatabase 食用指南

YapDatabase 是一个工作在 iOS 和 MAC 上的数据库,有两大主要特性:

  • 基于 sqlite 建立的 collection/key/value/metadata 基础存储功能
  • 提供类似视图、辅助索引、全文搜索等高级功能的插件

同时还有以下特性

  • 并发性:可以同时在多个连接上进行读写操作,不会阻塞主线程
  • 内建缓存:相比于 sqlite 的原始字节缓存,YapDatabase 的缓存可以跳过序列化和反序列化阶段
  • 集合:支持集合存储
  • 元数据 metadata:支持元数据存储,可以存储一些 object 的额外信息,比如时间戳等
  • 性能:在主线程获取数千对象不会掉帧
  • objectivec API
  • 拓展:内置拓展架构,同时支持自定义
  • 视图:YapDatabase 内置的 view 使得过滤、组合和排序数据非常便捷
  • 辅助索引:通过索引重要属性来加快搜索速度
  • 全文搜索:基于 sqlite 的 FTS 模块,可以以最小代价获得极速的搜索

并发行

YapDatabase 中的只读连接会保存数据库的即时快照,即使其他连接改变数据,也不会影响当前的连接。但必须遵循以下规则:

  • 可以同时建立多个连接 Connection
  • 每一个连接都是线程安全的
  • 可以同时拥有多个只读事务而不阻塞
  • 可以同时拥有多个只读事务和一个读写事务而不阻塞
  • 对于每一个数据库,同一时间只能有一个读写事务,有唯一一个串行duilie执行读写事务
  • 对于每一个连接,同一时间只能有一个事务,每一个连接都维护一个串行队列执行事务

存储

YapDatabase 支持任何类型的 object,只要设置好序列化和反序列化流程就可以,YapDatabase 有提供默认的序列化和反序列化流程,当然也支持自定义。对于支持了 NSCoding 协议的类,可以不需要额外的设置,比如 Cocoa 中的大多数内置类

  • NSString
  • NSNumber
  • NSArray
  • NSDictionary
  • NSSet
  • NSData
  • UIColor
  • UIImage

对于自定义类,只需要实现 NSCoding 的序列化、反序列化方法就可以了。

@interface Person : NSObject<NSCoding>

@property(strong, nonatomic) NSString *name;
@property(strong, nonatomic) NSString *gender;
@property(assign, nonatomic) NSInteger age;

@end

@implementation Person

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if ([super init])
    {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.gender = [aDecoder decodeObjectForKey:@"gender"];
        self.age = [aDecoder decodeIntegerForKey:@"age"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.gender forKey:@"gender"];
    [aCoder encodeInteger:self.age forKey:@"age"];
}

@end

如果序列化类的某个属性也是支持 NSCoding 的类,则也可以直接存进数据库,同样的,对于一个数组,序列化和反序列化信息也会依次发送给每一个成员。

YapDatabase 包含一些开箱即用的序列化函数

/**
默认的序列化/反序列化函数,实现了 NSCoding 协议,任何支持 NSCoding 协议的对象(包括系统自带的大多数类)都可以被序列化/反序列化。
**/
+ (YapDatabaseSerializer)defaultSerializer;
+ (YapDatabaseDeserializer)defaultDeserializer;

/**
属性列表序列化/反序列化函数,只支持 SData, NSString, NSArray, NSDictionary, NSDate, NSNumber 这些类,采取了一些优化措施
**/
+ (YapDatabaseSerializer)propertyListSerializer;
+ (YapDatabaseDeserializer)propertyListDeserializer;

/**
一个针对 NSDate 对象的快速序列化/反序列化函数
**/
+ (YapDatabaseSerializer)timestampSerializer;
+ (YapDatabaseDeserializer)timestampDeserializer;

自定义序列化/反序列化函数的方法可以用于加密、压缩和性能优化等方面。

typedef NSData* (^YapDatabaseSerializer)(NSString *collection, NSString *key, id object);
typedef id (^YapDatabaseDeserializer)(NSString *collection, NSString *key, NSData *data);

集合

集合为众多拥有同一关键属性的元素提供了更方拜年的存储结构,它为元素提供除了 key 值外另一个层次的标识,所以 YapDatabase 也可以理解为是 “字典的字典”。

YapDatabase 关于集合的 API 有以下几个

/**
 * Returns the total number of collections.
 * Each collection may have 1 or more key/object pairs.
**/
- (NSUInteger)numberOfCollections;

/**
 * Returns the total number of keys in the given collection.
 * Returns zero if the collection doesn't exist (or all key/object pairs from the collection have been removed).
**/
- (NSUInteger)numberOfKeysInCollection:(NSString *)collection;

/**
 * Object access.
 * Objects are automatically deserialized using database's configured deserializer.
**/
- (id)objectForKey:(NSString *)key inCollection:(NSString *)collection;

/**
 * Fast enumeration over all keys in the given collection.
 *
 * This uses a "SELECT key FROM database WHERE collection = ?" operation,
 * and then steps over the results invoking the given block handler.
**/
- (void)enumerateKeysInCollection:(NSString *)collection
                       usingBlock:(void (^)(NSString *key, BOOL *stop))block;

可以看到一个 object 在 YapDatabase 中是依靠 collection 和 key 两个标识共同唯一确定的。

缓存

YapDatabase 的每一个连接都有自己专属的数据库缓存,与 sqlite 的二进制数据缓存层相比,YapDatabase 的缓存层直接缓存 objectivec 对象,减少了序列化和反序列化的开销。

缓存默认打开,大小为 250,以下是 API

// 这一属性可以选择关闭或开启缓存
@property (atomic, assign, readwrite) BOOL objectCacheEnabled;
@property (atomic, assign, readwrite) BOOL metadataCacheEnabled;

// 可以设置缓存大小,如果赋值为 0 则缓存空间无限
@property (atomic, assign, readwrite) NSUInteger objectCacheLimit;
@property (atomic, assign, readwrite) NSUInteger metadataCacheLimit;

支持对象与元数据分开缓存,也支持在运行中进行缓存大小的修改,每一个连接的缓存都会自动与数据库进行同步。

元数据 Metedata

YapDatabase 支持存储的元祖不仅仅包含对象,还有元数据,底层的数据库表也为元数据开辟了独立的存储列。对象是必须有的,但是元数据是可选的,如果对象为 nil 则元组会被移除,但是元数据为 nil 并不会。元数据与对象可以拥有独立的缓存机制和序列化/反序列化函数。

元数据相关的存储与更新 API 有以下这些

/**
 * Invokes setObject:forKey:inCollection:withMetadata:,
 * and passes a nil value for the metadata parameter.
**/
- (void)setObject:(nullable id)object
           forKey:(NSString *)key
     inCollection:(nullable NSString *)collection;

/**
 * If you call this method with a nil object, then it will delete the row.
 * (Equivalent to calling removeObjectForKey:inCollection:)
 * 
 * Otherwise, this method inserts/updates the row,
 * and sets BOTH the object & metadata columns to the given values.
**/
- (void)setObject:(nullable id)object
           forKey:(NSString *)key
     inCollection:(nullable NSString *)collection
     withMetadata:(nullable id)metadata;

/**
 * If a row with the given collection/key already exists,
 * then this method updates ONLY the object value.
 * The metadata value for the row isn't touched. (It remains whatever it was before.)
 * 
 * Again, it's not possible to have a nil object for a row.
 * So if you try to set the object to nil, this is just going to delete the row.
**/
- (void)replaceObject:(nullable id)object
               forKey:(NSString *)key
         inCollection:(nullable NSString *)collection;

/**
 * If a row with the given collection/key already exists,
 * then this method updates ONLY the metadata value.
 * The object value for the row isn't touched. (It remains whatever it was before.)
**/
- (void)replaceMetadata:(nullable id)metadata
                 forKey:(NSString *)key
           inCollection:(nullable NSString *)collection;

最佳实践

  • 避免用同一个连接在主线程和其他线程同时执行事务,因为连接只能串行执行事务
  • 避免创建太多连接,会带来开销问题,同时会降低缓存命中,因为连接中执行的事务少,缓存性能没有表现出来
  • 为主线程使用专用的连接
  • 在主线程的专用连接上不执行任何读写事务(ReadWrite transaction),只执行只读事务
  • 为读写操作建立单独的连接

相关 API 与操作

1. 删除

  • 删除指定集合的所有元素

    - (void)removeAllObjectsInCollection:(NSString *)collection
    
  • 删除数据库所有元素

    - (void)removeAllObjectsInAllCollections;
    
  • 删除某一个指定元素

    - (void)removeObjectForKey:(NSString *)key inCollection:(nullable NSString *)collection;
    
  • 删除某一些指定元素

    - (void)removeObjectsForKeys:(NSArray<NSString *> *)keys inCollection:(nullable NSString *)collection;
    

2. 获取总数

  • 获取集合总数

    - (NSUInteger)numberOfCollections;
    
  • 获取集合中 key 总数

    - (NSUInteger)numberOfKeysInCollection:(nullable NSString *)collection;
    
  • 获取数据库中 key 总数

    - (NSUInteger)numberOfKeysInAllCollections;
    

3. 获取列表

  • 获取所有集合的列表

    - (NSArray<NSString *> *)allCollections;
    
  • 获取某个集合中所有 key 的列表

    - (NSArray<NSString *> *)allKeysInCollection:(nullable NSString *)collection;
    

4. 获取对象与元数据

  • 获取指定对象

    - (nullable id)objectForKey:(NSString *)key inCollection:(nullable NSString *)collection;
    
  • 检测指定对象是否在集合中

    - (BOOL)hasObjectForKey:(NSString *)key inCollection:(nullable NSString *)collection;
    
  • 获取指定对象和元数据到指定地址,存在该 key 则返回 YES,否则返回 NO

    - (BOOL)getObject:(__nullable id * __nullable)objectPtr
             metadata:(__nullable id * __nullable)metadataPtr
               forKey:(NSString *)key
         inCollection:(nullable NSString *)collection;
    
  • 获取指定元数据

    - (nullable id)metadataForKey:(NSString *)key inCollection:(nullable NSString *)collection;
    

5. 获取原始数据

下面的方法会跳过 oc 层缓存,直接获取数据库中的原始数据,因此速度不如上面的方法。

  • 获取指定对象

    - (nullable NSData *)serializedObjectForKey:(NSString *)key inCollection:(nullable NSString *)collection;
    
  • 获取指定元数据

    - (nullable NSData *)serializedMetadataForKey:(NSString *)key inCollection:(nullable NSString *)collection;
    
  • 读取指定对象和元数据到指定地址

- (BOOL)getSerializedObject:(NSData * __nullable * __nullable)serializedObjectPtr
        serializedMetadata:(NSData * __nullable * __nullable)serializedMetadataPtr
                    forKey:(NSString *)key
              inCollection:(nullable NSString *)collection;

6. 枚举

  • 枚举集合

    - (void)enumerateCollectionsUsingBlock:(void (^)(NSString *collection, BOOL *stop))block;
    - (void)enumerateCollectionsForKey:(NSString *)key usingBlock:(void (^)(NSString *collection, BOOL *stop))block;
    
  • 枚举key

    - (void)enumerateKeysInCollection:(nullable NSString *)collection
                         usingBlock:(void (^)(NSString *key, BOOL *stop))block;
    - (void)enumerateKeysInAllCollectionsUsingBlock:(void (^)(NSString *collection, NSString *key, BOOL *stop))block;
    - (void)enumerateKeysAndObjectsInCollection:(nullable NSString *)collection
                                   usingBlock:(void (^)(NSString *key, id object, BOOL *stop))block;
    - (void)enumerateKeysAndMetadataInCollection:(nullable NSString *)collection
                                    usingBlock:(void (^)(NSString *key, __nullable id metadata, BOOL *stop))block;
    

7. 存储和更新

  • 存储

    - (void)setObject:(nullable id)object forKey:(NSString *)key inCollection:(nullable NSString *)collection;
    - (void)setObject:(nullable id)object
             forKey:(NSString *)key
       inCollection:(nullable NSString *)collection
       withMetadata:(nullable id)metadata;
    
  • 更新

    - (void)replaceObject:(nullable id)object forKey:(NSString *)key inCollection:(nullable NSString *)collection;
    - (void)replaceMetadata:(nullable id)metadata forKey:(NSString *)key inCollection:(nullable NSString *)collection;
    

这里要注意如果存储的对象所属 key 是已经存储在数据库的,则会自动更新这个元素,如果传递的对象是 nil,则会移除这个元素。

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

推荐阅读更多精彩内容