kysonyangs

Stay Hungry, Stay Foolish


  • 首页

  • 标签

  • 分类

  • 归档

  • 关于

  • 搜索

SwiftLint 使用

发表于 2019-08-11 | 更新于 2020-05-16 | 阅读次数:

SwiftLint

官方文档

  • SwiftLint 介绍
  • SwiftLint 安装
  • SwiftLint 规则
  • 自定义配置

SwiftLint 介绍

  • SwiftLint 是 Realm 推出的一款 Swift 代码规范检查工具, SwiftLint 基于 Github 公布的 Swift 代码规范 进行代码检查,并且能够很好的和 Xcode 整合
  • 配置好所有的设置之后,在 Xcode 中执行编译时,SwiftLint 会自动运行检查,不符合规范的代码会通过 警告 或者 红色错误 的形式指示出来
  • 支持自定义规则,可禁用或者开启某一些规则
阅读全文 »

URLNavigator 使用

发表于 2019-08-11 | 更新于 2020-06-08 | 阅读次数:

URLNavigator 框架

⛵️ URLNavigator 是 Swift 下一个优雅的 URL 路由。它提供了通过 URL 导航到 view controller 的方式。URL 参数的对应关系通过 URLNavigator.register(_:_:) 方法进行设置。

URLNavigator 提供了两种方法来设置 URL 参数的对应关系:URLNavigable 和 URLOpenHandler。URLNavigable 通过自定义的初始化方法进行设置,URLOpenHandler 通过一个可执行的闭包进行设置。初始化方法和闭包都接受一个 URL 和占位符值。

开始

1. 理解 URL 模式

URL 模式可以包含多个占位符。占位符将会被匹配的 URL 中的值替换。使用 < 和 > 来设置占位符。占位符的类型可以设置为:string(默认), int, float, 和 path。

例如,myapp://user/<int:id> 将会和下面的 URL 匹配:

  • myapp://user/123
  • myapp://user/87

但是,无法和下面的 URL 配置:

  • myapp://user/devxoul (类型错误,需要 int)
  • myapp://user/123/posts (url 的结构不匹配))
  • /user/devxoul (丢失 scheme)
阅读全文 »

Mac下载m3u8文件到mp4

发表于 2019-05-01 | 更新于 2020-06-04 | 阅读次数:

使用FFmpeg下载m3u8文件

  1. 安装 homebrew,已安装的跳过

    1
    2
    $ ruby -e"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    # 安装完成 输入brew检查是否成功
  2. 安装 ffmpeg

    1
    2
    3
    $ brew install ffmpeg
    # 验证是否成功
    $ brew info ffmpeg
  3. 下载m3u8->mp4

    1
    $ ffmpeg -i 视频地址 [输出的文件名.mp4]
  4. 等待下载完成…

一些站点

发表于 2019-02-10 | 更新于 2020-06-08 | 阅读次数:

iOS Developer

常用苹果开发者网页:

  • App Store Connect (App管理后台)
  • Apple Developer (苹果开发者网站)
  • Apple Dev Member Center * (苹果开发者后台)
  • News - Apple Developer (苹果开发新闻)
  • Search - Apple Developer (苹果开发搜索)
  • Documentation Archive (苹果开发文档库)
  • App Store 审核指南 (苹果审核条款(中文))
  • App Store Review Guidelines (苹果审核条款(英文))
  • Contact the App Review Team * (联系苹果审核人员)
  • Tell Us Your Story * (申请App推荐)
  • Human Interface Guidelines (苹果开发人机交互指南)
  • iTuns 报告问题 * (申请内购退款)
  • App Store Connect (AppStore开发网站)
  • App Store Connect 帮助
  • 开发者帐户帮助 (苹果开发者后台帮助文档
  • Contact Us (请求苹果帮助)

注:链接需要先登陆开发者账号才能打开

Masonry源码阅读1

发表于 2019-01-05 | 更新于 2020-05-17 | 阅读次数:

Masonry

Masonry是一个轻量级的布局框架,它使用更好的语法包装AutoLayout。 Masonry有自己的布局DSL,它提供了一种描述NSLayoutConstraints的可链接方式,从而使布局代码更简洁,更易读。 Masonry支持iOS和Mac OS X。

AutoLayout

先看一下,如果使用原生的 AutoLayout,需要那些代码

子视图离父视图上下左右各有10个像素的间距

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
UIView *superview = self.view;

UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[superview addConstraints:@[

//view1 constraints
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:padding.left],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-padding.bottom],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeRight
multiplier:1
constant:-padding.right],

]];

但如果使用 Masonry 的话,代码量以及阅读起来都十分可观

1
2
3
4
5
6
7
8
9
10
11
12
13
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

