Skip to content

Commit 8fe2da1

Browse files
authored
Merge pull request #249 from boostcampwm-2022/feature/imageCache
Feature/image cache
2 parents 819288c + 653429f commit 8fe2da1

File tree

9 files changed

+315
-84
lines changed

9 files changed

+315
-84
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// CacheableImage.swift
3+
// Queenfisher
4+
//
5+
// Created by kimchansoo on 2022/12/28.
6+
// Copyright © 2022 Trinap. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public struct CacheableImage: Codable {
12+
var imageData: QFData
13+
var etag: String
14+
}

Queenfisher/Sources/ImageCache/ImageCache.swift

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,19 @@ import Foundation
1010

1111
public final class ImageCache {
1212

13-
/// 이미지 캐싱 위치를 나타냅니다.
14-
public enum Policy {
15-
case memory, disk
16-
}
17-
1813
// MARK: - Properties
1914
private static let shared = ImageCache()
2015

21-
private lazy var memoryImageCache: MemoryImageCache = {
22-
return MemoryImageCache()
23-
}()
24-
25-
private lazy var diskImageCache: DiskImageCache = {
26-
return DiskImageCache()
27-
}()
16+
private let hybridImageCache: DefaultImageCache
17+
18+
// MARK: Initializers
19+
init(performance: ConfigType = .normal) {
20+
hybridImageCache = DefaultImageCache(configType: performance)
21+
}
2822

2923
// MARK: - Methods
30-
public static func policy(_ policy: Policy) -> ImageCacheProtocol {
31-
switch policy {
32-
case .memory:
33-
return Self.shared.memoryImageCache
34-
case .disk:
35-
return Self.shared.diskImageCache
36-
}
24+
public static func instance(performance: ConfigType = .normal) -> ImageCacheProtocol {
25+
self.shared.hybridImageCache.config(performance)
26+
return self.shared.hybridImageCache
3727
}
3828
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// ImageCacheConfig.swift
3+
// Queenfisher
4+
//
5+
// Created by kimchansoo on 2022/12/27.
6+
// Copyright © 2022 Trinap. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public struct MemoryConfig {
12+
var totalCostLimit: Int = 150 * 1024 * 1024 // 메모리 캐시 최대 용량.
13+
var countLimit: Int = 50 // 메모리 캐시 최대 적재 개수.
14+
}
15+
16+
typealias DiskConfig = Int
17+
18+
public enum ConfigType {
19+
case lower
20+
case normal
21+
case high
22+
23+
var memoryConfig: MemoryConfig {
24+
switch self {
25+
case .lower:
26+
return MemoryConfig(
27+
totalCostLimit: 70 * 1024 * 1024,
28+
countLimit: 25
29+
)
30+
case .normal:
31+
return MemoryConfig()
32+
case .high:
33+
return MemoryConfig(
34+
totalCostLimit: 300 * 1024 * 1024,
35+
countLimit: 75
36+
)
37+
}
38+
}
39+
40+
var diskConfig: DiskConfig {
41+
switch self {
42+
case .lower:
43+
return 30
44+
case .normal:
45+
return 50
46+
case .high:
47+
return 70
48+
}
49+
}
50+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// ImageCacheError.swift
3+
// Queenfisher
4+
//
5+
// Created by kimchansoo on 2022/12/28.
6+
// Copyright © 2022 Trinap. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
enum ImageCacheError: Error {
12+
case imageNotModifiedError
13+
case httpResponseTransformError
14+
case unknownError
15+
}

Queenfisher/Sources/ImageCache/ImageCacheImplements.swift

Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,69 +8,100 @@
88

99
import Foundation
1010

11-
public final class MemoryImageCache: ImageCacheProtocol {
11+
public final class DefaultImageCache: ImageCacheProtocol {
1212

1313
// MARK: - Properties
14-
private let cache = NSCache<NSString, NSData>()
14+
private let memoryCache = MemoryCacheStorage()
15+
private let diskCache = DiskCacheStorage()
16+
17+
// 여기서 imageData 하나의 크기까지 지정해주려고 했는데 data 자체의 크기는 크지 않고, image로 바꾸는 연산이 들어가야하므로 빼기로 결정했습니다.
18+
// private let imageCostLimit: Int
19+
20+
// MARK: Initializers
21+
init(configType: ConfigType) {
22+
self.config(configType)
23+
}
1524

1625
// MARK: - Methods
17-
public func fetch(at url: URL, completion: @escaping (QFData?) -> Void) {
18-
let key = key(for: url)
19-
20-
if let data = cache.object(forKey: key) {
21-
completion(data as QFData)
22-
} else {
23-
fetchImage(at: url) { [weak self] fetchedData in
24-
guard let self, let fetchedData else {
25-
completion(nil)
26-
return
26+
public func fetch(at url: URL, completion: @escaping (CacheableImage?) -> Void) {
27+
memoryCache.fetch(at: url) { [weak self] cacheableImage in
28+
if let cacheableImage {
29+
completion(cacheableImage)
30+
return
31+
}
32+
33+
// disk cache fetch
34+
self?.diskCache.fetch(at: url) { [weak self] diskImage in
35+
self?.executeDiskCacheLogic(diskImage: diskImage, url: url) { image in
36+
completion(image)
2737
}
28-
self.cache.setObject(fetchedData as NSData, forKey: key)
29-
completion(fetchedData)
3038
}
3139
}
3240
}
33-
34-
private func key(for url: URL) -> NSString {
35-
return url.absoluteString as NSString
41+
42+
func config(_ configType: ConfigType) {
43+
memoryCache.config(
44+
countLimit: configType.memoryConfig.countLimit,
45+
totalCostLimit: configType.memoryConfig.totalCostLimit
46+
)
47+
diskCache.config(diskConfig: configType.diskConfig)
3648
}
3749
}
3850

39-
public final class DiskImageCache: ImageCacheProtocol {
40-
41-
// MARK: - Properties
42-
private let fileManager = FileManager.default
51+
// MARK: 이미지 캐시 로직
52+
extension DefaultImageCache {
4353

44-
private var cacheURL: URL {
45-
return fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
54+
private func executeDiskCacheLogic(diskImage: CacheableImage?, url: URL, completion: @escaping (CacheableImage?) -> Void) {
55+
// 캐시에 값 O
56+
if let diskImage {
57+
diskCacheHitted(diskImage: diskImage, url: url) { cacheableImage in
58+
completion(cacheableImage)
59+
return
60+
}
61+
}
62+
// 캐시에 값 X
63+
else {
64+
diskCacheNotHitted(url: url) { cacheableImage in
65+
completion(cacheableImage)
66+
return
67+
}
68+
}
69+
completion(nil)
4670
}
4771

48-
// MARK: - Methods
49-
public func fetch(at url: URL, completion: @escaping (QFData?) -> Void) {
50-
DispatchQueue.global().async { [weak self] in
51-
guard let self else { return }
52-
53-
let localPath = self.path(for: url)
54-
55-
if let data = try? QFData(contentsOf: localPath) {
56-
completion(data)
57-
} else {
58-
self.fetchImage(at: url) { fetchedData in
59-
guard let fetchedData else {
60-
completion(nil)
61-
return
62-
}
63-
64-
try? fetchedData.write(to: localPath)
65-
completion(fetchedData)
72+
private func diskCacheHitted(diskImage: CacheableImage, url: URL, completion: @escaping (CacheableImage?) -> Void) {
73+
self.fetchImage(at: url, etag: diskImage.etag) { [weak self] result in
74+
switch result {
75+
case .success(let networkImage): // 데이터 변경되어서 새로운 데이터 받아왔을 경우
76+
self?.diskCache.save(of: networkImage, at: url)
77+
self?.memoryCache.save(at: url, of: networkImage)
78+
completion(networkImage)
79+
return
80+
case .failure(let error):
81+
switch error {
82+
case .imageNotModifiedError: // disk cache에 있는 데이터가 변경되지 않은 데이터일 경우
83+
self?.memoryCache.save(at: url, of: diskImage)
84+
completion(diskImage)
85+
return
86+
default:
87+
completion(nil)
6688
}
6789
}
6890
}
6991
}
7092

71-
func path(for url: URL) -> URL {
72-
let imageName = url.absoluteString.replacingOccurrences(of: "/", with: "_")
73-
74-
return cacheURL.appendingPathExtension(imageName)
93+
private func diskCacheNotHitted(url: URL, completion: @escaping (CacheableImage?) -> Void) {
94+
self.fetchImage(at: url, etag: nil) { [weak self] result in
95+
switch result {
96+
case .success(let cacheableImage):
97+
self?.memoryCache.save(at: url, of: cacheableImage)
98+
self?.diskCache.save(of: cacheableImage, at: url)
99+
completion(cacheableImage)
100+
return
101+
case .failure:
102+
completion(nil)
103+
return
104+
}
105+
}
75106
}
76107
}

Queenfisher/Sources/ImageCache/ImageCacheProtocol.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,35 @@ import Foundation
1010

1111
public protocol ImageCacheProtocol {
1212

13-
func fetch(at url: URL, completion: @escaping (QFData?) -> Void)
13+
// func fetch(at url: URL, etag: String?, completion: @escaping (QFData?) -> Void)
14+
func fetch(at url: URL, completion: @escaping (CacheableImage?) -> Void)
1415
}
1516

1617
extension ImageCacheProtocol {
17-
18-
func fetchImage(at url: URL, completion: @escaping (QFData?) -> Void) {
19-
DispatchQueue.global().async {
20-
let data = try? Data(contentsOf: url)
21-
completion(data)
18+
19+
func fetchImage(at url: URL, etag: String?, completion: @escaping (Result<CacheableImage, ImageCacheError>) -> Void) {
20+
var urlRequest = URLRequest(url: url)
21+
22+
if let etag = etag {
23+
urlRequest.addValue(etag, forHTTPHeaderField: "If-None-Match")
24+
}
25+
26+
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
27+
guard let response = response as? HTTPURLResponse else {
28+
completion(.failure(.httpResponseTransformError))
29+
return
30+
}
31+
switch response.statusCode {
32+
case (200...299):
33+
guard let data else { completion(.failure(.unknownError)); return }
34+
let etag = response.allHeaderFields["Etag"] as? String ?? ""
35+
let image = CacheableImage(imageData: data, etag: etag)
36+
completion(.success(image))
37+
case 304:
38+
completion(.failure(.imageNotModifiedError))
39+
default:
40+
completion(.failure(.unknownError))
41+
}
2242
}
2343
}
2444
}

0 commit comments

Comments
 (0)