浅谈AFNetworking之《一》总览

AFNetworking框架是当下iOS网络开发中使用最广的开源框架。今天起,和大家一起从源码的角度来重新认识一下他。
AFNetworking的核心是对URLSession的封装。而URLSession正式引入iOS是从iOS7开始,用来取代URLConnection。直到iOS9,苹果完全放弃了URLConnection,全面使用URLSession.

首先我们要认识的第一个概念就是URLSession。
简单总结一下:
1、URLSession实例是线程安全的。
2、URLSession的创建是伴随着一个 NSURLSessionConfiguration 的,这个配置控制着网络凭证、缓存、cookie等等。在URLSession创建时就需要提供,创建之后再给就不起作用了
3、URLSession可以是各种URLSessionTask的管理者。
4、URLSessionTask对象默认被创建于挂起状态,当需要执行时,必须要调用resume

第二个概念是URLSessionTask
用苹果的话讲 URLSessionTask — a cancelable object that refers to the lifetime of processing a given request. 也就是 掌控了一个请求的整个处理生命周期的一个可取消的对象。所以,它的整个存在的意义,就是对请求生命周期的掌控。 这中间包含了对请求的唯一定位、优先设定、记录将要(/已经/对端期待等等)发出/收到的包以及请求的当前状态等、请求的挂起、恢复、取消。 总之一句话,掌控请求生命周期。
至于它的子类,Data/Update/Download/Stream 则是根据不通的情形所做的特定处理。

回到我们的AFNetworking.
前面也说了,其实AFNetworking做的主要的事情就是针对URLSession框架的一个封装,以便于我们业务上的使用。

结构如上:分为AFNetworking.h + URLSession + Reachability + Security + Serialization + UI
我们后面分析的模块顺序从URLSession -> Serialization -> Security -> Reachability -> UIKit
URLSession模块中核心类是AFURLSesssionManager, AFHTTPURLSesssionManager只是对它的一个包装,核心实现都在AFURLSesssionManager中。AFURLSesssionManager承接管理了所有URLSessionTask的生命周期,以及对处理结果的一个异步处理,整体分发。
Serialization模块的核心是AFURLResponseSerialization和AFURLRequestSerialization,AFURLResponseSerialization是解析处理的核心抽象类,目前AF支持JSON、XML、Plist、简单图片、混合5中解析方式。
模块安全、网络连通性类相对少,按顺序来就是
最后的UI模块,我们准备挑几个有代表性的来进行研究。

浅谈yycache之《三》 内存缓存(终结版)

前两篇中,介绍了YYCache中的Disk Cache, 本篇介绍一下Memory Cache.

业内地位

作者在调研对比了TMMemoryCache、PINMemoryCache、NSCache、NSDictionary、NSDictionary+Lock多钟方式之后,取精去粕,实现了自己的YYMemoryCache。
我把它归纳一下:

可见,YYMemoryCache基本上在性能上仅次于系统原始字典api,优于系统Cache、TM、PIN。但系统原始字典只是一个最通用的hashtable,并不能直接用做cache.所以YYMemoryCache是当前最好的内存缓存。
有一张基准性能对比表:

技术关键点

访问:CFDictionaryRef
操作:双向链表
淘汰:从大小、数量、使用时长三个维度自定义阈值去淘汰缓存
管理:可配置内存警告或退到后台是否清空缓存;是否自动校验阈值淘汰;释放对象是否异步,是否在主线程;
兼容:和NSCache API基本一致,线程安全

实现

主要流程如下:

1、Cache对象维护一个链表,一个字典。链表使修改删除的时间复杂度为O(1), 字典使查找的时间复杂度为O(1)。
2、用了pthread_mutex_t的锁,作者最开始(2015年)用的是自旋锁OSSpinLock,后来大家爆出自旋锁会在一定的时机出现死锁,比如持有锁的一方依赖等待锁的一方,由于没有优先级,谁都不进入睡眠,所以就死锁了。更多调研参考
自旋锁问题
作者之后(2016年)的做法就是替换为pthread_mutex_t,现在苹果引入了不公平锁os_unfair_lock,效率是要比pthread_mutex_t高一些的,也可以满足线程优先级。其实这里可以做一点优化。
3、一些配置,大小、数量、时长、清除时机,轮询监测。这些比较简单,大家感兴趣可以自己读一下源码。值得说的一点是,我对作者的默认每5秒监测一次(当然可以配置)持保留意见。轮询唤醒CPU的做法是非常耗电的,内存缓存在应用中一般会是一个单例,生命周期贯穿整个app,这样的操作可以优化到具体操作过程中。不建议使用这个默认轮询。可以在设置值的时候进行调整。
4、作者在一些细节方面的处理很值得人敬佩和学习。比如淘汰对象的释放,他支持了异步。作者认为大量对象的释放也是一笔不小的开销,会对主线程造成影响。处理的也比较巧