更简单的方法
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];

是不是非常简洁,调用起来方便,阅读起来也清晰明了

Masonry 会自动将约束添加到适当的视图中, 且自动调用 view1.translatesAutoresizingMaskIntoConstraints = NO;

Masonry

看一下 mas_makeConstraints 方法声明

1
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;

这个方法传递的参数是一个参数为 MASConstraintMaker 类型的无返回值的 block,而该方法的返回值则是一个数组。

方法声明中我们看到了一个叫做 NS_NOESCAPE 的宏,NS_NOESCAPE 用于修饰方法中的 block 类型参数,作用是告诉编译器,该 block 在方法返回之前就会执行完毕,而不是被保存起来在之后的某个时候再执行。编译器被告知后,就会相应的进行一些优化。更详细的内容请参考 Add @noescape to public library API

看一下 mas_makeConstraints 方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
// 默认情况下,view 上的 autoresizing mask 会产生约束条件,以完全确定视图的位置。
// 这允许 AutoLayout 系统跟踪其布局被手动控制的 view 的 frame(例如通过 setFrame:)。
// 当你选择通过添加自己的约束来使用 AutoLayout 来定位视图时,必须 self.translatesAutoresizingMaskIntoConstraints = NO;
// IB 会自动为你做这件事。
self.translatesAutoresizingMaskIntoConstraints = NO;
// 通过view初始化一个MASConstraintMaker
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
// 将 constraintMaker 传递给block,方便我们在外部调用 make.top.equalTo(superview.mas_top).with.offset(padding.top);
block(constraintMaker);
// 返回添加的约束数组
return [constraintMaker install];
}

看一下 MASConstraintMaker 的 init 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface MASConstraintMaker () <MASConstraintDelegate>

@property (nonatomic, weak) MAS_VIEW *view; // 弱指针保留 view
@property (nonatomic, strong) NSMutableArray *constraints; // 约束数组

@end

- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;

self.view = view;
self.constraints = NSMutableArray.new;

return self;
}

看一下 block(constraintMaker); 将 constraintMaker 传递给我们,方便使用属性添加约束

1
2
3
4
make.top.equalTo(superview.mas_top).with.offset(padding.top);
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);

看下 make.top

make 是 MASConstraintMaker 类型的对象,这个类型封装了一系列只读 MASConstraint 属性,top 就是其中之一,声明和实现如下:

1
2
3
4
@property (nonatomic, strong, readonly) MASConstraint *top;
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}

addConstraintWithLayoutAttribute 方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}

// 间接调用了 `constraint:addConstraintWithLayoutAttribute:` 方法:

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}

再来看下 equalTo 方法,也是用 MASConstraint 调用的,并且返回 MASConstraint 对象,方便链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
- (MASConstraint * (^)(id attr))equalTo;
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }
#define MASMethodNotImplemented() \
//
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]

而我们在 makeConstraints 的时候,实际调用的是 MASViewConstraint 这个 MASConstraint 子类中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
NSMutableArray *children = NSMutableArray.new;
for (id attr in attribute) {
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.layoutRelation = relation;
viewConstraint.secondViewAttribute = attr;
[children addObject:viewConstraint];
}
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}

该方法接收两个参数,一个表示了对应的属性(mas_top),一个表示了相等关系(NSLayoutRelationEqual),进入方法后会先对我们传入的属性做一个类型判断,我们传入的是一个单个的属性,所以会落入 else 分支,同样是依赖断言做了一系列保护性的判断,并将相等关系和视图属性分别赋值给 layoutRelation 和 secondViewAttribute 属性,并返回 self。

返回 self,看似简简单单的一个操作,却是 Masonry 能够实现链式 DSL 最重要的基石。

superview.mas_top

再来看看传入的 mas_top,这是一个声明在 View+MASAdditions.h 当中的只读属性:

1
2
3
4
5
@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;

- (MASViewAttribute *)mas_top {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTop];
}

.with

1
2
3
- (MASConstraint *)with {
return self;
}

.offset

1
2
3
4
5
6
7
8
- (MASConstraint * (^)(CGFloat offset))offset;

- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}

[constraintMaker install];

在配置好想要的约束后,调用 [constraintMaker install]; 对视图施加约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSArray *)install {
if (self.removeExisting) {
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
NSArray *constraints = self.constraints.copy;
for (MASConstraint *constraint in constraints) {
constraint.updateExisting = self.updateExisting;
[constraint install];
}
[self.constraints removeAllObjects];
return constraints;
}

对 constraints 属性做一份 copy后,遍历 constraints 中的所有 MASConstraint 及其子类型的属性,并调用其 install 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
- (void)install {
// 是否已经 install 了
if (self.hasBeenInstalled) {
return;
}

// 如果 supportsActiveProperty 且 layoutConstraint 不为空,则将 layoutConstraint.active 设为 YES,并将其添加到 firstViewAttribute.view 的 mas_installedConstraints 只读属性中去
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}

// 生成约束
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

// alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}

MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];

layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;

// 施加约束
if (self.secondViewAttribute.view) {
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
NSAssert(closestCommonSuperview,
@"couldn't find a common superview for %@ and %@",
self.firstViewAttribute.view, self.secondViewAttribute.view);
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}


MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant
existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}
}

SDWebImage源码阅读2

发表于 2018-12-22 | 更新于 2020-05-17 | 阅读次数:

其他

判断是否是gif图

1
2
3
4
5
6
7
8
9
10
@implementation UIImage (GIF)

+ (nullable UIImage *)sd_imageWithGIFData:(nullable NSData *)data {
if (!data) {
return nil;
}
return [[SDImageGIFCoder sharedCoder] decodedImageWithData:data options:0];
}

@end

SDWebImage源码阅读2

发表于 2018-12-22 | 更新于 2020-05-17 | 阅读次数:

SDImageCache

SDImageCache 是 SDWebImage 中用来处理缓存的类,他是一个单例

SDWebImage 中关于缓存可以分为磁盘缓存 id<SDDiskCache> 和 内存缓存 id<SDMemoryCache>

1
2
3
4
5
6
7
8
9
10
@interface SDImageCache ()

#pragma mark - Properties
@property (nonatomic, strong, readwrite, nonnull) id<SDMemoryCache> memoryCache; // 内存缓存管理
@property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache; // 磁盘缓存管理
@property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config; // 基本设置
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath; // 磁盘缓存路径
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue; // io队列

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 查询缓存操作
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {

//不存在这个key,就调用完成block
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}

// 是否有转换操作,获取转换操作完成后图片的key
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer) {
// grab the transformed disk image if transformer provided
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}

// 先检查内存是否存在该图片
UIImage *image = [self imageFromMemoryCacheForKey:key];

if (image) {
if (options & SDImageCacheDecodeFirstFrameOnly) {
// Ensure static image
Class animatedImageClass = image.class;
if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}
} else if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
}

// 是否只查找内存缓存
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}

// 2.查找内存缓存
NSOperation *operation = [NSOperation new];
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}

@autoreleasepool {
///如果盘中存在图片,并且如果可以使用缓存
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// the image is from in-memory cache, but need image data
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
//将硬盘中的图片在缓存中存一份
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}

if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};

// Query in ioQueue to keep IO-safe
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}

return operation;
}

先看一下 SDImageCacheConfig

SDImageCacheConfig 是一个关于缓存设置相关的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (instancetype)init {
if (self = [super init]) {
_shouldDisableiCloud = YES; // 忽略iCloud
_shouldCacheImagesInMemory = YES; // 缓存到内存
_shouldUseWeakMemoryCache = YES; // 缓存到磁盘
_shouldRemoveExpiredDataWhenEnterBackground = YES; // 进入后台模式删除过期的数据
_diskCacheReadingOptions = 0;
_diskCacheWritingOptions = NSDataWritingAtomic;
_maxDiskAge = kDefaultCacheMaxDiskAge; // 磁盘缓存最大存储时间 1WEEK
_maxDiskSize = 0; // 最大磁盘缓存大小 0表示无上限
_diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;
_memoryCacheClass = [SDMemoryCache class];
_diskCacheClass = [SDDiskCache class];
}
return self;
}

SDMemoryCache 内存缓存

SDMemoryCache 继承自 NSCache,实现了 SDMemoryCache 协议,封装了一系列方法,便于扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType> <SDMemoryCache>

@property (nonatomic, strong, nonnull, readonly) SDImageCacheConfig *config;

@end

@interface SDMemoryCache <KeyType, ObjectType> ()

@property (nonatomic, strong, nullable) SDImageCacheConfig *config;
#if SD_UIKIT
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe
#endif
@end

SDMemoryCache 也使用 NSMapTable 来储存缓存的数据,并加了锁来保证线程安全

那为什么 SDMemoryCache 继承自 NSCache 还要维护一个 NSMapTable 呢,因为 NSCache 对于 value 是强引用的,而 NSMapTable 对于value 是弱引用的!

可以通过config 的 shouldUseWeakMemoryCache 来关闭使用 NSMapTable 管理。

SDDiskCache 磁盘缓存

SDMemoryCache 实现了 SDDiskCache 协议,封装了一系列方法,便于扩展

SDWebImage源码阅读1

发表于 2018-12-22 | 更新于 2020-05-17 | 阅读次数:

SDWebImage

SDWebImage具有缓存支持的异步映像下载程序。并添加了像UI元素分类UIImageView、UIButton、MKAnnotationView,可以直接为这些UI元素添加图片。

功能

  • 对UIImageView、UIButton、MKAnnotationView添加Web图像和高速缓存管理
  • 异步图像下载器
  • 具有自动缓存到期处理的异步内存+磁盘映像缓存
  • 背景图像解压缩以避免帧率下降
  • 逐步加载图像(包括GIF图)
  • 缩略图图像解码可节省大图像的CPU和内存
  • 可扩展的图像编码器以支持海量图像格式,例如WebP
  • 动画图像的全栈解决方案,可在CPU和内存之间保持平衡
  • 可以自定义和组合的转换,可在下载后立即应用于图像
  • 可定制的多缓存系统
  • 可以自定义加载器(如照片库)来扩展图像加载功能
  • 图像加载指示器
  • 图像加载过渡动画
  • 保证不会多次下载相同的URL
  • 保证不会重复尝试伪造的URL
  • 保证主线程永远不会被阻塞
  • 支持OC和Swift

支持的图片格式

  • Apple系统支持的图像格式(JPEG,PNG,TIFF,HEIC等),包括GIF / APNG / HEIC动画
  • WebP格式,包括动画WebP(使用SDWebImageWebPCoder项目)
  • 支持可扩展的编码器插件,用于BPG,AVIF等新图像格式。以及矢量格式,例如PDF,SVG。查看图像编码器插件列表中的所有列表

更多模块

查看 Github,有许多基于 SDWebImage 的三方库。

源码分析

SDWebImage架构图

时序图

从SDWebImage中提供的架构图中我们可以大概的看出,整个库分为两层,Top Level、Base Module。

  • Top Level:当UIImageView调用加载image方法,会进入SDWebImage中的UIImageView分类,在分类中调用负责加载UIImage的核心代码块ImageManager,其主要负责调度Image Cache/Image Loader,这两者分别从缓存或者网络端加载图片,并且又进行了细分。Cache中获取图片业务,拆分到了memory/disk两个分类中;Image Loader中又分为从网络端获取或者从系统的Photos中获取。
  • Base Module:获取到图片的二进制数据处理,二进制解压缩,二进制中格式字节判断出具体的图片类型。

核心类

  • UIImageView+WebCache:SDWebImage加载图片的入口,随后都会进入sd_setImageWithURL: placeholderImage: 中。
  • UIView+WebCache:由于分类中不能添加成员变量,所以runtime关联了sd_imageURL图片的url、sd_imageProgress下载进度;sd_cancelCurrentImageLoad方法取消当前下载,sd_imageIndicator设置当前加载动画,sd_internalSetImageWithURL: 内部通过SDWebImageManager调用加载图片过程并返回调用进度
  • SDWebImageManager:内部分别由SDImageCache、SDWebImageDownloader来处理下载任务,

SDWebImage 入口

面向UIImageView的是UIImageView+WebCache这个分类,将图片的URL,占位图片直接给这个类,该类提供众多的设置方法,不过最后都会调用以下这个方法.

1
2
3
4
5
6
7
8
9

UIImageView+WebCache.m

- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;

最后进入的是 UIView+WebCache 的 sd_internalSetImageWithURL方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

UIView+WebCache.m

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 1.取消之前的下载请求
// 1.1生成self类名的key值
// 1.2通过key值取消之前的下载请求
// 2.设置placeholder
// 3.使用SDWebImageManager来管理是下载/缓存获取图片
// 3.1重置下载进度
// 3.2展示loading
// 3.3使用SDWebImageManager来管理是下载/缓存获取图片
// 3.4同步loading与下载进度
// 3.5图片下载完成进行转换展示
// 4.保存当前下载操作

// 下面将SDWebImgaeManager的逻辑
}

UIView+WebCache通过关联为UIView扩展了几个属性

  • sd_imageURL: 当前图片的URL
  • sd_imageProgress: 当前view中图片loading的下载进度
  • sd_imageTransition: 图片的过渡属性,内部包含了很多的属性duration、过渡的动画样式
  • sd_imageIndicator: 图片loading的indicator图标
  • sd_latestOperationKey: 最后一次的操作Key

UIView+WebCacheOperation

UIView+WebCacheOperation 中为UIView通过关联添加了一个 sd_operationDictionary 属性,是一个 MapTable 列表,类似于字典<Key:id>

主要提供一下几个方法

1
2
3
4
5
6
7
8
9
10
11
/// 通过key获取图片下载操作
- (nullable id<SDWebImageOperation>)sd_imageLoadOperationForKey:(nullable NSString *)key;

