Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Sources/ImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,17 @@ public final class ImageCache: ImageCaching {

/// Returns the `ImageResponse` stored in the cache with the given request.
public func cachedResponse(for request: ImageRequest) -> ImageResponse? {
return impl.value(forKey: ImageRequest.CacheKey(request: request))
return impl.value(forKey: request.makeCacheKeyForProcessedImage())
}

/// Stores the given `ImageResponse` in the cache using the given request.
public func storeResponse(_ response: ImageResponse, for request: ImageRequest) {
impl.set(response, forKey: ImageRequest.CacheKey(request: request), cost: self.cost(for: response.image))
impl.set(response, forKey: request.makeCacheKeyForProcessedImage(), cost: self.cost(for: response.image))
}

/// Removes response stored with the given request.
public func removeResponse(for request: ImageRequest) {
impl.removeValue(forKey: ImageRequest.CacheKey(request: request))
impl.removeValue(forKey: request.makeCacheKeyForProcessedImage())
}

/// Removes all cached images.
Expand Down
18 changes: 10 additions & 8 deletions Sources/ImagePipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public /* final */ class ImagePipeline {
private typealias DecompressedImageFetchTask = Task<ImageResponse, Error>

private func getDecompressedImage(for request: ImageRequest) -> DecompressedImageFetchTask {
let key = ImageRequest.ImageLoadKey(request: request)
let key = request.makeLoadKeyForProcessedImage()
return decompressedImageFetchTasks.task(withKey: key) { job in
self.loadDecompressedImage(for: request, job: job)
}
Expand Down Expand Up @@ -277,7 +277,7 @@ public /* final */ class ImagePipeline {
return getOriginalImage(for: request) // No processing needed
}

let key = ImageRequest.ImageLoadKey(request: request)
let key = request.makeLoadKeyForProcessedImage()
return processedImageFetchTasks.task(withKey: key) { job in
self.loadProcessedImage(for: request, job: job)
}
Expand All @@ -292,7 +292,7 @@ public /* final */ class ImagePipeline {
return loadOriginaImage(for: request, job: job)
}

let key = (request.urlString ?? "") + ImageProcessor.Composition(request.processors).identifier
let key = request.makeCacheKeyForProcessedImageData()

let operation = BlockOperation { [weak self, weak job] in
guard let self = self, let job = job else { return }
Expand Down Expand Up @@ -415,7 +415,7 @@ public /* final */ class ImagePipeline {
signpost.log(.end, name: "Encode Image")

guard let data = encodedData else { return }
let key = (request.urlString ?? "") + ImageProcessor.Composition(request.processors).identifier
let key = request.makeCacheKeyForProcessedImageData()
dataCache.storeData(data, for: key) // This is instant
}
}
Expand All @@ -434,7 +434,7 @@ public /* final */ class ImagePipeline {
}

private func getOriginalImage(for request: ImageRequest) -> OriginalImageFetchTask {
let key = ImageRequest.LoadKey(request: request)
let key = request.makeLoadKeyForOriginalImage()
return originalImageFetchTasks.task(withKey: key) { job in
let context = OriginalImageFetchContext(request: request)
let task = self.getOriginalImageData(for: request)
Expand Down Expand Up @@ -512,7 +512,7 @@ public /* final */ class ImagePipeline {
}

private func getOriginalImageData(for request: ImageRequest) -> OriginalImageDataFetchTask {
let key = ImageRequest.LoadKey(request: request)
let key = request.makeLoadKeyForOriginalImage()
return originalImageDataFetchTasks.task(withKey: key) { job in
let context = OriginalImageDataFetchContext(request: request)
if self.configuration.isRateLimiterEnabled {
Expand All @@ -532,11 +532,12 @@ public /* final */ class ImagePipeline {
}

private func loadImageDataFromCache(for job: OriginalImageDataFetchTask.Job, context: OriginalImageDataFetchContext) {
guard let cache = configuration.dataCache, configuration.isDataCachingForOriginalImageDataEnabled, let key = context.request.urlString else {
guard let cache = configuration.dataCache, configuration.isDataCachingForOriginalImageDataEnabled else {
loadImageData(for: job, context: context) // Skip disk cache lookup, load data
return
}

let key = context.request.makeCacheKeyForOriginalImageData()
let operation = BlockOperation { [weak self, weak job] in
guard let self = self, let job = job else { return }

Expand Down Expand Up @@ -675,7 +676,8 @@ public /* final */ class ImagePipeline {
}

// Store in data cache
if let dataCache = configuration.dataCache, configuration.isDataCachingForOriginalImageDataEnabled, let key = context.request.urlString {
if let dataCache = configuration.dataCache, configuration.isDataCachingForOriginalImageDataEnabled {
let key = context.request.makeCacheKeyForOriginalImageData()
dataCache.storeData(context.data, for: key)
}

Expand Down
10 changes: 5 additions & 5 deletions Sources/ImagePreheater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public final class ImagePreheater {
private let pipeline: ImagePipeline
private let queue = DispatchQueue(label: "com.github.kean.Nuke.Preheater")
private let preheatQueue = OperationQueue()
private var tasks = [ImageRequest.ImageLoadKey: Task]()
private var tasks = [AnyHashable: Task]()
private let destination: Destination

/// Prefetching destination.
Expand Down Expand Up @@ -63,7 +63,7 @@ public final class ImagePreheater {
}

private func _startPreheating(with request: ImageRequest) {
let key = ImageRequest.ImageLoadKey(request: request)
let key = request.makeLoadKeyForProcessedImage()

// Check if we we've already started preheating.
guard tasks[key] == nil else {
Expand Down Expand Up @@ -141,7 +141,7 @@ public final class ImagePreheater {
}

private func _stopPreheating(with request: ImageRequest) {
if let task = tasks[ImageRequest.ImageLoadKey(request: request)] {
if let task = tasks[request.makeLoadKeyForProcessedImage()] {
tasks[task.key] = nil
task.cancel()
}
Expand All @@ -164,13 +164,13 @@ public final class ImagePreheater {
}

private final class Task {
let key: ImageRequest.ImageLoadKey
let key: AnyHashable
let request: ImageRequest
var isCancelled = false
var onCancelled: (() -> Void)?
weak var operation: Operation?

init(request: ImageRequest, key: ImageRequest.ImageLoadKey) {
init(request: ImageRequest, key: AnyHashable) {
self.request = request
self.key = key
}
Expand Down
109 changes: 68 additions & 41 deletions Sources/ImageRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ public struct ImageRequestOptions {
/// `MemoryCacheOptions()` (read allowed, write allowed) by default.
public var memoryCacheOptions: MemoryCacheOptions

/// In some cases your image URLs might contains transient query parameters
/// like access tokens which must be ignored when generating cache keys. If
/// that's the case, set this property to a URL without the unwanted fields.
public var filteredURL: String?

/// Returns a key that compares requests with regards to caching images.
///
/// The default key considers two requests equivalent it they have the same
Expand All @@ -182,79 +187,101 @@ public struct ImageRequestOptions {
public var userInfo: Any?

public init(memoryCacheOptions: MemoryCacheOptions = .init(),
filteredURL: String? = nil,
cacheKey: AnyHashable? = nil,
loadKey: AnyHashable? = nil,
userInfo: Any? = nil) {
self.memoryCacheOptions = memoryCacheOptions
self.filteredURL = filteredURL
self.cacheKey = cacheKey
self.loadKey = loadKey
self.userInfo = userInfo
}
}

// MARK: - ImageRequest (Internal)
// MARK: - ImageRequestKeys (Internal)

extension ImageRequest {

// MARK: - Cache Keys

/// A key for processed image in memory cache.
func makeCacheKeyForProcessedImage() -> ImageRequest.CacheKey {
return CacheKey(request: self)
}

/// A key for processed image data in disk cache.
func makeCacheKeyForProcessedImageData() -> String {
return preferredURLString + ImageProcessor.Composition(processors).identifier
}

/// A key for original image data in disk cache.
func makeCacheKeyForOriginalImageData() -> String {
return preferredURLString
}

private var preferredURLString: String {
return options.filteredURL ?? urlString ?? ""
}

// MARK: - Load Keys

/// A key for deduplicating operations for fetching the processed image.
func makeLoadKeyForProcessedImage() -> AnyHashable {
return LoadKeyForProcessedImage(cacheKey: makeCacheKeyForProcessedImage(),
loadKey: makeLoadKeyForOriginalImage())
}

/// A key for deduplicating operations for fetching the original image.
func makeLoadKeyForOriginalImage() -> AnyHashable {
if let loadKey = self.options.loadKey {
return loadKey
}
return LoadKeyForOriginalImage(request: self)
}

// MARK: - Internals (Keys)

// Uniquely identifies a cache processed image.
struct CacheKey: Hashable {
let request: ImageRequest

func hash(into hasher: inout Hasher) {
if let customKey = request.ref.options.cacheKey {
if let customKey = request.options.cacheKey {
hasher.combine(customKey)
} else {
hasher.combine(request.ref.urlString?.hashValue ?? 0)
hasher.combine(request.preferredURLString)
}
}

/// The implementaion is a bit clever because we want to achieve good
/// performance when using memory cache, so we can't simply go with
/// `AnyHashable` like we do for load keys.
static func == (lhs: CacheKey, rhs: CacheKey) -> Bool {
let lhs = lhs.request.ref, rhs = rhs.request.ref
if let lhsCustomKey = lhs.options.cacheKey, let rhsCustomKey = rhs.options.cacheKey {
return lhsCustomKey == rhsCustomKey
}
guard lhs.urlString == rhs.urlString else {
return false
let lhs = lhs.request, rhs = rhs.request
if lhs.options.cacheKey != nil || rhs.options.cacheKey != nil {
return lhs.options.cacheKey == rhs.options.cacheKey
}

return lhs.processors == rhs.processors
return lhs.preferredURLString == rhs.preferredURLString && lhs.processors == rhs.processors
}
}

// Uniquely identifies a task of retrieving the processed image.
struct ImageLoadKey: Hashable {
private struct LoadKeyForProcessedImage: Hashable {
let cacheKey: CacheKey
let loadKey: LoadKey

init(request: ImageRequest) {
self.cacheKey = CacheKey(request: request)
self.loadKey = LoadKey(request: request)
}
let loadKey: AnyHashable
}

/// Uniquely identifies a task of loading image data.
struct LoadKey: Hashable {
let request: ImageRequest
private struct LoadKeyForOriginalImage: Hashable {
let urlString: String?
let cachePolicy: URLRequest.CachePolicy
let allowsCellularAccess: Bool

func hash(into hasher: inout Hasher) {
if let customKey = request.ref.options.loadKey {
hasher.combine(customKey)
} else {
hasher.combine(request.ref.urlString?.hashValue ?? 0)
}
}

static func == (lhs: LoadKey, rhs: LoadKey) -> Bool {
func isEqual(_ lhs: URLRequest, _ rhs: URLRequest) -> Bool {
return lhs.cachePolicy == rhs.cachePolicy
&& lhs.allowsCellularAccess == rhs.allowsCellularAccess
}

let lhs = lhs.request.ref, rhs = rhs.request.ref
if let lhsCustomKey = lhs.options.loadKey, let rhsCustomKey = rhs.options.loadKey {
return lhsCustomKey == rhsCustomKey
}
return lhs.urlString == rhs.urlString
&& isEqual(lhs.resource.urlRequest, rhs.resource.urlRequest)
init(request: ImageRequest) {
self.urlString = request.urlString
let urlRequest = request.urlRequest
self.cachePolicy = urlRequest.cachePolicy
self.allowsCellularAccess = urlRequest.allowsCellularAccess
}
}
}
15 changes: 7 additions & 8 deletions Tests/DeprecatedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -658,25 +658,25 @@ class DeprecatedImageRequestCacheKeyTests: XCTestCase {
class DeprecatedImageRequestLoadKeyTests: XCTestCase {
func testDefaults() {
let request = ImageRequest(url: Test.url)
AssertHashableEqual(LoadKey(request: request), LoadKey(request: request))
AssertHashableEqual(request.makeLoadKeyForOriginalImage(), request.makeLoadKeyForOriginalImage())
}

func testRequestsWithTheSameURLsAreEquivalent() {
let request1 = ImageRequest(url: Test.url)
let request2 = ImageRequest(url: Test.url)
AssertHashableEqual(LoadKey(request: request1), LoadKey(request: request2))
AssertHashableEqual(request1.makeLoadKeyForOriginalImage(), request2.makeLoadKeyForOriginalImage())
}

func testRequestsWithDifferentURLsAreNotEquivalent() {
let request1 = ImageRequest(url: URL(string: "http://test.com/1.png")!)
let request2 = ImageRequest(url: URL(string: "http://test.com/2.png")!)
XCTAssertNotEqual(LoadKey(request: request1), LoadKey(request: request2))
XCTAssertNotEqual(request1.makeLoadKeyForOriginalImage(), request2.makeLoadKeyForOriginalImage())
}

func testRequestWithDifferentURLRequestParametersAreNotEquivalent() {
let request1 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50))
let request2 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))
XCTAssertNotEqual(LoadKey(request: request1), LoadKey(request: request2))
XCTAssertNotEqual(request1.makeLoadKeyForOriginalImage(), request2.makeLoadKeyForOriginalImage())
}

// MARK: - Custom Load Key
Expand All @@ -686,28 +686,27 @@ class DeprecatedImageRequestLoadKeyTests: XCTestCase {
request1.loadKey = "1"
var request2 = ImageRequest(url: Test.url)
request2.loadKey = "1"
AssertHashableEqual(LoadKey(request: request1), LoadKey(request: request2))
AssertHashableEqual(request1.makeLoadKeyForOriginalImage(), request2.makeLoadKeyForOriginalImage())
}

func testRequestsWithSameCustomKeysButDifferentURLsAreEquivalent() {
var request1 = ImageRequest(url: URL(string: "https://example.com/photo1.jpg")!)
request1.loadKey = "1"
var request2 = ImageRequest(url: URL(string: "https://example.com/photo2.jpg")!)
request2.loadKey = "1"
AssertHashableEqual(LoadKey(request: request1), LoadKey(request: request2))
AssertHashableEqual(request1.makeLoadKeyForOriginalImage(), request2.makeLoadKeyForOriginalImage())
}

func testRequestsWithDifferentCustomKeysAreNotEquivalent() {
var request1 = ImageRequest(url: Test.url)
request1.loadKey = "1"
var request2 = ImageRequest(url: Test.url)
request2.loadKey = "2"
XCTAssertNotEqual(LoadKey(request: request1), LoadKey(request: request2))
XCTAssertNotEqual(request1.makeLoadKeyForOriginalImage(), request2.makeLoadKeyForOriginalImage())
}
}

private typealias CacheKey = ImageRequest.CacheKey
private typealias LoadKey = ImageRequest.LoadKey

private func AssertHashableEqual<T: Hashable>(_ lhs: T, _ rhs: T, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(lhs.hashValue, rhs.hashValue, file: file, line: line)
Expand Down
Loading