develop

Cache 앱의 캐시 사용하기 본문

iOS

Cache 앱의 캐시 사용하기

pikachu987 2021. 1. 23. 20:15
반응형

MemoryCache

메모리 캐시는 NSCache를 사용하여 캐시를 관리한다.

메모리를 사용하기 때문에 앱의 메모리가 커지면 캐시가 삭제될 수 있다.

 

Swift에서는 NSCache라는 클래스가 있다.

https://developer.apple.com/documentation/foundation/nscache

 

Apple Developer Documentation

 

developer.apple.com

class NSCache<KeyType, ObjectType> : NSObject where KeyType : AnyObject, ObjectType : AnyObject

Key와 Value를 Generic으로 구현되어 있다.

사용 방법은

let cache = NSCache<NSString, UIImage>()

 

SET

cache.setObject(UIImage(), forKey: "image1")
cache.setObject(UIImage(), forKey: "image2")
cache.setObject(UIImage(), forKey: "image3", cost: 2)

cost는 0이 default이다.

 

GET

print(cache.object(forKey: "image1"))
print(cache.object(forKey: "image2"))

 

REMOVE

cache.removeObject(forKey: "image1")
cache.removeObject(forKey: "image2")
cache.removeAllObjects()

 

LIMIT

cache.countLimit = 50
cache.totalCostLimit = 60

countLimit은 제한 갯수이고

totalCostLimit는 set할때 cost를 인자로 받는데 cost의 총 합이다.

LIMIT에 도달하면 cost기준으로 낮은 cost가 먼저 지워진다.

 

NSCache는 memory에 저장되고 disk에 저장하려면 FileManager를 사용하여야 한다.

class MemoryCache: NSObject {
    static let shared = MemoryCache(name: "cache")
    
    let cache = NSCache<NSString, NSData>()
    
    init(name: String, countLimit: Int = 50) {
        self.cache.name = name
        self.cache.countLimit = countLimit
    }
    
    subscript(_ key: String) -> Data? {
        set {
            if let data = newValue {
                self.cache.setObject(NSData(data: data), forKey: key as NSString)
            } else {
                self.cache.removeObject(forKey: key as NSString)
            }
        }
        get {
            guard let data = self.cache.object(forKey: key as NSString) else { return nil }
            return Data(referencing: data)
        }
    }
    
    func removeAll() {
        self.cache.removeAllObjects()
    }
}
let memoryCache = MemoryCache(name: "customCache")
memoryCache["http://www.image.com/image1.png"] = Data()
memoryCache["http://www.image.com/image2.png"] = Data()
memoryCache["http://www.image.com/image3.png"] = Data()

MemoryCache.shared["http://www.image.com/image1.png"] = Data()
MemoryCache.shared["http://www.image.com/image2.png"] = Data()
MemoryCache.shared["http://www.image.com/image3.png"] = Data()


DiskCache

 

디스크 캐시는 데이터를 파일로 저장을 한다.

디스크 캐시를 사용하게 되면 앱의 용량이 커질 수 있다.

 

 

DocumentDirectory 가져오기

FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first

 

폴더 생성되어 있는지 확인

FileManager.default.fileExists(atPath: folderURL.path)

 

폴더 생성

FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil)

 

파일 저장

try? Date().write(to: writeURL)

 

파일 불러오기

FileManager.default.contents(atPath: writeURL.path)

또는

try? Data(contentsOf: writeURL.absoluteString)

 

폴더 또는 파일 삭제

try? FileManager.default.removeItem(at: writeURL)

 

class DiskCache: NSObject {
    static let shared = DiskCache(name: "cache")
    
    var folderURL: URL? {
        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(self.name)
    }
    
    var name: String = "cache"
    var countLimit: Int
    var sizeLimit: Int
    
    init(name: String, countLimit: Int = 3, sizeLimit: Int = 1000*1000*100) {
        self.name = name
        self.countLimit = countLimit
        self.sizeLimit = sizeLimit
				super.init()
        self.createFolder()
    }
    
    subscript(_ key: String) -> Data? {
        set {
            guard let fileURL = URL(string: key) else { return }
            guard let writeURL = self.folderURL?.appendingPathComponent(fileURL.lastPathComponent) else { return }
            if let data = newValue {
                try? data.write(to: writeURL)
            } else {
                try? FileManager.default.removeItem(at: writeURL)
            }
            self.sweep()
        }
        get {
            guard let fileURL = URL(string: key) else { return nil }
            guard let writeURL = self.folderURL?.appendingPathComponent(fileURL.lastPathComponent) else { return nil }
            return FileManager.default.contents(atPath: writeURL.path)
        }
    }
    