/// 保存下载操作
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key;

/// 取消下载操作
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key;

/// 删除下载操作
- (void)sd_removeImageLoadOperationWithKey:(nullable NSString *)key;

SDWebImageOptions: 设置的图片加载以及缓存策略,这里只看几个常用的,具体请看 SDWebimageDefine.h -> SDWebImageOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
//失败后重试
SDWebImageRetryFailed = 1 << 0,
//UI交互期间开始下载,导致延迟下载比如UIScrollView减速。
SDWebImageLowPriority = 1 << 1,
//这个标志可以渐进式下载,显示的图像是逐步在下载
SDWebImageProgressiveLoad = 1 << 2,
//刷新缓存,如果设置了该类型,缓存策略依据NSURLCache而不是SDImageCache,所以可以通过NSURLCache进行缓存了
SDWebImageRefreshCached = 1 << 3,
//后台下载
SDWebImageContinueInBackground = 1 << 4,
SDWebImageHandleCookies = 1 << 5,
//允许使用无效的SSL证书
SDWebImageAllowInvalidSSLCertificates = 1 << 6,
//优先下载
SDWebImageHighPriority = 1 << 7,
//延迟占位符
SDWebImageDelayPlaceholder = 1 << 8,
//改变动画形象
SDWebImageTransformAnimatedImage = 1 << 9,
//手动在图片下载完成后设置图片
SDWebImageAvoidAutoSetImage = 1 << 10,
SDWebImageScaleDownLargeImages = 1 << 11,
//查询内存缓存
SDWebImageQueryMemoryData = 1 << 12,
//同步查询内存缓存
SDWebImageQueryMemoryDataSync = 1 << 13,
//同步查询磁盘缓存
SDWebImageQueryDiskDataSync = 1 << 14,
SDWebImageFromCacheOnly = 1 << 15,
SDWebImageForceTransition = 1 << 17,
SDWebImageAvoidDecodeImage = 1 << 18,
SDWebImageDecodeFirstFrameOnly = 1 << 19,
SDWebImagePreloadAllFrames = 1 << 20,
SDWebImageMatchAnimatedImageClass = 1 << 21,
}

SDWebImageManager

上面关于图片缓存和下载的调用的是SDWebImageManager里面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
SDWebImageManager.m

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// 断言,completedBlock不能为空
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// 容错机制,如果url是字符串转为URL
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}

// 防崩溃处理,如果不是NSString和NSURL类型,就赋值为nil,防止app崩溃
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}

// 绑定的操作类,下面介绍
// 实现了 SDWebImageOperation 协议,含有取消方法
// 包含一个cache操作和下载操作
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;

BOOL isFailedUrl = NO;
if (url) {
// 加锁操作,失败URLSet是够包含该URL
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}

// 如果是无效的URL。调用完成操作,返回一个ERROR
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
return operation;
}

// 将operation添加到正在运行的操作Set中
SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);

// 获取一个操作结果
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

// Start the entry to load image from cache
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

return operation;
}

// 查找缓存操作
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// 是否不找缓存,直接下载
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
if (!operation || operation.isCancelled) {
// 返回用户取消缓存错误
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
// 安全的从正在运行中的操作SET中删除该操作
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 下载操作
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// 下载操作
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}

// Download process
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image loader to use
id<SDImageLoader> imageLoader;
if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
imageLoader = context[SDWebImageContextImageLoader];
} else {
imageLoader = self.imageLoader;
}
// Check whether we should download image from network
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
shouldDownload &= [imageLoader canRequestImageForURL:url];
if (shouldDownload) {
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
// 先设置缓存数据在进行下载操作
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}

// 下载
@weakify(operation);
operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation);
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
// 用户取消了下载操作
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] url:url];
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
// Download operation cancelled by user before sending the request, don't block failed URL
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
} else if (error) {
// 下载失败了
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];

// 加入失败的URLSet中
if (shouldBlockFailedURL) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
SD_UNLOCK(self.failedURLsLock);
}
} else {
// 从失败SET中移除URL
if ((options & SDWebImageRetryFailed)) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
}
// Continue store cache process
// 将图片缓存
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}

if (finished) {
[self safelyRemoveOperationFromRunning:operation];
}
}];
} else if (cachedImage) {
// 直接使用缓存
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
// 删除操作,且没数据
// Image not in cache and download disallowed by delegate
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}

