diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index 0cedd923c..8605c29cd 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -18,11 +18,28 @@ extension Notification.Name { final class FaviconDownloader { + private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0) + private let folder: String private let diskCache: BinaryDiskCache private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader + private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL - private var homePageURLsWithNoFaviconURL = Set() + private var homePageToFaviconURLCachePath: String + private var homePageToFaviconURLCacheDirty = false { + didSet { + queueSaveHomePageToFaviconURLCacheIfNeeded() + } + } + + private var homePageURLsWithNoFaviconURLCache = Set() + private var homePageURLsWithNoFaviconURLCachePath: String + private var homePageURLsWithNoFaviconURLCacheDirty = false { + didSet { + queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() + } + } + private let queue: DispatchQueue private var cache = [Feed: RSImage]() // faviconURL: RSImage @@ -36,6 +53,11 @@ final class FaviconDownloader { self.diskCache = BinaryDiskCache(folder: folder) self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)") + self.homePageToFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageToFaviconURLCache.plist") + self.homePageURLsWithNoFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist") + loadHomePageToFaviconURLCache() + loadHomePageURLsWithNoFaviconURLCache() + NotificationCenter.default.addObserver(self, selector: #selector(didLoadFavicon(_:)), name: .DidLoadFavicon, object: nil) } @@ -92,7 +114,7 @@ final class FaviconDownloader { func favicon(withHomePageURL homePageURL: String) -> RSImage? { let url = homePageURL.rs_normalizedURL() - if homePageURLsWithNoFaviconURL.contains(url) { + if homePageURLsWithNoFaviconURLCache.contains(url) { return nil } @@ -103,10 +125,12 @@ final class FaviconDownloader { findFaviconURL(with: url) { (faviconURL) in if let faviconURL = faviconURL { self.homePageToFaviconURLCache[url] = faviconURL + self.homePageToFaviconURLCacheDirty = true let _ = self.favicon(with: faviconURL) } else { - self.homePageURLsWithNoFaviconURL.insert(url) + self.homePageURLsWithNoFaviconURLCache.insert(url) + self.homePageURLsWithNoFaviconURLCacheDirty = true } } @@ -126,6 +150,18 @@ final class FaviconDownloader { postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL) } + + @objc func saveHomePageToFaviconURLCacheIfNeeded() { + if homePageToFaviconURLCacheDirty { + saveHomePageToFaviconURLCache() + } + } + + @objc func saveHomePageURLsWithNoFaviconURLCacheIfNeeded() { + if homePageURLsWithNoFaviconURLCacheDirty { + saveHomePageURLsWithNoFaviconURLCache() + } + } } private extension FaviconDownloader { @@ -175,4 +211,60 @@ private extension FaviconDownloader { NotificationCenter.default.post(name: .FaviconDidBecomeAvailable, object: self, userInfo: userInfo) } } + + func loadHomePageToFaviconURLCache() { + let url = URL(fileURLWithPath: homePageToFaviconURLCachePath) + guard let data = try? Data(contentsOf: url) else { + return + } + let decoder = PropertyListDecoder() + homePageToFaviconURLCache = (try? decoder.decode([String: String].self, from: data)) ?? [String: String]() + } + + func loadHomePageURLsWithNoFaviconURLCache() { + let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath) + guard let data = try? Data(contentsOf: url) else { + return + } + let decoder = PropertyListDecoder() + let decoded = (try? decoder.decode([String].self, from: data)) ?? [String]() + homePageURLsWithNoFaviconURLCache = Set(decoded) + } + + func queueSaveHomePageToFaviconURLCacheIfNeeded() { + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded)) + } + + func queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() { + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded)) + } + + func saveHomePageToFaviconURLCache() { + homePageToFaviconURLCacheDirty = false + + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + let url = URL(fileURLWithPath: homePageToFaviconURLCachePath) + do { + let data = try encoder.encode(homePageToFaviconURLCache) + try data.write(to: url) + } catch { + assertionFailure(error.localizedDescription) + } + } + + func saveHomePageURLsWithNoFaviconURLCache() { + homePageURLsWithNoFaviconURLCacheDirty = false + + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath) + do { + let data = try encoder.encode(Array(homePageURLsWithNoFaviconURLCache)) + try data.write(to: url) + } catch { + assertionFailure(error.localizedDescription) + } + } + } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 17a1ef1bd..6d2e2f292 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -201,12 +201,14 @@ private extension AppDelegate { let imagesFolderURL = tempDir.appendingPathComponent("Images") let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist") let homePagesWithNoIconURL = tempDir.appendingPathComponent("HomePagesWithNoIconURLCache.plist") - + let homePageToFaviconURL = tempDir.appendingPathComponent("HomePageToFaviconURLCache.plist") + let homePageURLsWithNoFaviconURL = tempDir.appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist") + // If the image disk cache hasn't been flushed for 3 days and the network is available, delete it if let flushDate = AppDefaults.lastImageCacheFlushDate, flushDate.addingTimeInterval(3600*24*3) < Date() { if let reachability = try? Reachability(hostname: "apple.com") { if reachability.connection != .unavailable { - for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL, homePagesWithNoIconURL] { + for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL, homePagesWithNoIconURL, homePageToFaviconURL, homePageURLsWithNoFaviconURL] { do { os_log(.info, log: self.log, "Removing cache file: %@", tempItem.absoluteString) try FileManager.default.removeItem(at: tempItem)