Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
//
2
//  ImageCache.swift
3
//
4
//  Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
5
//
6
//  Permission is hereby granted, free of charge, to any person obtaining a copy
7
//  of this software and associated documentation files (the "Software"), to deal
8
//  in the Software without restriction, including without limitation the rights
9
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
//  copies of the Software, and to permit persons to whom the Software is
11
//  furnished to do so, subject to the following conditions:
12
//
13
//  The above copyright notice and this permission notice shall be included in
14
//  all copies or substantial portions of the Software.
15
//
16
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
//  THE SOFTWARE.
23
//
24
 
25
import Alamofire
26
import Foundation
27
 
28
#if os(iOS) || os(tvOS) || os(watchOS)
29
import UIKit
30
#elseif os(macOS)
31
import Cocoa
32
#endif
33
 
34
// MARK: ImageCache
35
 
36
/// The `ImageCache` protocol defines a set of APIs for adding, removing and fetching images from a cache.
37
public protocol ImageCache {
38
    /// Adds the image to the cache with the given identifier.
39
    func add(_ image: Image, withIdentifier identifier: String)
40
 
41
    /// Removes the image from the cache matching the given identifier.
42
    func removeImage(withIdentifier identifier: String) -> Bool
43
 
44
    /// Removes all images stored in the cache.
45
    @discardableResult
46
    func removeAllImages() -> Bool
47
 
48
    /// Returns the image in the cache associated with the given identifier.
49
    func image(withIdentifier identifier: String) -> Image?
50
}
51
 
52
/// The `ImageRequestCache` protocol extends the `ImageCache` protocol by adding methods for adding, removing and
53
/// fetching images from a cache given an `URLRequest` and additional identifier.
54
public protocol ImageRequestCache: ImageCache {
55
    /// Adds the image to the cache using an identifier created from the request and identifier.
56
    func add(_ image: Image, for request: URLRequest, withIdentifier identifier: String?)
57
 
58
    /// Removes the image from the cache using an identifier created from the request and identifier.
59
    func removeImage(for request: URLRequest, withIdentifier identifier: String?) -> Bool
60
 
61
    /// Returns the image from the cache associated with an identifier created from the request and identifier.
62
    func image(for request: URLRequest, withIdentifier identifier: String?) -> Image?
63
}
64
 
65
// MARK: -
66
 
