持续更新中...
YYModel
- 使用runtime的动态性,为对象动态添加属性,方法(set,get)
- 利用runtime获取属性列表,方法列表,遍历属性,为属性赋值
YYModel整个部分分为两块:
- 封装Runtime 层级的一些结构体的
YYClassInfo, - 提供接口调用的
NSObject+YYModel
| 类别 | 名字 | Runtime 对应 | 作用 |
|---|---|---|---|
| YYClassInfo部分 | YYClassInfo | objc_class | 对于Class进行了封装,进行封装增加描述 |
| - | YYClassIvarInfo | objc_ivar | 对Class的Ivar进行了进行封装增加描述 |
| - | YYClassMethodInfo | objc_method | 对 Class 的 Property进行了封装描述 |
| - | YYClassPropertyInfo | property_t | 存储属性的信息 |
| YYModel部分 | _YYModelMeta | 对YYClassInfo进行封装描述,完成Model属性、方法、实例变量的解析,并生成与数据源相对应的字典映射 | |
| - | _YYModelPropertyMeta | YYClassProperty进行封装描述,包含了属性的信息和设置属性时所需要的信息 |
- YYClassInfo 对
YYClassIvarInfo、YYClassMethodInfo、YYClassPropertyInfo封装,形式是字典,因为一个类可以有好多属性,方法,实例变量
@interface YYClassInfo : NSObject
@property (nonatomic, assign, readonly) Class cls; ///< class object
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< super class object
@property (nullable, nonatomic, assign, readonly) Class metaCls; ///< class's meta class object
@property (nonatomic, readonly) BOOL isMeta; ///< whether this class is meta class
@property (nonatomic, strong, readonly) NSString *name; ///< class name
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< super class's class info
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< ivars
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< methods
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos;
_YYModelPropertyMeta封装了这个属性在对象模中的情况,其内部包含了属性信息YYClassPropertyInfo_YYModelMeta封装了YYClassInfo,以及保存映射关系的NSDictionary *_mapper;和NSArray *_allPropertyMetas;等
@interface _YYModelMeta : NSObject {
@package
YYClassInfo *_classInfo;
/// Key:mapped key and key path, Value:_YYModelPropertyMeta.
NSDictionary *_mapper;
/// Array<_YYModelPropertyMeta>, all property meta of this model.
NSArray *_allPropertyMetas;
/// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path.
NSArray *_keyPathPropertyMetas;
/// Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.
NSArray *_multiKeysPropertyMetas;
/// The number of mapped key (and key path), same to _mapper.count.
NSUInteger _keyMappedCount;
/// Model class type.
YYEncodingNSType _nsType;
BOOL _hasCustomWillTransformFromDictionary;
BOOL _hasCustomTransformFromDictionary;
BOOL _hasCustomTransformToDictionary;
BOOL _hasCustomClassFromDictionary;
}
YYClassInfo
- YYClassInfo:封装了
Runtime层级的结构体,里面封装了objc_ivar、objc_method、property_t已经封装上述结构体的YYClassInfo
NSObject+YYModel
主要两个功能 json to model和model to json
json to model
整个部分分为两步 json to dictionary&dictionary to model
1. json to dictionary
这一步主要通过系统函数dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];将json转为dic
2. dictionary to model
+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json- 又通过cls生成
_YYModelMeta,_YYModelMeta内部封装了YYClassInfo。
// 使用当前类生成一个 _YYModelMeta 模型元类
Class cls = [self class];
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
YYClassInfo内部通过runtime api获取methods、properties、ivars
Method *methods = class_copyMethodList(cls, &methodCount);
objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
-(BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic,这个方法就是将dic中的数值赋值到实例对象的属性,映射字典和实例对象的属性名称进行赋值
- 入参校验
- 初始化模型元以及映射表校验
- 初始化模型设置上下文 ModelSetContext
- 为字典中的每个键值对调用 ModelSetWithDictionaryFunction
- 检验转换结果
ModelSetWithDictionaryFunction,赋值核心函数1,通过模型设置上下文拿到带赋值模型,之后遍历当前的属性元(直到 propertyMeta->_next == nil),找到 setter 不为空的属性元通过 ModelSetValueForProperty 方法赋值。
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
ModelSetContext *context = _context;
// 1.从上下文中取到model 的信息
__unsafe_unretained _YYModelMeta meta = (__bridge _YYModelMeta )(context->modelMeta);
// 2.从转换字典中取到属性对象
__unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
__unsafe_unretained id model = (__bridge id)(context->model);
// 3.以防有多个相同key 的不同值
while (propertyMeta) {
if (propertyMeta->_setter) {
// 为model 的该属性赋值。
ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
}
propertyMeta = propertyMeta->_next;
};
ModelSetValueForProperty()赋值核心函数2:为模型中的属性赋值的实现方法
static void ModelSetValueForProperty(__unsafe_unretained id model,
__unsafe_unretained id value,
__unsafe_unretained _YYModelPropertyMeta *meta) {
if (meta->_isCNumber) {//属于CNumber类型
NSNumber *num = YYNSNumberCreateFromID(value);
//赋值函数
ModelSetNumberToProperty(model, num, meta);
if (num) [num class]; // hold the number
} else if (meta->_nsType) {
}
... 不同类型不同处理
}
- 根据属性元类型划分代码逻辑
- 如果属性元是 CNumber 类型,即 int、uint 之类,则使用 ModelSetNumberToProperty 赋值
- 如果属性元属于 NSType 类型,即 NSString、NSNumber 之类,则根据类型转换中可能涉及到的对应类型做逻辑判断并赋值(可以去上面代码中查看具体实现逻辑)
- 如果属性元不属于 CNumber 和 NSType,则猜测为 id,Class,SEL,Block,struct、union、char[n],void* 或 char* 类型并且做出相应的转换和赋值
model to json
通过递归方法使 Model 转换为 JSON 的
static id ModelToJSONObjectRecursive(NSObject *model)
-
1:Model -> JSON的关键在于:递归调用ModelToJSONObjectRecursive函数,实现对model各个属性的的转换,最终生成只包含NSArray/NSDictionary/NSString/NSNumber/NSNull的JSON对象。
-
2:通过yy_modelToJSONObject得到JSON对象之后,还可以通过NSJSONSerialization的dataWithJSONObject函数将JSONObject为NSData对象,这也解释了为什么JSONObject必须是NSArray或NSDictionary类型,最后将NSData对象转成NSString对象,即JSON字符串。
参考 揭秘YYModel的魔法
YYCacge
YYCache主要分成两部分:内存缓存YYMemoryCache和磁盘缓存YYDiskCache
- YYCache:提供了最外层的接口,调用了YYMemoryCache与YYDiskCache的相关方法。
- YYMemoryCache:负责处理容量小,相对高速的内存缓存。线程安全,支持自动和手动清理缓存等功能。
- _YYLinkedMap:YYMemoryCache使用的双向链表类。
- _YYLinkedMapNode:是_YYLinkedMap使用的节点类。
- YYDiskCache:负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作,自动和手动清理缓存等功能。
- YYKVStorage:YYDiskCache的底层实现类,用于管理磁盘缓存。
- YYKVStorageItem:内置在YYKVStorage中,是YYKVStorage内部用于封装某个缓存的类
YYCache 实现
- 取
- (id<NSCoding>)objectForKey:(NSString *)key
- (id<NSCoding>)objectForKey:(NSString *)key {
//首先尝试获取内存缓存,然后获取磁盘缓存
id<NSCoding> object = [_memoryCache objectForKey:key];
//如果内存缓存不存在,就会去磁盘缓存里面找:如果找到了,则再次写入内存缓存中;如果没找到,就返回nil
if (!object) {
object = [_diskCache objectForKey:key];
if (object) {
[_memoryCache setObject:object forKey:key];
}
}
return object;
}
- 存
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
//先写入内存缓存,后写入磁盘缓存
[_memoryCache setObject:object forKey:key];
[_diskCache setObject:object forKey:key];
}
YYMemoryCache
-
缓存淘汰算法:使用LRU(least-recently-used 最近最少使用) 算法来淘汰(清理)使用频率较低的缓存。
-
缓存清理策略:使用三个维度来标记,分别是count(缓存数量),cost(开销),age(距上一次的访问时间)。无论是以哪个维度来清理缓存,都是从缓存使用频率最低的那个缓存开始清理(由LRU算法决定)。
-
YYMemoryCache用一个链表节点类来保存某个单独的内存缓存的信息(键,值,缓存时间等),然后用一个双向链表类来保存和管理这些节点。这两个类的名称分别是:
- _YYLinkedMapNode:链表内的节点类,可以看做是对某个单独内存缓存的封装。
- _YYLinkedMap:双向链表类,用于保存和管理所有内存缓存(节点)
YYDiskCache
- 与YYMemoryCache的相同点
- 都具有查询,写入,读取,删除缓存的接口
- 不直接操作缓存,也是间接地通过另一个类(YYKVStorage)来操作缓存。
- 它使用LRU算法来清理缓存
- 支持按 cost,count 和 age 这三个维度来清理不符合标准的缓存。
- 不同点
- 根据缓存数据的大小来采取不同的形式的缓存:
- 数据库sqlite: 针对小容量缓存,缓存的data和元数据都保存在数据库里。
- 文件+数据库的形式: 针对大容量缓存,缓存的data写在文件系统里,其元数据保存在数据库里。
- 除了 cost,count 和 age 三个维度之外,还添加了一个磁盘容量的维度。
保证线程安全的方案
- YYMemoryCache 使用了 pthread_mutex 线程锁(互斥锁)来确保线程安全
- YYDiskCache 则选择了更适合它的 dispatch_semaphore信号量。 为什么
- dispatch_semaphore 是信号量,它的性能比 pthread_mutex 还要高
- 因为YYDiskCache在写入比较大的缓存时,可能会有比较长的等待时间,而dispatch_semaphore在这个时候是不消耗CPU资源的,所以比较适合。
数据结构
- YYMemoryCache为什么不选择单向链表:单链表的节点只知道它后面的节点(只有指向后一节点的指针),而不知道前面的。所以如果想移动其中一个节点的话,其前后的节点不好做衔接。
- YYMemoryCache为什么不选择数组:数组中元素在内存的排列是连续的,对于寻址操作非常便利;但是对于插入,删除操作很不方便,需要整体移动,移动的元素个数越多,代价越大。而链表恰恰相反,因为其节点的关联仅仅是靠指针,所以对于插入和删除操作会很便利,而寻址操作缺比较费时。由于在LRU策略中会有非常多的移动,插入和删除节点的操作,所以使用双向链表是比较有优势的。
选择合适的线程来操作不同的任务
无论缓存的自动清理和释放,作者默认把这些任务放到子线程去做
参考 YYCache 源码解析
SDWebImage
内部实现原理步骤
-
入口setImageWithUrl:placeHolderImage:options:会把placeHolderImage显示,然后SDWebImageManager根据URL开始处理图片.
-
进入SDWebImageManager-downloadWithURL:delegate:options:userInfo:交给SDImageCache从缓存查找图片是否已经下载queryDiskCacheForKey:delegate:userInfo:
-
先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调imageCache:didFineImage:forKey:userInfo:到SDWebImageManager.
-
SDWebImageManagerDelegate回调webImageManager:didFinishWithImage:到UIImageView + WebCache等前端展示图片.
-
如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存
-
根据URLKey在硬盘缓存目录下尝试读取图片文件.这一步是在NSOperation进行的操作,所以回主线程进行结果回调notifyDelegate.
-
如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小 会先清空内存缓存).SDImageCacheDelegate 回调imageCache:didFinishImage:forKey:userInfo:进而回调展示图片.
-
如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调imageCache:didNotFindImageForKey:userInfo.
-
共享或重新生成一个下载器SDWebImageDownLoader开始下载图片
-
图片下载由NSURLConnection来做,实现相关delegate来判断图片下载中,下载完成和下载失败
-
connection:didReceiveData:中利用ImageIO做了按图片下载进度加载效果
-
connectionDidFinishLoading:数据下载完成后交给SDWebImageDecoder做图片解码处理
-
图片解码处理在一个NSOperationQueue完成,不会拖慢主线程UI.如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多.
-
在主线程notifyDelegateOnMainThreadWithInfo:宣告解码完成imageDecoder:didFinishDecodingImage:userInfo:回调给SDWebImageDownloader
-
imageDownLoader:didFinishWithImage:回调给SDWebImageManager告知图片下载完成
-
通知所有的downloadDelegates下载完成,回调给需要的地方展示图片
-
将图片保存到SDImageCache中内存缓存和硬盘缓存同时保存,写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程
-
SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片
-
SDWI也提供UIButton + WebCache和MKAnnptation + WebCache方便使用
-
SDWebImagePrefetcher 可以预先下载图片,方便后续使用
缓存
- 以image的url作为key
- 先查看内存图片缓存,内存图片缓存没有,后生成操作,查看磁盘图片缓存
- 磁盘图片缓存有,就加载到内存缓存,没有就下载图片
- 在建立下载操作之前,判断下载操作是否存在
- 默认情况下,下载的图片数据会同时缓存到内存和磁盘中
关于缓存位置
- 内存缓存是通过 NSCache的子类AutoPurgeCache来实现的;
- 磁盘缓存是通过 NSFileManager 来实现文件的存储(默认路径为/Library/Caches/default/com.hackemist.SDWebImageCache.default),是异步实现的。
问题
- SD怎样解决Cell移出屏幕后的cell中的UIImageView继续下载问题?-- 移除UIImageView当前绑定的操作。
答:sd_setImageWithURL方法内部调用了[self sd_cancelCurrentImageLoad];,它会取消和暂停正在下载和缓存的操作
- SDWebImage怎么实现屏幕滑动时,暂停数据源的任务?通过NSOperationQueue的setSuspend吗?
答: 会暂停
setImage,但不会暂停downloadIamge,由于setImage在主线程调用,当滑动时runloop的mode从kCFRunLoopDefaultMode切换到了UITrackingRunLoopMode,setImage方法就不会被调用。而downloadIamge是基于NSURLSession,NSURLSession会新开一个线程而不受当前runloop影响
AFNetworking
NSURLSession & NSURLConnection
- 协议类型
- NSURLSession支持的HTTP 2.0,会比HTTP 1.1快
- 普通任务和上传
- NSURLSession针对普通、上传和下载三种的网络请求任务,创建了三种task:NSURLSessionDataTask,NSURLSessionUploadTask和NSURLSessionDownloadTask
- 当服务器返回的数据较小时,NSURLSession与NSURLConnection执行普通任务的操作步骤没有区别。
- 执行上传任务时,NSURLSession与NSURLConnection一样需要设置POST请求的请求体进行上传。
- 下载任务方式
- NSURLConnection下载文件时,先是将整个文件下载到内存,然后再写入到沙盒,如果文件比较大,就会出现内存暴涨的情况。
- NSURLSession 使用NSURLSessionUploadTask下载,下载的数据会被加载到沙盒中的temp目录,所以内存不会暴涨。
- 请求方法的控制
- NSURLConnection请求在被cancel后,不能继续访问,而需要重新创建请求。
- NSURLSession有三个控制方法,取消(cancel)、暂停(suspend)、继续(resume),暂停以后可以通过继续恢复当前的请求任务。
- 断点续传
- NSURLSession支持网络操作的取消和断点续传,相较于NSURLConnection更加便捷
- NSURLSession支持后台处理上传和下载,即使你点击了“Home”按钮,后台仍然可以继续下载,并且提供了根据网络状况,电力情况进行处理的配置。
- 配置信息 NSURLSession 有一个NSURLSessionConfiguration类,其可以设置配置信息:cookie、安全和高速缓存策略、最大主机连接数、资源管理、网络超时等配置。比只依赖一个全局配置的NSURLConnection更加的灵活
AFNetworking架构
- 网络通信模块(AFURLSessionManager、AFHTTPSessionManger)
- 网络状态监听模块(Reachability)
- 网络通信安全策略模块(Security)
- 网络通信信息序列化/反序列化模块(Serialization)
- 对于iOS UIKit库的扩展(UIKit)