大概流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1.判断并处理url
// 2.初始化一个操作
// 3.坏url直接返回成功操作,并带回一个error
// 4.将当前操作添加到runningOpearations中
// 5.获取 SDWebImageOptionsResult 对象
// 6.调用callCacheProcessForOperation方法
// 6.1.1 有缓存,从缓存里面找 [imageCache queryImageForKe...],并设置operation.cacheOperation
// 6.1.2 拿到缓存,并执行下一步操作callDownloadProcessForOperation方法下载
// 6.2 调用callDownloadProcessForOperation方法下载
// 7.调用callDownloadProcessForOperation方法下载
// 7.1 判断是否只从缓存获取 判断是否需要结合SDWebImageRefreshCached,来读取缓存
// 7.2 如果不需要下载,有缓存数据,直接调用成功回调
// 7.3 如果不需要下载,又没有缓存数据,也调用成功回调
// 7.4 需要下载,进行下载,成功后调用成功回调,并执行存储方法callStoreCacheProcessForOperation

SDWebImageCombinedOperation

SDWebImageCombinedOperation 实现了 SDWebImageOperation 代理,而 SDWebImageOperation 里面只有一个cancel方法,上面那个NSMapTable里面的value 类型是 id 不难猜测存储的就是 SDWebImageCombinedOperation 类型,便于调用cancel方法取消任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// 取消当前的operation任务
* (void)cancel;

/// 表示当前的图片是从缓存中查找到的,将从缓存中获取任务放到cacheOperation属性中
@property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> cacheOperation;

/// 当前的图片需要下载,将下载任务放入loaderOperation属性中
@property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> loaderOperation;

//SDWebImageCombinedOperation
//当前对象中取消当前的下载任务

* (void)cancel {

@synchronized(self) {
if (self.isCancelled) {
return;
}
self.cancelled = YES;
//是从缓存中读取数据 SDImageCache
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
//下载中读取数据 SDWebImageDownloadToken
//取消NSOperation中的任务
//取消SDWebImageDownloaderOperation中的自定义回调任务
if (self.loaderOperation) {
[self.loaderOperation cancel];
self.loaderOperation = nil;
}
//从当前正在执行的operation列表中移除当前的SDWebImageCombinedOperation任务
[self.manager safelyRemoveOperationFromRunning:self];
}
}

YYCache源码阅读:YYKVStorage

发表于 2018-12-16 | 更新于 2020-05-17 | 阅读次数:
  • YYDiskCache 磁盘缓存
    • NSMapTable
    • 信号量 dispatch_semaphore
    • YYDiskCache
    • 边界检查
    • YYKVStorage
      • YYKVStorageType 枚举类型,代表使用哪个方式进行存储
      • YYKVStorageItem: YYKVStorage 操作的数据
      • 初始化
      • 增删查改
      • DB操作
    • 总结

YYDiskCache 磁盘缓存

YYDiskCache是一个线程安全的缓存,用于存储SQLite支持的键值对和文件系统(类似于NSURLCache的磁盘缓存)。

YYDiskCache具有以下功能:

  1. 它使用LRU(最近最少使用)来删除对象。
  2. 它可以通过成本,计数和年龄来控制。
    3 .它可以配置为在没有可用磁盘空间时自动驱逐对象。
  3. 它可以自动决定每个对象的存储类型(sqlite / file)。

所以 YYDiskCache 磁盘缓存又分为 文件缓存 和 数据库缓存

1
2
3
4
5
/// 根据这个属性进行区分,当大于该值时使用数据库缓存,小于该值使用文件缓存
/// 默认 20480(20kb)
/// 0 文件缓存
/// NSUIntegerMax 数据库缓存
@property (readonly) NSUInteger inlineThreshold;

NSMapTable

YYDiskCache 维护一个 NSMapTable(Path : YYDiskCache) 来进行缓存操作

NSMapTable: 类似于 NSDictionary 但是可以指定key-value的引用类型,当value的引用类型为 weak 时,当 value 为 nil 时,自动删除key-value

1
2
3
4
5
6
/// 初始化
_globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
/// set
[_globalInstances setObject:cache forKey:cache.path];
/// get
id cache = [_globalInstances objectForKey:path];

信号量 dispatch_semaphore

YYDiskCache 使用信号量进行加锁解锁操作。

dispatch_semaphore_t GCD 中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量 +1;每当发送一个等待信号时信号量 -1,;如果信号量为 0 则信号会处于等待状态,直到信号量大于 0(或者超时) 开始执行之后代码。

YYDiskCache

YYDishCache 使用 YYKVStorage 进行文件缓存处理,并自定义了淘汰算法,删除那些最近时间段内不常用的对象。类似于 YYMemoryCache 与 _YYLinkMap 的关系。

1
2
3
4
5
@implementation YYDiskCache {
YYKVStorage *_kv;
dispatch_semaphore_t _lock;
dispatch_queue_t _queue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 需要通过Path进行初始化
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
return [self initWithPath:@"" inlineThreshold:0];
}