67
/// The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When
68
/// the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously
69
/// purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the
70
/// internal access date of the image is updated.
71
open class AutoPurgingImageCache: ImageRequestCache {
72
    class CachedImage {
73
        let image: Image
74
        let identifier: String
75
        let totalBytes: UInt64
76
        var lastAccessDate: Date
77
 
78
        init(_ image: Image, identifier: String) {
79
            self.image = image
80
            self.identifier = identifier
81
            lastAccessDate = Date()
82
 
83
            totalBytes = {
84
                #if os(iOS) || os(tvOS) || os(watchOS)
85
                let size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
86
                #elseif os(macOS)
87
                let size = CGSize(width: image.size.width, height: image.size.height)
88
                #endif
89
 
90
                let bytesPerPixel: CGFloat = 4.0
91
                let bytesPerRow = size.width * bytesPerPixel
92
                let totalBytes = UInt64(bytesPerRow) * UInt64(size.height)
93
 
94
                return totalBytes
95
            }()
96
        }
97
 
98
        func accessImage() -> Image {
99
            lastAccessDate = Date()
100
            return image
101
        }
102
    }
103
 
104
    // MARK: Properties
105
 
106
    /// The current total memory usage in bytes of all images stored within the cache.
107
    open var memoryUsage: UInt64 {
108
        var memoryUsage: UInt64 = 0
109
        synchronizationQueue.sync(flags: [.barrier]) { memoryUsage = self.currentMemoryUsage }
110
 
111
        return memoryUsage
112
    }
113
 
114
    /// The total memory capacity of the cache in bytes.
115
    public let memoryCapacity: UInt64
116
 
117
    /// The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory
118
    /// capacity drops below this limit.
119
    public let preferredMemoryUsageAfterPurge: UInt64
120
 
121
    private let synchronizationQueue: DispatchQueue
122
    private var cachedImages: [String: CachedImage]
123
    private var currentMemoryUsage: UInt64
124
 
125
    // MARK: Initialization
126
 
127
    /// Initializes the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage
128
    /// after purge limit.
129
    ///
130
    /// Please note, the memory capacity must always be greater than or equal to the preferred memory usage after purge.
131
    ///
132
    /// - parameter memoryCapacity:                 The total memory capacity of the cache in bytes. `100 MB` by default.
133
    /// - parameter preferredMemoryUsageAfterPurge: The preferred memory usage after purge in bytes. `60 MB` by default.
134
    ///
135
    /// - returns: The new `AutoPurgingImageCache` instance.
136
    public init(memoryCapacity: UInt64 = 100_000_000, preferredMemoryUsageAfterPurge: UInt64 = 60_000_000) {
137
        self.memoryCapacity = memoryCapacity
138
        self.preferredMemoryUsageAfterPurge = preferredMemoryUsageAfterPurge
139
 
140
        precondition(memoryCapacity >= preferredMemoryUsageAfterPurge,
141
                     "The `memoryCapacity` must be greater than or equal to `preferredMemoryUsageAfterPurge`")
142
 
143
        cachedImages = [:]
144
        currentMemoryUsage = 0
145
 
146
        synchronizationQueue = {
147
            let name = String(format: "org.alamofire.autopurgingimagecache-%08x%08x", arc4random(), arc4random())
148
            return DispatchQueue(label: name, attributes: .concurrent)
149
        }()
150
 
151
        #if os(iOS) || os(tvOS)
152
        let notification = UIApplication.didReceiveMemoryWarningNotification
153
 
154
        NotificationCenter.default.addObserver(self,
155
                                               selector: #selector(AutoPurgingImageCache.removeAllImages),
156
                                               name: notification,
157
                                               object: nil)
158
        #endif
159
    }
160
 
161
    deinit {
162
        NotificationCenter.default.removeObserver(self)
163
    }
164
 
165
    // MARK: Add Image to Cache
166
 
167
    /// Adds the image to the cache using an identifier created from the request and optional identifier.
168
    ///
169
    /// - parameter image:      The image to add to the cache.
170
    /// - parameter request:    The request used to generate the image's unique identifier.
171
    /// - parameter identifier: The additional identifier to append to the image's unique identifier.
172
    open func add(_ image: Image, for request: URLRequest, withIdentifier identifier: String? = nil) {
173
        let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)
174
        add(image, withIdentifier: requestIdentifier)
175
    }
176
 
177
    /// Adds the image to the cache with the given identifier.
178
    ///
179
    /// - parameter image:      The image to add to the cache.
180
    /// - parameter identifier: The identifier to use to uniquely identify the image.
181
    open func add(_ image: Image, withIdentifier identifier: String) {
182
        synchronizationQueue.async(flags: [.barrier]) {
183
            let cachedImage = CachedImage(image, identifier: identifier)
184
 
185
            if let previousCachedImage = self.cachedImages[identifier] {
186
                self.currentMemoryUsage -= previousCachedImage.totalBytes
187
            }
188
 
189
            self.cachedImages[identifier] = cachedImage
190
            self.currentMemoryUsage += cachedImage.totalBytes
191
        }
192
 
193
        synchronizationQueue.async(flags: [.barrier]) {
194
            if self.currentMemoryUsage > self.memoryCapacity {
195
                let bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge
196
 
197
                var sortedImages = self.cachedImages.map { $1 }
198
 
199
                sortedImages.sort {
200
                    let date1 = $0.lastAccessDate
201
                    let date2 = $1.lastAccessDate
202
 
203
                    return date1.timeIntervalSince(date2) < 0.0
204
                }
205
 
206
                var bytesPurged = UInt64(0)
207
 
208
                for cachedImage in sortedImages {
209
                    self.cachedImages.removeValue(forKey: cachedImage.identifier)
210
                    bytesPurged += cachedImage.totalBytes
211
 
212
                    if bytesPurged >= bytesToPurge {
213
                        break
214
                    }
215
                }
216
 
217
                self.currentMemoryUsage -= bytesPurged
218
            }
219
        }
220
    }
221
 
222
    // MARK: Remove Image from Cache
223
 
224
    /// Removes the image from the cache using an identifier created from the request and optional identifier.
225
    ///
226
    /// - parameter request:    The request used to generate the image's unique identifier.