- 所有的网络请求最终都会经过
AFURLSessionManager处理,几乎所有的类都是围绕着这个类在处理业务逻辑。 AFHTTPSessionManger也会把数据包装后下发给AFURLSessionManager处理
AFNetworking相关知识点
-
为什么要尽量共享Session,而不是每次新建Session **答:**共享的Session将会复用TCP的连接,而每次都新建Session的操作将导致每次的网络请求都开启一个TCP的三次握手
-
使用反射机制来确定KeyPath,防止书写错误
[progress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
这里NSStringFromSelector(@selector(fractionCompleted))就是通过反射机制获取到字符串,从而进行监听,避免了字符串的书写错误
- 增加方法到类中再进行Method Swizzling 同时为了避免直接换带来多次交换把原来方法弄乱的问题,是先将需要换的方法add到需要替换类中(相当于生成一个副本),然后让那个类里面的副本方法去交换,也就是不影响原来拥有这个方法的类里的方法。
+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass{
Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));
//先add
if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
}
if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
}
}
- 读写锁:多读单写 在读写HTTPHeaderField的时候
- 读:dispatch_sync() + 并发队列
- 写:dispatch_barrier_async() + 并发队列
self.requestHeaderModificationQueue 是个并发队列
- (NSString *)valueForHTTPHeaderField:(NSString *)field {
NSString __block *value;
dispatch_sync(self.requestHeaderModificationQueue, ^{
value = [self.mutableHTTPRequestHeaders valueForKey:field];
});
return value;
}
- (void)setValue:(NSString *)value
forHTTPHeaderField:(NSString *)field
{
dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
[self.mutableHTTPRequestHeaders setValue:value forKey:field];
});
}
- 线程&线程常驻
- 用NSOperationQueue控制线程,拥有一个全局的Queue,所有请求operation,都会被加入到这个全局的Queue。
- 由这个全局的Queue来控制并发
- 系统会根据当前可用的核心数以及负载情况动态地调整最大的并发 operation 数量。(注意:并发数并不等于所开辟的线程数。具体开辟几条线程由系统决定。)
- AFNetworking 2.0还会创建一个子线程,用于处理所有的子线程的回调,而这个线程又是通过
runloop保持常驻的。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}

5.NSURLConnection的一大痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态,所以需要线程常驻。 而AFNetworking 3.0 是基于NSURLSession的,而NSURLSession可以指定回调队列,所以就不再设置常驻线程
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
//这里传了代理回调队列delegateQueue
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
资料参考 AFNetworking到底做了什么? 、NSURLSession与NSURLConnection区别 、、、别说你会AFNetworking3.0/NSURLSession