Introduction: Some time ago, the business has the cache requirement, so combined with YYCache and the business requirement, has done the cache layer (memory & disk) + network layer scheme attempt, because YYCache has the excellent performance, here takes its principle as an example, how to design a set of cache ideas and combined with the network to organize a set of complete process

# directory

  • A primer on caching
  • How to optimize cache (YYCache design Idea)
  • Network and cache synchronization process

A preliminary understanding of cache

##1. What is caching? Before we do a cache, let’s understand what it is, a cache is a local data store, and there are two main types of storage: disk storage and memory storage, right

1.1 Disk Storage

Disk cache, disk is also disk cache, disk is the storage space of the program, disk cache capacity is large and slow, disk is permanent storage things, iOS for different data management to make the storage path specification as follows: 1, every application will have an application sandbox. 2. The application sandbox is a file system directory. Sandbox root directory structure: Documents, Library, Temp.

Disk storage mainly includes file management and database. Its features are as follows:

The memory cache refers to the running space of the current program. The memory cache is fast and has small capacity. It is directly read by the CPU. IOS memory is divided into five areas: stack area, heap area, global area, constant area, code area

** Stack area: ** This area will be managed by the system, we do not need to intervene, mainly save some local variables, as well as the on-site protection when the function jumps. So a large number of local variables, deep recursion, and function loop calls can all cause memory to run out and crash. In contrast to the stack area, this area is generally managed by ourselves, such as alloc, free operations, storing objects created by ourselves. Global variables and static variables are stored here. Initialized and uninitialized variables are stored in adjacent areas. When the program is finished, the system will release the constant area: constant strings and const constants ** Code area: ** Stores code

Containers (arrays, dictionaries) declared in a program can be treated as in-memory storage, with the following properties:

##2. What does caching do? We use scenarios such as: offline loading, preloading, local address book… Etc., for non-network data, the use of local data management, there are many specific use scenarios. How do I cache?

The simple cache can only use disk storage. IOS provides four disk storage modes:

  • NSKeyedArchiver: Saves data in the form of archive. The data object must comply with NSCoding protocol, and the corresponding class of the object must provide encodeWithCoder: and initWithCoder: methods.
#import <Foundation/ foundation.h > @interface Person: NSObject<NSCoding> @property(nonatomic,copy) NSString * name; #import "person.h" @implementation Person - (void)encodeWithCoder:(NSCoder *)aCoder {[aCoder encodeObject:_name forKey:@"name"]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder {if (self = [super init]) {_name = [aDecoder decodeObjectForKey:@"name"]; } return self; } @endCopy the code

Use archiving to unfile

[NSKeyedArchiver archiveRootObject:p toFile:path]; / / Person according to the path the path to find the solution file file * p = [NSKeyedUnarchiver unarchiveObjectWithFile: path];Copy the code

Disadvantages: the form of archive to save data, can only be a one-time archive save and one-time decompression. Therefore, only a small amount of data can be used. If you want to change a small part of the data, you need to decompress the whole data or archive the whole data.

  • NSUserDefaults: Saves application Settings and properties, user-saved data. The data is still there when the user opens the program again or starts it up.

NSUserDefaults stores the following data types :NSData, NSString, NSNumber, NSDate, NSArray, and NSDictionary.

[[NSUserDefaults standardUserDefaults] setObject:@"value" forKey:@"key"]; [[NSUserDefaults standardUserDefaults] objectForKey:@"key"];Copy the code
  • Write Write mode: Permanently saved on the disk. Specific methods are as follows:
// Write the NSData object data to a file named FileName [data writeToFile:FileName atomically:YES]; / / read out data from FileName NSData * data = [NSData dataWithContentsOfFile: FileName options: 0 error: NULL];Copy the code
  • SQLite: Uses SQLite database to store data. SQLite as a small and medium-sized database, ios applications compared to the other three save methods, relatively complex
If (sqlite3_open([databaseFilePath UTF8String], &database)==SQLITE_OK) {NSLog(@"sqlite dadabase is opened."); } else { return; }// select * from a database where a table is not available. char *error; const char *createSql="create table(id integer primary key autoincrement, name text)"; if (sqlite3_exec(database, createSql, NULL, NULL, &error)==SQLITE_OK) { NSLog(@"create table is ok."); } else { sqlite3_free(error); Const char *insertSql="insert into a person (name) values(' gg ')"; if (sqlite3_exec(database, insertSql, NULL, NULL, &error)==SQLITE_OK) { NSLog(@"insert operation is ok."); } else { sqlite3_free(error); // Empty the error string after each use to provide the next use}Copy the code

The above mentioned disk storage features, with large space, durable, but read slowly, in the face of a large number of data frequently read more obvious, in the past test disk read than memory read conservative measurement is lower than dozens of times, then how do we solve the disk read slow shortcomings? How do you take advantage of memory?

2. How to optimize cache (YYCache design idea)

YYCache background: The source code consists of two main classes

  • YYMemoryCache

To realize memory optimization, LRU algorithm is implemented by using bidirectional linked list data structure, and YYLinkedMapItem is each child node

  • YYDiskCache (DiskCache)