if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                [node class]; //hold and release in queue
            });
        }

YYMemoryCache之API

与NSCache非常类似

One more thing

之前讲过的YYDiskCache中,存储对象可以存于sqlite和file中,作者给出的阈值是20k,但没有给出具体测试的案例,经查询,sqlite官网中给出的结论是100k,所以这块有待研究和讨论。https://www.sqlite.org/intern-v-extern-blob.html

总结

1、YYCache(包括Memory和Disk)在当前业内非常优秀,可以满足几乎所有的缓存需求。
2、作者受年代的影响(2016年),锁的选择这块还可以略微优化,用上苹果最新的不公平锁。
3、对于APP电量消耗的考虑,不建议开启自动轮询trim功能,而是将监测时机放于添加修改时,以及一些系统时机,比如内存预警,后切前,前切后等。
4、对db与file阈值这件事,不确定到底是20k还是100k,有时间可以自己测试一下。
5、祝YY大神身体健康,一切顺利~

YYCache系列剧终了,准备炒作下一个IP ~_~

浅谈yycache之《二》磁盘缓存的实现原理
浅谈yycache之《一》磁盘缓存的简单使用

浅谈yycache之《二》 磁盘缓存的实现原理

上一篇中,介绍了YYDiskCache的使用,本篇介绍一下它的实现。
YYDiskCache支持在一个app中创建多个缓存实例,每个独立的实例都必须以独立的路径为准,多次创建同一路径的cache,返回的可能是同一个实例。YYCache管理了一个全局集合,类型为NSMapTable,所有的DiskCache实例都保存在这个Map中,该集合是线程安全的,由一个单独的信号量机实例保证。 创建DiskCache对象会先从Map中找对应路径的,没有才会创建新的。

_globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

NSMapTable是Foundation下的API,可以扩展key和value的内存语义,比NSDictionary要强大,上面这句话的意思就是,全局集合中的value是weak的,可以随时被释放,不会造成相互引用。

每一个YYDiskCache都由一个YYKVStorage,一个信号量锁,和一个并发任务队列构成。YYKYStorage封装了对SQL、文件的读写操作,信号量保证线程安全,并发队列提升效率。

@implementation YYDiskCache {
    YYKVStorage *_kv;
    dispatch_semaphore_t _lock;
    dispatch_queue_t _queue;
}

KVStorage的操作的两份内容 DB/File,结构如下:

/*
 File:
 /path/
      /manifest.sqlite
      /manifest.sqlite-shm
      /manifest.sqlite-wal
      /data/
           /e10adc3949ba59abbe56e057f20f883e
           /e10adc3949ba59abbe56e057f20f883e
      /trash/
            /unused_file_or_folder

 SQL:
 create table if not exists manifest (
    key                 text,
    filename            text,
    size                integer,
    inline_data         blob,
    modification_time   integer,
    last_access_time    integer,
    extended_data       blob,
    primary key(key)
 ); 
 create index if not exists last_access_time_idx on manifest(last_access_time);
 */

这个path的父目录即创建diskcache时外部设置的path。
/manifest.sqlite是整个cache的数据库,另外两个文件.sqlite-shm/.sqlite-wal 是数据库产生的临时文件,一个代表共享内存(shared memory),一个代表日志(write-ahead log)。DB删除的时候,需要把这两个文件一起删除掉。
/data/目录存储了我们缓存的所有文件,和实际文件之间的二进制对应关系可以自定义,默认是archeive方式,文件名也可以自定义,默认是MD5方式,上一篇文章中已经说过。
/trash/是data中文件删除前的一道屏障,相当于回收站。不过当前使用的时候,是在reset时机,这时,两个文件夹中的文件会被先后删除。