// 默认边界存储值为 20KB,超出20KB 使用数据库存储,否则文件存储
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}

- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;

// 根据path获取cache,有的话就直接返回
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;

// 根据传入的存储边界值来决定使用哪种方式存储
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}

// 初始化 YYKVStorage
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;

_kv = kv;
_path = path;
// 初始化锁
_lock = dispatch_semaphore_create(1);
// 初始化串行队列
_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
_inlineThreshold = threshold;
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_freeDiskSpaceLimit = 0;
_autoTrimInterval = 60; // 60s轮询检查一遍内存

// 轮询内存检查
[self _trimRecursively];
// 将自身存储于NSMapTable中
_YYDiskCacheSetGlobal(self);

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
return self;
}

边界检查

YYDiskCache 在初始化时也会进行一个递归调用的边界检查任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)_trimRecursively {
__weak typeof(self) _self = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground]; // 边界检查
[self _trimRecursively]; // 递归调用
});
}

- (void)_trimInBackground {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
if (!self) return;
Lock();
[self _trimToCost:self.costLimit]; // 缓存大小判断清理
[self _trimToCount:self.countLimit]; // 缓存个数判断清理
[self _trimToAge:self.ageLimit]; // 缓存的时间判断清理
[self _trimToFreeDiskSpace:self.freeDiskSpaceLimit]; // 最小空白磁盘判断, 当空闲磁盘空间小于该值则清楚缓存已达到最小的空白磁盘空间
Unlock();
});
}

分别调用 YYKVStorage 的相应方法

注意

  • YYDiskCache 默认将数据归档成 Data 然后操作 Data 数据进行缓存
  • 如果实现了归档协议则使用归档协议进行存储

YYKVStorage

YYKVStorage是一个基于sqlite和文件系统的键值存储。但作者不建议我们直接使用此类(ps:这个类被封装到了YYDiskCache里,可以通过YYDiskCache间接使用此类),这个类只有一个初始化方法,即initWithPath:type:,初始化后,讲根据path创建一个目录来保存键值数据,初始化后,如果没有得到当前类的实例对象,就表示你不应该对改目录进行读写操作。最后,作者还写了个警告,告诉我们这个类的实例对象并不是线程安全的,你需要确保同一时间只有一条线程访问实例对象,如果你确实需要在多线程中处理大量数据,可以把数据拆分到多个实例对象当中去。

YYKVStorage: YYDiskCache 操作数据的工具类。 YYKVStorage 实现了 文件缓存 和 数据库缓存 功能。可以像操作字典一样使用 key-value 操作数据。

YYKVStorageType 枚举类型,代表使用哪个方式进行存储

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
// 使用文件缓存 当 YYDiskCache 的 inlineThreshold 为 O 时
YYKVStorageTypeFile = 0,
// 使用数据库缓存 当 YYDiskCache 的 inlineThreshold 为 NSUIntegerMax 时
YYKVStorageTypeSQLite = 1,
// 同时使用数据库和文件缓存,当大于 YYDiskCache 的 inlineThreshold 时使用数据库缓存,小于使用文件缓存
YYKVStorageTypeMixed = 2,
};

YYKVStorageItem: YYKVStorage 操作的数据

1
2
3
4
5
6
7
8
9
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; // 缓存数据的key
@property (nonatomic, strong) NSData *value; // 缓存数据
@property (nullable, nonatomic, strong) NSString *filename; // 缓存文件名(文件缓存时有,数据库缓存没有,有可能是数据库缓存但是仍然存在,应为没大于边界值时使用的文件缓存)
@property (nonatomic) int size; // 数据大小
@property (nonatomic) int modTime; // 修改时间
@property (nonatomic) int accessTime; // 访问时间
@property (nullable, nonatomic, strong) NSData *extendedData; // 附加数据,通过关联存储在YYDiskCache中
@end

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {
...
self = [super init];
_path = path.copy;
_type = type;
_dataPath = [path stringByAppendingPathComponent:kDataDirectoryName]; // 文件存储文件夹路径
_trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName]; // 删除文件的文件夹路径,删除的时候先添加到该文件夹,到时候统一删除,回收站
_trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL); // 删除文件队列
_dbPath = [path stringByAppendingPathComponent:kDBFileName]; // 数据库路径
_errorLogsEnabled = YES;
... 文件夹创建

// _dbOpen 方法创建和打开数据库 manifest.sqlite
// 调用 _dbInitialize 方法创建数据库中的表
if (![self _dbOpen] || ![self _dbInitialize]) {
// db file may broken...
[self _dbClose];
[self _reset]; // rebuild
if (![self _dbOpen] || ![self _dbInitialize]) {
[self _dbClose];
NSLog(@"YYKVStorage init error: fail to open sqlite db.");
return nil;
}
}
[self _fileEmptyTrashInBackground]; // 清空回收站
return self;
}

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
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);
*/