    func removeAll() {
        if let folderURL = self.folderURL {
            try? FileManager.default.removeItem(at: folderURL)
        }
        self.createFolder()
    }
    
    private func createFolder() {
        guard let folderURL = self.folderURL else { return }
        if FileManager.default.fileExists(atPath: folderURL.path) { return }
        try? FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil)
    }
    
    
    struct Entry {
        let url: URL
        let meta: URLResourceValues
        
        init?(url: URL?, meta: URLResourceValues?) {
            guard let url = url else { return nil }
            guard let meta = meta else { return nil }
            self.url = url
            self.meta = meta
        }
    }
    
    private func sweep() {
        guard let folderURL = self.folderURL else { return }
        let keys: [URLResourceKey] = [.contentAccessDateKey, .totalFileAllocatedSizeKey, .contentModificationDateKey]
        guard let urls = try? FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: keys, options: .skipsHiddenFiles) else { return }
        if urls.isEmpty { return }
        let past = Date.distantPast
        let forKeys = Set(keys)
        var entrys = urls.compactMap { (url) -> Entry? in
            return Entry(url: url, meta: try? url.resourceValues(forKeys: forKeys))
        }.sorted(by: {
            let firstModDate = $0.meta.contentModificationDate ?? past
            let firstAccDate = $0.meta.contentAccessDate ?? past
            let firstDate = firstModDate > firstAccDate ? firstModDate : firstAccDate
            let lastModDate = $1.meta.contentModificationDate ?? past
            let lastAccDate = $1.meta.contentAccessDate ?? past
            let lastDate = lastModDate > lastAccDate ? lastModDate : lastAccDate
            return firstDate > lastDate
        })
        var count = entrys.count
        var totalSize = entrys.reduce(0, { $0 + ($1.meta.totalFileAllocatedSize ?? 0) })
        guard count > self.countLimit || totalSize > self.sizeLimit else { return }
        
        while ( count > self.countLimit || totalSize > self.sizeLimit ), let entry = entrys.popLast() {
            count -= 1
            totalSize -= (entry.meta.totalFileAllocatedSize ?? 0)
            try? FileManager.default.removeItem(at: entry.url)
        }
    }
}
let diskCache = DiskCache(name: "customCache")
diskCache["http://www.image.com/image1.png"] = Data()
diskCache["http://www.image.com/image2.png"] = Data()
diskCache["http://www.image.com/image3.png"] = Data()

DiskCache.shared["http://www.image.com/image1.png"] = Data()
DiskCache.shared["http://www.image.com/image2.png"] = Data()
DiskCache.shared["http://www.image.com/image3.png"] = Data()

 

 

MemoryCache와 DiskCache를 같이 쓰고 MemoryCache에 데이터가 없으면 DiskCache에 데이터를 조회하는 코드이다.

DiskCache를 사용하려면 FileManager를 이용하여야 한다.

class Cache: NSObject {
    static let shared = Cache(name: "cache")
    
    let memoryCache: MemoryCache
    let diskCache: DiskCache
    
    init(name: String, countLimit: Int = 3, sizeLimit: Int = 1000*1000*100) {
        self.memoryCache = MemoryCache(name: name, countLimit: countLimit)
        self.diskCache = DiskCache(name: name, countLimit: countLimit, sizeLimit: sizeLimit)
    }
    
    subscript(_ key: String) -> Data? {
        set {
            self.memoryCache[key] = newValue
            self.diskCache[key] = newValue
        }
        get {
            if let data = self.memoryCache[key] {
                return data
            } else {
                return self.diskCache[key]
            }
        }
    }
    
    func removeAll() {
        self.memoryCache.removeAll()
        self.diskCache.removeAll()
    }
}
let cache = Cache(name: "customCache")
cache["http://www.image.com/image1.png"] = Data()
cache["http://www.image.com/image2.png"] = Data()
cache["http://www.image.com/image3.png"] = Data()

Cache.shared["http://www.image.com/image1.png"] = Data()
Cache.shared["http://www.image.com/image2.png"] = Data()
Cache.shared["http://www.image.com/image3.png"] = Data()
반응형

'iOS' 카테고리의 다른 글

autoreleasepool  (0) 2021.01.25
BackgroundFetch 앱 백그라운드 상태에서 로직 실행하기  (0) 2021.01.24
Generic  (0) 2021.01.21
Throttle, Debounce  (0) 2021.01.20
Declaration Attributes  (0) 2021.01.19
Comments