YYStorage学习笔记
YYKVStorage:在YYDiskCache中,主要使用YYKVStorage,因此,我们先看下YYKVStorage;
YYKVStorage:非线程安全的,如果我们强行要在多个线程中访问同一个数据,你需要保证数据安全;
YY:Typically, you should not use this class directly.
-
YYKVStorage属性
- path: storage的full path;
- type: 存储类型,是file or SQLite or mixed
- errorLogsEnabled: 是否需要输出(错误)日志
-
存储元数据:YYKVStorageItem的属性
- key:存储数据的key值,根据key值可以找到value;
- value:存储的数据(NSData);
- filename:当存储类型为file的时候不可为空;
- size:value的bytes
- modTime:修改的时间戳;
- accessTime:上次访问的时间戳
- extendedData:扩展数据(NSData)
-
YYKVStorage外部接口
生命周期方法
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type;
可失败的初始化方法,path不存在则创建,存在则读取目录下的数据;
在path目录下创建dbPath,trashPath;
创建后台清空垃圾箱的串行队列_trashQueue;
后台清空一次垃圾箱;
- (void)dealloc
生成后台运行的UIBackgroundTaskIdentifier:taskID,关闭db,关闭完毕之后结束taskID对应的后台进程
Update
- (BOOL)saveItem:(YYKVStorageItem *)item;
保存或者更新存储的数据;
type为YYKVStorageTypeMixed,当item.filename为空时才会使用SQLite存储数据,否则使用文件存储;
type为YYKVStorageTypeSQLite,item.filename会被忽略;
type为YYKVStorageTypeFile,item.filename不可为空或长度为0;
只会保存itme的这些属性item.key, item.value, item.filename and
item.extendedData,其他的属性将会被忽略;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
只有当type为YYKVStorageTypeSQLite或者YYKVStorageTypeMixed时生效,如果是YYKVStorageTypeFile,返回NO;
如果filename不为空,则在filename目录下写入value数据;并在db中存储此数据(key,value,filename,extendedData);若存储失败,则删除此目录下的文件;
如果filename为空,则在db中查找此key对用的数据的filename,若找到了,则删除filename中的数据,然后在db中存储参数中的数据;
结果返回是否存储成功;
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;
同saveItem类似
Delete
- (BOOL)removeItemForKey:(NSString *)key;
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;
删除指定key的数据;
首先判断是否是db存储,如果是的,直接删除db中对应key的数据并返回;
如果是file存储,进行下一步;
如果是mix存储,找到db中的key对应的数据中filename字段,并删除filename里面的数据,最后再删掉数据库中此key对应的数据;
- (BOOL)removeItemsLargerThanSize:(int)size;
遍历所有数据,当item的value的大小超过指定的size的时候,remove掉;
- (BOOL)removeItemsEarlierThanTime:(int)time;
遍历所有数据,删除所有item中早于time时间戳的数据;
- (BOOL)removeItemsToFitSize:(int)maxSize;
如果maxSize <= 0;则直接removeAllItems;
如果通过db获取的itemSIze < = maxSize则直接返回YES;
通过两个循环来逐步删除数据以达到满足total <= max的要求
外层循环定义每次从db中查找数据的基数perCount为16;从db中取出最早的16条数据(即升序找出time最大的16条数据生成的YYKVStorageItem);
内存循环做删除操作,若totla <= max了,则break;否则通过item的filename,删除文件夹中的数据和db中的数据;并将totalSize - item.size;若在在db中出差了,则break,并返回NO;
在外层循环中,每次delete了16条数据后,就会检查一次total;若total < max或者所有数据都删完了,或者删除数据库数据失败时,退出外层循环;
最后外层循环也走完了之后,db都正常删除后,则检查point,即将sqlite-wal文件合并到sqlite文件中;
最后返回suc(db删除是否正常的标志量);
- (BOOL)removeItemsToFitCount:(int)maxCount;
同removeItemsToFitSize,根据YYKVStorageItem中的time更新storage;
- (BOOL)removeAllItems;
调用_reset
将所有的files and sqlite db 移除到垃圾箱,然后在background queue中做清除工作;
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
直接将所有数据删除,切记不可在block中访问storage;(速度比removeAllItems要慢)
perCount设置为32同样通过_dbGetItemSizeInfoOrderByTimeAscWithLimit
方法找到前32位的数据,每删除32个之后会调用一次progress,结束后会调用一次end
Find
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
取key 对应的item。并更新key的accessTime;
如果可以正常从db中获取到数据(会生成YYKVStorageItem返回),那么判断filename,不为空则将value写入filename目录下,如果item的value为空,则删除此key对应的条目,并返回item
- (nullable YYKVStorageItem *)getItemInfoForKey:(NSString *)key;
取key 对应的item,但是返回的item的value属性会被忽略;\
- (nullable NSData *)getItemValueForKey:(NSString *)key;
返回key 对应的item的value;
如果是File存储类型,则直接读取目录下的数据,为空则删除数据库中key对应的条目;
如果是sqlite,则直接从db中获取条目对应的value;
如果是mix,则从db中找出key对应的filename,然后从filename中读取数据,为空则删除key对应的条目;如果没有filename,则直接取出db里面的value;
最后更新key对应的条目的accessTime并返回value;
- (nullable NSArray<YYKVStorageItem *> *)getItemInfoForKeys:(NSArray<NSString *> *)keys;
首先根据keys从db中找到对应的数据(YYKVStorageItem数组items);遍历数组,若item有filename则读取filename的数据,读取的数据为空则删除db中此条目,并在items中remove此index;
更新db中对应keys的accessTime;
返回items;
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;
返回由item的key,value组成的dicionary;
Select
- (BOOL)itemExistsForKey:(NSString *)key;
是否存在key对应的item
- (int)getItemsCount;
返回item的count,若出错,返回-1;
- (int)getItemsSize;
返回所有item的value的total size,若出错,返回-1;
- 具体实现
.m文件里面有定义一些static const数据,列出如下:
* kMaxErrorRetryCount:最大重试次数;
* kMinRetryTimeInterval:最小重试时间间隔;
* kPathLengthMax: 最大路径长度;
* kDBFileName: DB的名字;
* kDBShmFileName: sqlite中间文件.db_shm文件的名字;
* kDBWalFileName: sqlite中间文件.db_wal文件的名字;
* kDataDirectoryName: 存放数据的文件夹名字;
* kTrashDirectoryName: 存放垃圾数据的文件夹名字;
静态函数:
* _YYSharedApplication:若在app extension中返回nil,否则返回application;
私有成员:
*_path:
*_dbPath:db存放的path;
*_dataPath:value存放的路径;
*_trashPath:垃圾箱路径;
*_db:sqlite3对象;
*_dbStmtCache: db接口的缓存,类型为CFMutableDictionaryRef;(stmt:用于执行一个静态SQL语句的对象并返回它产生的结果)
*_dbLastOpenErrorTime:上次打开数据库失败的时间;
*_dbOpenErrorCount:打开数据库失败的次数;
- sqlite相关知识
在这里我想先跟同学们讲下sqlite相关的知识,如果你已经了解了这部分的知识,可以直接跳过去看YYKVStorage的其他方法实现了;
struct sqlite3_stmt:
stmt结构体表示一条已经被编译为二进制并且准备要执行的单条SQL语句;原始的SQL语句作为源代码供上层方便使用,而stmt则是已经编译好的代码,所有的SQL语句都必须先转为一个prepared statement,也就是stmt后才可以被执行;
一个stmt的生命周期如下:
a.使用sqlite3_prepare_v2()函数创建stmt object;
b.使用sqlite3_bind_*()给stmt的参数绑定一个值;
c.使用sqlite3_step()函数运行SQL stmt;
d.使用sqlite3_reset()重置stmt,并使用b步骤继续绑定其他数据;
e.使用sqlite3_finalize()去销毁这个stmt
*sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt);
当pStmt已经连接到数据库之后,返回指向下一个准备执行的stmt的指针;
如果pStmt为NULL,则返回指向第一个执行的stmt的指针;如果没有这样指针,则返回NULL;
注意:参数pDb必须是一个已经打开的db,并且绝对不能是一个空指针;
SQLITE_API int sqlite3_wal_checkpoint(sqlite3 *db, const char *zDb);
这个函数的作用就是将连接在数据库db
上的数据库zDb
中的预写日志内容传输到数据库文件中,并重置预写日志;即将sqlite-wal
文件合并到sqlite
文件中;
这个函数其实是一个老接口了,sqlite后面又增加了更强大的sqlite3_wal_checkpoint_v2()函数,保留此接口是为了向后兼容。
这个函数等同于函数sqlite3_wal_checkpoint_v2
;
SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
重置stmt到初始状态,并准备重新执行,这个函数不会改变stmt的绑定的value,除非你用sqlite3_clear_bindings()
去重置绑定;
如果最近的一个sql的执行sqlite3_step(S)
返回的是SQLITE_ROW
或者SQLITE_DONE
,或者说这个stmt
还未执行sqlite3_step
,则此函数会返回SQLITE_OK
;
如果出现错误信息,改函数会返回error code
;
sSQLITE_API int sqlite3_prepare_v2(
sqlite3 *db, /* Database handle */
const char *zSql, /* SQL statement, UTF-8 encoded */
int nByte, /* Maximum length of zSql in bytes. */
sqlite3_stmt **ppStmt, /* OUT: Statement handle */
const char **pzTail /* OUT: Pointer to unused portion of zSql */
);
这个函数创建一个ppstmt,返回值表明是否创建成功;
SQLITE_API const void *sqlite3_errmsg16(sqlite3*);
返回在执行_db时的错误信息,返回UTF-16格式的message
补充:C语言知识,通过一个指针跟0比较,可以判断这个指针是否为空指针;
- 私有方法
- (BOOL)_dbOpen;
如果_db
存在,则直接返回,否则使用函数sqlite3_open
初始化_db
如果初始化成功,则创建两个结构体CFDictionaryKeyCallBacks
和CFDictionaryValueCallBacks
,再使用这两个结构体去初始化_dbStmtCache
如果失败,则释放相应的内存,并将_dbOpenErrorCount++
;
注
- CFDictionaryKeyCallBacks是为了保证toll-free时可以对建进行拷贝操作,而不是retain,这样就可以保证key值是不变的;
- CFDictionaryValueCallBacks valueCallbacks = {0};可以使用{0}来初始化结构体;
- (BOOL)_dbClose;
如果_db
已经关闭,直接返回;
否则执行以下步骤:
- 初始化如下变量做标志位:int result = 0;BOOL retry = NO;BOOL stmtFinalized = NO,分别表示最终结果,是否重试和stmt是否被析构;
-
release
_dbStmtCache
; - 通过一个do-while循环来尝试关闭
_db
a.关闭_db
:sqlite3_close(_db)
;
b.如果sqlite返回busy或者是locked状态,执行c去手动销毁stmt,否则执行e;
c.如果stmt没有析构,则进行d去析构stmt;
d.使用while循环调用sqlite3_next_stmt
函数去遍历每个stmt,并将他们销毁;
e.如果正常关闭了则根据_errorLogsEnabled
的值去输出log日志;
- (BOOL)_dbInitialize;
sqlite的初始化,sql语句包括:创建配置文件wal,normal,建表manifest和创建表的索引last_access_time_idx
,调用_dbExecute
方法去执行sql语句;
wal是一种日志模式,同步机制选择的是normal(即在关键的磁盘操作的每个序列后同步)
- (BOOL)_dbExecute:(NSString *)sql;
如果[self _dbCheck]
返回NO,则直接返回NO;
否则,使用函数sqlite3_exec
执行sql语句,如果执行出现错误,要记得free error指针;
有的同学可能会认为_dbCheck
,_dbInitialize
,_dbExecute
会造成循环调用,但是_dbCheck方法执行_dbInitialize
时会先判断_dbOpenErrorCount
和_dbLastOpenErrorTime
,这两个值会在_dbOpen
方法里面有更新操作,所以不会一直操作循环调用,超过限制后就不会调用了;
- (BOOL)_dbCheck;
检测_db
的状态;
如果_db
已经打开,直接返回YES;
否则判断_dbOpenErrorCount
和_dbLastOpenErrorTime
两个成员变量是否超过了定义好的值kMaxErrorRetryCount
和kMinRetryTimeInterval
;如果超过,返回NO,否则返回[self _dbOpen]
&& [self _dbInitialize]
,即_db
是否正常打开并且正常初始化了;
- (void)_dbCheckpoint;
sqlite的检查点出现的时候,使用函数sqlite3_wal_checkpoint将sqlite_wal文件合并到sqlite文件中;
- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql;
1.首先检查_db
的状态以及参数sql
和_dbStmtCache
2.尝试从_dbStmtCache
中读取key:sql对应的value,即sqlite3_stmt
*stmt;
a、如果没有对应的stmt,则使用函数sqlite3_prepare_v2
创建该stmt;并将此stmt文件保存在_dbStmtCache
中,
b、如果有对应的stmt,则重置stmt为可执行状态;
3.返回这个可以执行的stmt
- (NSString *)_dbJoinedKeys:(NSArray *)keys
用来生成sql语句用的主要生成?,?,?,的字符串;
- (void)_dbBindJoinedKeys:(NSArray *)keys stmt:(sqlite3_stmt *)stmt fromIndex:(int)index;
keys中保存的是sql语句,
index指的是设置的SQL parameter index;
使用sqlite3_bind_text
将keys中的sql语句与stmt绑定起来;
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData;
创建sql语句为@"insert or replace into manifest (key, filename, size, inline_data
, modification_time
, last_access_time
, extended_data
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);"的stmt,然后将方法中各个参数的值与对应sql parameter index绑定起来,然后使用sqlite3_step
执行stmt;
- (BOOL)_dbUpdateAccessTimeWithKey:(NSString *)key;
更新对应key的sql中的最后一次访问时间;过程同上一个方法差不多,也是先用方法_dbPrepareStmt
去取,然后重新绑定(因为由_dbPrepareStmt
得来的stmt要么是新建的,要么是reset了的,因此可以直接绑定);
这个方法YY没有一开始检测_db
的状态,很奇怪
- (BOOL)_dbUpdateAccessTimeWithKeys:(NSArray *)keys;
更新keys对应的数据
- 检测
_db
的状态; - 通过
_dbJoinedKeys
方法生成update 的sql语句; - 通过
_dbBindJoinedKeys:stmt:fromIndex:
方法绑定对应的key值; - 执行stmt
sqlite3_step
; - 销毁
stmt:sqlite3_finalize
; - 执行成功返回YES,否则返回NO;
- (BOOL)_dbDeleteItemWithKey:(NSString *)key
删除key对应的数据,过程同update
- (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys
同- (BOOL)_dbUpdateAccessTimeWithKeys:(NSArray *)keys;
- (BOOL)_dbDeleteItemsWithSizeLargerThan:(int)size
删除size大于指定size的数据;
- (BOOL)_dbDeleteItemsWithTimeEarlierThan:(int)time
删除add的时间早于指定时间戳的数据;
- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData
通过sqlite3_column_
函数系列从数据库中取出stmt对应的数据,然后生成一个YYKVStorageItem对象后,返回;如果excludeInlineData为NO,则将inline_data
(即YYKVStorageItem的value属性)也返回,反之返回的value为空;
这其实是一个中间函数,因为肯定需要其他函数写了sql语句之后,生成了stmt之后才可以去调用此方法之后生成item的,比如下面这个方法;
- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData
使用_dbPrepareStmt
方法,通过给定的sql语句生成stmt,并通过上面的方法- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData
生成item返回;
- (NSMutableArray *)_dbGetItemWithKeys:(NSArray *)keys excludeInlineData:(BOOL)excludeInlineData
通过keys找到生成sql语句,然后将keys绑定到stmt上,再通过一个do-while的循环来将数据逐条取出,使用上面那个方法得到items,返回;
- (NSData *)_dbGetValueWithKey:(NSString *)key
这个方法就是通过key取出数据库中的inline_data
数据并返回;
- (NSString *)_dbGetFilenameWithKey:(NSString *)key
通过key取出数据库中的filename字段并返回;
- (NSMutableArray *)_dbGetFilenameWithKeys:(NSArray *)keys
取出keys对应的filename,若不为空,则添加进数组并返回;
- (NSMutableArray *)_dbGetFilenamesWithSizeLargerThan:(int)size
遍历所有的数据,若size大于指定的size,则取出filename,若不为空,加入数组中,返回数组;
- (NSMutableArray *)_dbGetFilenamesWithTimeEarlierThan:(int)time
取出早于time的上有所filename组成的数组;
- (NSMutableArray *)_dbGetItemSizeInfoOrderByTimeAscWithLimit:(int)count
先按照size来排序,然后找出前count位的数据,取出其key,filename和size条目来生成YYKVStorageItem对象item,并将这些item组成数组返回;
- (int)_dbGetItemCountWithKey:(NSString *)key
查看数据库中key为key的条目数,其实是用来判断数据库中是否有此key对应的数据;
- (int)_dbGetTotalItemSize
计算所有条目对应的size的总和;
- (int)_dbGetTotalItemCount
计算所有的条目数;
- (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data
将指定data写入到指定的目录(自定义路径+kDataDirectoryName)下的filename文件夹中;
- (NSData *)_fileReadWithName:(NSString *)filename
取出指定文件名下的data;
- (BOOL)_fileDeleteWithName:(NSString *)filename
使用NSFileManager删除指定文件夹下的数据;
- (BOOL)_fileMoveAllToTrash
使用CFUUIDRef生成一个唯一标识的文件夹名称并放在_trashPath
下;使用NSFileManager将_dataPath
中的数据move到此目录下;若移动成功,则继续创建_dataPath
目录,便于后面又有添加数据到_dataPath
目录下;
- (void)_fileEmptyTrashInBackground
在异步串行队列_trashQueue
,使用NSFileManager逐个remove每个path下的数据;
- (void)_reset
删除所有的数据(包括db,shm和wal),并清空trash;
使用此方法时,必须保证db是关闭的状态;
注:笔者认为如果要真的好好看懂YYKVStorage,需要掌握一定的sqlite的姿势,属性stmt代表的是什么,stmt是怎么执行的,生命周期怎么样。
最后希望大家能够根据这篇博客可以更好的使用YYCache或者是学到一点东西。
下一篇是总的YYCache的阅读。
网友评论