增删查改

  • 增

    1. 如果存文件类型且没有文件名,则直接返回NO
    2. 如果有文件名
      1. 如果存文件失败,直接返回NO
      2. 如果存数据库失败(将filename作为value处在数据库里面),则删除文件,并返回NO
      3. 存文件,存数据库都成功,返回yes
    3. 文件名不存在,存数据库类型或者混合存储类型

      1. 不是存数据库类型,获取文件名,删除文件
      2. 存数据库
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
      ...

      // 文件名存在 (>inlineThreshold)
      if (filename.length) {
      if (![self _fileWriteWithName:filename data:value]) { // 创建文件失败
      return NO;
      }
      if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) { // 存储数据失败
      [self _fileDeleteWithName:filename]; // 删除文件
      return NO;
      }
      return YES;
      } else {
      if (_type != YYKVStorageTypeSQLite) {
      NSString *filename = [self _dbGetFilenameWithKey:key];
      if (filename) {
      [self _fileDeleteWithName:filename];
      }
      }
      return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
      }
      }
  • 删

    1. 如果是数据库类型,直接删除数据库里面的数据
    2. 如果是存文件,或者混合类型
      1. 从数据库获取filename
      2. 如果文件存在,删除文件
      3. 删除数据库里面的值
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        - (BOOL)removeItemForKey:(NSString *)key {
        ...
        switch (_type) {
        case YYKVStorageTypeSQLite: {
        return [self _dbDeleteItemWithKey:key];
        } break;
        case YYKVStorageTypeFile:
        case YYKVStorageTypeMixed: {
        NSString *filename = [self _dbGetFilenameWithKey:key];
        if (filename) {
        [self _fileDeleteWithName:filename];
        }
        return [self _dbDeleteItemWithKey:key];
        } break;
        default: return NO;
        }
        }
        - (BOOL)removeItemForKeys:(NSArray *)keys;
        // 删除时间超了的数据
        - (BOOL)removeItemsEarlierThanTime:(int)time;
        // 当数据总大小大于临界值时删除
        - (BOOL)removeItemsToFitSize:(int)maxSize;
        // 当数据个数大于临界值时执行删除操作
        - (BOOL)removeItemsToFitCount:(int)maxCount;
        - (BOOL)removeAllItems;
  • 查

    1. 文件类型
      1. 通过key从数据库获取文件名
      2. 通过文件名获取数据
      3. 如果没有数据,从数据库删除这条数据
    2. 数据库类型
      1. 从数据库根据key获取value data数据
    3. 混合类型
      1. 先获取文件名
      2. 文件名存在,根据文件名获取文件数据,不存在,则删除数据库中数据
      3. 文件名不存在,直接从数据可获取数据
    4. 如果获取到值,则更新数据访问时间

DB操作

_db... 直接操作sqlite数据库,又想了解的自己去看,挺全面的

总结

  • YYDiskCache 使用 YYKVStorage 进行文件缓存 和 数据库缓存,提供了一整套接口,让我们使用时就像使用字典一样方便

YYCache源码阅读:YYMemoryCache

发表于 2018-12-16 | 更新于 2020-05-17 | 阅读次数:

一直想写一写自己读源码的感受与理解,可惜一直未付诸行动。最近闲来无事,索性整理一下,记录于此.

  • YYMemoryCache 内存缓存
    • _YYLinkedMap
      • _YYLinkedMapNode 链表节点数据
    • YYMemoryCache
      • pthread_mutex_t 互斥锁,保证线程安全
      • Init YYMemoryCache
      • 边界检测:内存检测
    • 监听内存警告通知/进入后台通知
    • 增删查改
    • 总结

YYCache

YYCahce 包括 内存缓存(YYMemoryCache) 与 磁盘缓存(YYDiskCache), 而且都是线程安全的!

YYMemoryCache 内存缓存

YYMemoryCache用于对内存缓存进行管理,因为需要高效查询数据,所以 key-value 模式储存。为什么不直接使用字典呢,因为需要保证线程安全。而为什么不直接使用 NSCache 呢,因为 YYMemoryCache 基于自定义的双向链表,提供一种 LRU(least-recently-used: 最近最久少使用的会先删除) 淘汰算法去操作缓存,进行性能优化。

阅读全文 »
123…6

kysonyangs

kysonyangs个人站,备忘

54 日志
13 标签
GitHub
© 2015 — 2020 kysonyangs
由 Hexo 强力驱动 v3.7.1
|
主题 — NexT.Mist v6.3.0