Instead of directly manipulating cache objects (SQLite /file), YYKVStorage indirectly manipulates cache objects.

Capacity management:

  • AgeLimit: Time period limit, such as starting the cleanup daily or weekly
  • CostLimit: Indicates the capacity limit. For example, the memory will be cleared after 10 MB is exceeded
  • CountLimit: Limit the number of entries that exceed 1000

Here we use YYCache design to talk about cache optimization

1. Optimize the combination of disk and memory

The advantages of memory and disk are integrated as follows:

  • The APP preferentially requests resources in the memory buffer
  • If there is a resource file in the memory buffer, the resource file is directly returned. If there is no resource file, the resource file is requested. In this case, the resource file is stored on the local disk by default, and you need to operate the file system or database to obtain the resource file.
  • The obtained resource files are first cached in the memory cache to save time.

And then you get the data from the cache and you give it to the app. In this way, the two characteristics are fully combined, and the fast memory read feature is utilized to reduce the reading time of data.

YYCache source code parsing:

- (id<NSCoding>)objectForKey:(NSString *)key { // 1. Id <NSCoding> object = [_memoryCache objectForKey:key]; id<NSCoding> object = [_memoryCache objectForKey:key]; if (! Object = [_diskCache objectForKey:key]; If (object) {// 3. Save the data to the memory [_memoryCache setObject:object forKey:key]; } } return object; }Copy the code

2. Memory optimization — Improve memory hit ratio

However, if we want to make some basic optimizations, for example, we want to keep the frequently accessed data in memory, improve the memory hit ratio and reduce the disk read, how do we do that? – the LRU algorithm

LRU algorithm: We can regard the linked list as a string of data, each data is a node on the string, the frequently accessed data is moved to the head, and when the data exceeds the capacity, it is destroyed from some nodes at the back of the linked list, so that the frequently accessed data is in the head position, but also retained in memory.

Linked list implementation structure diagram:

YYCache source code analysis

/**
 A node in linked map.
 Typically, you should not use this class directly.
 */
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
/**
 A linked map used by YYMemoryCache.
 It's not thread-safe and does not validate the parameters.
 Typically, you should not use this class directly.
 */
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

/// Insert a node at head and update the total cost.
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
- (void)removeAll;

@end
Copy the code

_YYLinkedMapNode *_prev is the head pointer of this node, pointing to the previous node. _YYLinkedMapNode *_next is the tail pointer of this node, pointing to the next node. The head pointer and tail pointer connect a child node to form a bidirectional linked list

See bringNodeToHead: source code implementation, it is the main method to implement LRU algorithm, move node children to the header.

(Details are commented in the code)

- (void)bringNodeToHead:(_YYLinkedMapNode *)node { if (_head == node) return; If (_tail == node) {_tail = node->_prev; if (_tail == node) {_tail = node->_prev; // If current node is tail, update tail pointer _tail->_next = nil; } else {// for example: A, B, C list, remove B, reconnect A, C; node->_next->_prev = node->_prev; // Set the next node's head pointer to the previous node, node->_prev->_next = node->_next; } node->_next = _head; // Set the end pointer of the current node node to the previous header, because node is the latest first node node->_prev = nil; _head->_prev = node; // the previous _head will be the second node _head = node; // The current node becomes the new _head}Copy the code

Other methods are not one by one example, specific can look at the source code, these code structure is clear, classes and functions follow a single responsibility, interface high cohesion, low coupling, is a good learning example!

3. Disk optimization – Data storage by category

YYDiskCache is a thread-safe disk cache based on SQLite and File. Our cache objects can freely choose the storage type. Here is a simple comparison:

  • Sqlite: Access efficiency for small data (such as NSNumber) is significantly higher than file.
  • File: More efficient than SQLite for accessing large data, such as high-quality images.

Therefore, YYDiskCache uses the combination of the two to flexibly store data to improve performance.

In addition, YYDiskCache has the following functions:

  • It uses LRU(least-recently-used) to delete objects.
  • Supports cost, count, and age control.
  • It can be configured to automatically expel cache objects when no disk space is available.
  • It can automatically choose the storage type of each cached object (SQLite /file) to provide better performance.

YYCache source code analysis

YYKVStorageItem is a class used to store key/value pairs and metadata in YYKVStorage. NSObject @property (nonatomic, strong) NSString *key; ///< key @property (nonatomic, strong) NSData *value; ///< value @property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline) @property (nonatomic) int size; ///< value's size in bytes @property (nonatomic) int modTime; ///< modification unix timestamp @property (nonatomic) int accessTime; ///< last access unix timestamp @property (nullable, nonatomic, strong) NSData *extendedData; ///< extended Data (nil if no extended data) @end /** YYKVStorage is key value storage based on SQLite and file system. In general, we should not use this class directly. The @warning class instance is * non-* thread-safe, and you need to make sure that only one thread can access the instance at the same time. If you really need to process large amounts of data in multiple threads, you should split the data into multiple KVStorage instances (sharding). */ @interface YYKVStorage : NSObject #pragma mark - Attribute @property (nonatomic, readonly) NSString *path; /// storage path @property (nonatomic, readOnly) YYKVStorageType type; /// Storage type @Property (nonatomic) BOOL errorLogsEnabled; #pragma mark - Initializer - (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER; #pragma mark - Save Items - (BOOL)saveItem:(YYKVStorageItem *)item; . #pragma mark - Remove Items - (BOOL)removeItemForKey:(NSString *)key; . #pragma mark - Get Items - (nullable YYKVStorageItem *)getItemForKey:(NSString *)key; . #pragma mark - Get Storage Status - (BOOL)itemExistsForKey:(NSString *)key; - (int)getItemsCount; - (int)getItemsSize; @endCopy the code

We just need to look at the enumeration YYKVStorageType, which determines the storage type of YYKVStorage.

YYKVStorageType

/** Storage type, indicating where yykvStorageItem. value is stored. @discussion In general, writing data to SQLite is faster than external files, but read performance depends on the data size. In the iPhone 6S 64G test environment, it was faster to read data from external files when it was large (over 20KB) than SQLite. */ typedef NS_ENUM(NSUInteger, YYKVStorageType) { YYKVStorageTypeFile = 0, // Value is stored as a file in the file system YYKVStorageTypeSQLite = 1, // Value is stored as a binary file in sqLite YYKVStorageTypeMixed = 2, // Value will be mixed based on the above two forms as you choose};Copy the code

Conclusion:

Here said YYCache several major design optimization, in fact, there are also a lot of good details, such as:

  • Thread safety

If YYCache is a purely logic-level cache class, then YYMemoryCache and YYDiskCache do something about it. The most obvious is that YYMemoryCache and YYDiskCache are thread-safe for YYCache. YYMemoryCache uses a pthread_mutex thread lock to ensure thread-safety, whereas YYDiskCache chooses its more appropriate dispatch_semaphore

  • performance

Implementation details of YYCache for performance improvement:

  1. Asynchronously release the cache object
  2. The choice of the lock
  3. YYDiskCache managed using NSMapTable singleton
  4. The _dbStmtCache YYKVStorage
  5. Even using CoreFoundation in exchange for minimal performance gains

3. Network and cache synchronization process

Combining network layer and cache layer, a set of interface cache method is designed, which is more flexible and faster. For example, the home page may provide data by multiple interfaces. Instead of using the whole block storage, the storage is subdivided into each interface and controlled by API interfaces. The basic structure is as follows:

It is mainly divided into:

  • Application layer: Display data
  • Management layer: Manages the network layer and cache layer and provides data support for the application layer
  • Network layer: Requests network data
  • Cache layer: Caches data

Hierarchical graph:

  1. Each set of data on the server corresponds to a version (or timestamp). If the background data is changed, the version is changed and the client data is returned with the version.
  2. When the client requests the network, the version of the last local data is uploaded.
  3. After obtaining the version from the client, the server compares the version with the latest version. If the version is inconsistent, the server returns the latest data. If the version does not change, the server does not need to return all data but only needs to return 304 (No Modify) status
  4. The client returns data from the server. If all the returned data is not 304, the client synchronizes the latest data to the local cache. If the client receives the 304 status value, the server data is the same as the local data and is displayed directly from the cache

This is the general flow of ETag; Can view the detail baike.baidu.com/item/ETag/4…

The source code example

- (void)getDataWithPage:(NSNumber *)page pageSize:(NSNumber *)pageSize option:(DataSourceOption)option completion:(void (^)(HomePageListCardModel * _Nullable, NSError * _Nullable))completionBlock { NSString *cacheKey = CacheKey(currentUser.userId, PlatIndexRecommendation); // Global static constant (userID + apiName) // Whether cache mode is required, network mode go 304 logical switch (option) {case DataSourceCache: { if ([_cache containsObjectForKey:cacheKey]) { completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil); } else {completionBlock(nil, LJDError(400, @" not in cache ")); } } break; case DataSourceNetwork: { [NetWorkServer requestDataWithPage:page pageSize:pageSize completion:^(id _Nullable responseObject, NSError * _Nullable error) { if (responseObject && ! error) { HomePageListCardModel *model = [HomePageListCardModel yy_modelWithJSON:responseObject]; If (model.errnonumber == 304) {// Get cache data completionBlock((HomePageListCardModel *)[self-> _cacheForKey :cacheKey], nil); } else { completionBlock(model, error); [self->_cache setObject:model forKey:cacheKey]; }} else {completionBlock(nil, error);}}]; } break; default: break; }}Copy the code

Benefits of this:

  • For interfaces that do not update data frequently, a large amount of JSON data conversion time is saved
  • Save traffic and load time
  • User interface display speeds up

Conclusion: the project is not always done in this way, sometimes the transition design is also a waste, more understanding of other design ideas, for the project to find the appropriate is the best!

References:

YYCache: github.com/ibireme/YYC…

YYCache design idea: blog.ibireme.com/2015/10/26/…

Denver annual essay | 2020 technical way with me The campaign is under way…