SQL 即在manifest.sql库中创建一个manifest表,以记录每一条缓存数据。包括以文件形式存储的,只是文件的话,inline_data为空。每次对缓存的操作都由last_access_time modification_time进行记录。

Storage中定义实现了各种增删改查的操作,以及数据库的建立关闭。尤其是数据库的操作,涉及每一个缓存对象,所以在对象dealloc的时候,向系统申请了后台额外时长,以防止后台时进程被系统杀死,导致存储异常。

- (void)dealloc {
    UIBackgroundTaskIdentifier taskID = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{}];
    [self _dbClose];
    if (taskID != UIBackgroundTaskInvalid) {
        [_YYSharedApplication() endBackgroundTask:taskID];
    }
}

beginBackgroundTaskWithExpirationHandler 这个方法会申请3分钟左右的后台存活时间,ios7以前可以存活5-10分钟。这个方法必须与 endBackgroundTask 配对使用。

_YYSharedApplication() 的定义引出了引出了另一个概念:App Extension,看一下这个定义:

/// Returns nil in App Extension.
static UIApplication *_YYSharedApplication() {
    static BOOL isAppExtension = NO;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = NSClassFromString(@"UIApplication");
        if(!cls || ![cls respondsToSelector:@selector(sharedApplication)]) isAppExtension = YES;
        if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) isAppExtension = YES;
    });
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    return isAppExtension ? nil : [UIApplication performSelector:@selector(sharedApplication)];
#pragma clang diagnostic pop
}

App Extension的概念相信大家也见过,就是apple定义的一些app增强功能,包括widget, 分享,快速回复等等。以后文章专门介绍一下。官方文档:https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/index.html#//apple_ref/doc/uid/TP40014214-CH20-SW1
它的基本生命周期图如下:

总结一下:
1、YYDiskCache线程安全
2、YYDiskCache维护了一个全局集合,用于存储各个Cache对象,value全部为弱引用
3、YYDiskCache的操作行为主要由YYKVStorage来代理,数据库创建打开关闭销毁,增删改查,文件读写。
4、YYKVStorage做了进程后台免杀死的基本保护,beginBackgroundTaskWithExpirationHandler和endBackgroundTask要成对出现。
5、UIApplication有可能是 App Extension,并没有sharedApplication方法,需要加以区分。
6、NSMapTable是Foundation的API,功能强大,支持对key和value内存语义更广泛的定义。

浅谈yycache之《一》磁盘缓存的简单使用

缓存是ios开发中非常常见的,今天就介绍一个优秀的缓存开源库: yycache.
yycache分为两部分:磁盘缓存和内存缓存。
先介绍磁盘缓存:

创建缓存

- (nullable instancetype)initWithPath:(NSString *)path
                      inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;

1、路径:路径一旦创建,就是一个只供缓存操作的目录,不能人为读写。
2、yycache的缓存数据存储于sqlite和文件中,由阀值threshold决定,大于阈值的存储于文件,小于等于的存于sqlite.

存储数据
存储操作依赖于YYKVStorage,每一个要存储的数据对应一个YYKVStorageItem

YYKVStorageItem的结构如下:

@interface YYKVStorageItem : 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

每一条数据不管是否存以文件,都会在数据库中存一条记录。如果是文件的话,inline_data为空。

清除数据
清除数据的策略是根据缓存容量、缓存条目数量、条目存在的时间,三个维度来进行清理。缓存一建立就会起一个后台线程进行轮询清理,轮询间隔是由上层设置,默认是一分钟。

使用缓存
缓存的使用非常简单

//创建
_diskCache  = [[YYDiskCache alloc] initWithPath: [self classroomFilePath]
                                            inlineThreshold:0]; //0代表所有的数据都存储于磁盘
_diskCache.customArchiveBlock   = ^(id object) {return object;};
_diskCache.customUnarchiveBlock = ^(NSData *object) {return object;};

//设置
if (url != nil && url.length != 0 && url1 != nil && url1.path != nil && url1.path.length != 0)
                {
                    [strongSelf1.diskCache setObject:data forKey:url1.path];
                }
//读取
id obj  = [self.diskCache objectForKey:path];

使用时需要注意的是
1、写入和读取的block,默认是archieve,我们可以定义自己的方式,比如直接返回
2、读取和写入文件的路径,可以根据业务的需要,自己来定义路径。

使用就是这些了,下一篇将介绍磁盘缓存内部的实现原理