227
    /// - parameter identifier: The additional identifier to append to the image's unique identifier.
228
    ///
229
    /// - returns: `true` if the image was removed, `false` otherwise.
230
    @discardableResult
231
    open func removeImage(for request: URLRequest, withIdentifier identifier: String?) -> Bool {
232
        let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)
233
        return removeImage(withIdentifier: requestIdentifier)
234
    }
235
 
236
    /// Removes all images from the cache created from the request.
237
    ///
238
    /// - parameter request: The request used to generate the image's unique identifier.
239
    ///
240
    /// - returns: `true` if any images were removed, `false` otherwise.
241
    @discardableResult
242
    open func removeImages(matching request: URLRequest) -> Bool {
243
        let requestIdentifier = imageCacheKey(for: request, withIdentifier: nil)
244
        var removed = false
245
 
246
        synchronizationQueue.sync(flags: [.barrier]) {
247
            for key in self.cachedImages.keys where key.hasPrefix(requestIdentifier) {
248
                if let cachedImage = self.cachedImages.removeValue(forKey: key) {
249
                    self.currentMemoryUsage -= cachedImage.totalBytes
250
                    removed = true
251
                }
252
            }
253
        }
254
 
255
        return removed
256
    }
257
 
258
    /// Removes the image from the cache matching the given identifier.
259
    ///
260
    /// - parameter identifier: The unique identifier for the image.
261
    ///
262
    /// - returns: `true` if the image was removed, `false` otherwise.
263
    @discardableResult
264
    open func removeImage(withIdentifier identifier: String) -> Bool {
265
        var removed = false
266
 
267
        synchronizationQueue.sync(flags: [.barrier]) {
268
            if let cachedImage = self.cachedImages.removeValue(forKey: identifier) {
269
                self.currentMemoryUsage -= cachedImage.totalBytes
270
                removed = true
271
            }
272
        }
273
 
274
        return removed
275
    }
276
 
277
    /// Removes all images stored in the cache.
278
    ///
279
    /// - returns: `true` if images were removed from the cache, `false` otherwise.
280
    @discardableResult @objc
281
    open func removeAllImages() -> Bool {
282
        var removed = false
283
 
284
        synchronizationQueue.sync(flags: [.barrier]) {
285
            if !self.cachedImages.isEmpty {
286
                self.cachedImages.removeAll()
287
                self.currentMemoryUsage = 0
288
 
289
                removed = true
290
            }
291
        }
292
 
293
        return removed
294
    }
295
 
296
    // MARK: Fetch Image from Cache
297
 
298
    /// Returns the image from the cache associated with an identifier created from the request and optional identifier.
299
    ///
300
    /// - parameter request:    The request used to generate the image's unique identifier.
301
    /// - parameter identifier: The additional identifier to append to the image's unique identifier.
302
    ///
303
    /// - returns: The image if it is stored in the cache, `nil` otherwise.
304
    open func image(for request: URLRequest, withIdentifier identifier: String? = nil) -> Image? {
305
        let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)
306
        return image(withIdentifier: requestIdentifier)
307
    }
308
 
309
    /// Returns the image in the cache associated with the given identifier.
310
    ///
311
    /// - parameter identifier: The unique identifier for the image.
312
    ///
313
    /// - returns: The image if it is stored in the cache, `nil` otherwise.
314
    open func image(withIdentifier identifier: String) -> Image? {
315
        var image: Image?
316
 
317
        synchronizationQueue.sync(flags: [.barrier]) {
318
            if let cachedImage = self.cachedImages[identifier] {
319
                image = cachedImage.accessImage()
320
            }
321
        }
322
 
323
        return image
324
    }
325
 
326
    // MARK: Image Cache Keys
327
 
328
    /// Returns the unique image cache key for the specified request and additional identifier.
329
    ///
330
    /// - parameter request:    The request.
331
    /// - parameter identifier: The additional identifier.
332
    ///
333
    /// - returns: The unique image cache key.
334
    open func imageCacheKey(for request: URLRequest, withIdentifier identifier: String?) -> String {
335
        var key = request.url?.absoluteString ?? ""
336
 
337
        if let identifier = identifier {
338
            key += "-\(identifier)"
339
        }
340
 
341
        return key
342
    }
343
}