Index: ios/chrome/browser/ui/tab_switcher/tab_switcher_cache.mm |
diff --git a/ios/chrome/browser/ui/tab_switcher/tab_switcher_cache.mm b/ios/chrome/browser/ui/tab_switcher/tab_switcher_cache.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3072b9199fb9da10ce2ee77462a399283a150fca |
--- /dev/null |
+++ b/ios/chrome/browser/ui/tab_switcher/tab_switcher_cache.mm |
@@ -0,0 +1,274 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_cache.h" |
+ |
+#include <unordered_map> |
+ |
+#import "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/strings/stringprintf.h" |
+#include "base/synchronization/lock.h" |
+#import "ios/chrome/browser/tabs/tab.h" |
+#import "ios/chrome/browser/tabs/tab_model.h" |
+#import "ios/chrome/browser/ui/uikit_ui_util.h" |
+#include "ios/chrome/common/ios_app_bundle_id_prefix.h" |
+#include "ios/web/public/navigation_item.h" |
+ |
+namespace { |
+// The maximum amount of pixels the cache should hold. |
+NSUInteger kCacheMaxPixelCount = 2048 * 1536 * 4; |
+// Two floats that are different from less than |kMaxFloatDelta| are considered |
+// equals. |
+const CGFloat kMaxFloatDelta = 0.01; |
+} // namespace |
+ |
+@interface TabSwitcherCache () |
+// Clears the cache. Called when a low memory warning was received. |
+- (void)lowMemoryWarningReceived; |
+// Returns a autoreleased resized image of |image|. |
++ (UIImage*)resizedImage:(UIImage*)image toSize:(CGSize)size; |
+ |
+@end |
+ |
+@implementation TabSwitcherCache { |
+ base::scoped_nsobject<NSCache> _cache; |
+ dispatch_queue_t _cacheQueue; |
+ // The tab models. |
+ TabModel* _mainTabModel; // weak |
+ TabModel* _otrTabModel; // weak |
+ |
+ // Lock protecting the pending requests map. |
+ base::Lock _lock; |
+ std::unordered_map<NSUInteger, PendingSnapshotRequest> _pendingRequests; |
+} |
+ |
+@synthesize mainTabModel = _mainTabModel; |
+ |
+- (instancetype)init { |
+ self = [super init]; |
+ if (self) { |
+ _cache.reset([[NSCache alloc] init]); |
+ [_cache setTotalCostLimit:kCacheMaxPixelCount]; |
+ NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; |
+ [nc addObserver:self |
+ selector:@selector(lowMemoryWarningReceived) |
+ name:UIApplicationDidReceiveMemoryWarningNotification |
+ object:nil]; |
+ std::string queueName = |
+ base::StringPrintf("%s.chrome.ios.TabSwitcherCacheQueue", |
+ BUILDFLAG(IOS_APP_BUNDLE_ID_PREFIX)); |
+ _cacheQueue = |
+ dispatch_queue_create(queueName.c_str(), DISPATCH_QUEUE_SERIAL); |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; |
+ [nc removeObserver:self |
+ name:UIApplicationDidReceiveMemoryWarningNotification |
+ object:nil]; |
+ dispatch_release(_cacheQueue); |
+ [_mainTabModel removeObserver:self]; |
+ [_otrTabModel removeObserver:self]; |
+ [super dealloc]; |
+} |
+ |
+- (PendingSnapshotRequest)requestSnapshotForTab:(Tab*)tab |
+ withSize:(CGSize)size |
+ completionBlock: |
+ (SnapshotCompletionBlock)completionBlock { |
+ DCHECK([NSThread isMainThread]); |
+ DCHECK(tab); |
+ DCHECK(completionBlock); |
+ DCHECK(!CGSizeEqualToSize(size, CGSizeZero)); |
+ PendingSnapshotRequest currentRequest; |
+ UIImage* snapshot = [_cache objectForKey:[self keyForTab:tab]]; |
+ if (snapshot) { |
+ CGFloat tabContentAreaRatio = tab.snapshotContentArea.size.width / |
+ tab.snapshotContentArea.size.height; |
+ CGFloat cachedSnapshotRatio = |
+ [snapshot size].width / [snapshot size].height; |
+ // Check that the cached snapshot's ratio matches the content area ratio. |
+ if (std::abs(tabContentAreaRatio - cachedSnapshotRatio) < kMaxFloatDelta && |
+ [snapshot size].width >= size.width) { |
+ // Cache hit. |
+ completionBlock(snapshot); |
+ return currentRequest; |
+ } |
+ } |
+ |
+ // Cache miss. |
+ currentRequest = [self recordPendingRequestForTab:tab]; |
+ NSString* key = [self keyForTab:tab]; |
+ [tab retrieveSnapshot:^(UIImage* snapshot) { |
+ PendingSnapshotRequest requestForSession = [self pendingRequestForTab:tab]; |
+ // Cancel this request if another one has replaced it for this sessionId. |
+ if (currentRequest.requestId != requestForSession.requestId) |
+ return; |
+ dispatch_async(_cacheQueue, ^{ |
+ DCHECK(![NSThread isMainThread]); |
+ UIImage* resizedSnapshot = |
+ [TabSwitcherCache resizedImage:snapshot toSize:size]; |
+ if ([self storeImage:resizedSnapshot forKey:key request:currentRequest]) { |
+ dispatch_async(dispatch_get_main_queue(), ^{ |
+ // Cancel this request if another one has replaced it for this |
+ // sessionId. |
+ PendingSnapshotRequest requestForSession = |
+ [self pendingRequestForTab:tab]; |
+ if (currentRequest.requestId != requestForSession.requestId) |
+ return; |
+ completionBlock(resizedSnapshot); |
+ [self removePendingSnapshotRequest:currentRequest]; |
+ }); |
+ } |
+ }); |
+ }]; |
+ return currentRequest; |
+} |
+ |
+- (void)updateSnapshotForTab:(Tab*)tab |
+ withImage:(UIImage*)image |
+ size:(CGSize)size { |
+ DCHECK([NSThread isMainThread]); |
+ DCHECK(tab); |
+ DCHECK(image); |
+ PendingSnapshotRequest currentRequest = [self recordPendingRequestForTab:tab]; |
+ NSString* key = [self keyForTab:tab]; |
+ |
+ dispatch_async(_cacheQueue, ^{ |
+ DCHECK(![NSThread isMainThread]); |
+ UIImage* resizedSnapshot = |
+ [TabSwitcherCache resizedImage:image toSize:size]; |
+ [self storeImage:resizedSnapshot forKey:key request:currentRequest]; |
+ [self removePendingSnapshotRequest:currentRequest]; |
+ }); |
+} |
+ |
+- (void)cancelPendingSnapshotRequest:(PendingSnapshotRequest)pendingRequest { |
+ [self removePendingSnapshotRequest:pendingRequest]; |
+} |
+ |
+#pragma mark - Private |
+ |
+- (NSString*)keyForTab:(Tab*)tab { |
+ DCHECK([NSThread isMainThread]); |
+ return [tab currentSessionID]; |
+} |
+ |
+- (PendingSnapshotRequest)recordPendingRequestForTab:(Tab*)tab { |
+ PendingSnapshotRequest pendingRequest; |
+ pendingRequest.requestId = [[NSDate date] timeIntervalSince1970]; |
+ pendingRequest.sessionId = [[self keyForTab:tab] hash]; |
+ base::AutoLock guard(_lock); |
+ _pendingRequests[pendingRequest.sessionId] = pendingRequest; |
+ return pendingRequest; |
+} |
+ |
+- (PendingSnapshotRequest)pendingRequestForTab:(Tab*)tab { |
+ DCHECK([NSThread isMainThread]); |
+ PendingSnapshotRequest pendingRequest; |
+ if (![tab webStateImpl]) |
+ return pendingRequest; |
+ NSUInteger sessionId = [[self keyForTab:tab] hash]; |
+ base::AutoLock guard(_lock); |
+ auto it = _pendingRequests.find(sessionId); |
+ if (it != _pendingRequests.end()) |
+ pendingRequest = it->second; |
+ return pendingRequest; |
+} |
+ |
+- (void)removePendingSnapshotRequest:(PendingSnapshotRequest)pendingRequest { |
+ base::AutoLock guard(_lock); |
+ auto itRequest = _pendingRequests.find(pendingRequest.sessionId); |
+ if (itRequest != _pendingRequests.end() && |
+ pendingRequest.requestId == itRequest->second.requestId) { |
+ _pendingRequests.erase(itRequest); |
+ } |
+} |
+ |
+- (void)removePendingSnapshotRequestForTab:(Tab*)tab { |
+ base::AutoLock guard(_lock); |
+ auto itRequest = _pendingRequests.find([[self keyForTab:tab] hash]); |
+ if (itRequest != _pendingRequests.end()) |
+ _pendingRequests.erase(itRequest); |
+} |
+ |
+- (BOOL)storeImage:(UIImage*)image |
+ forKey:(NSString*)key |
+ request:(PendingSnapshotRequest)request { |
+ DCHECK(request.requestId != 0); |
+ if (!image) |
+ return NO; |
+ |
+ { |
+ base::AutoLock guard(_lock); |
+ auto it = _pendingRequests.find(request.sessionId); |
+ if (it == _pendingRequests.end()) |
+ return NO; |
+ |
+ // Only write the image in cache if the request is still valid. |
+ if (request.requestId != it->second.requestId) |
+ return NO; |
+ } |
+ |
+ const CGFloat screenScale = [[UIScreen mainScreen] scale]; |
+ const NSUInteger cost = |
+ image.size.width * screenScale * image.size.height * screenScale; |
+ [_cache setObject:image forKey:key cost:cost]; |
+ return YES; |
+} |
+ |
++ (UIImage*)resizedImage:(UIImage*)image toSize:(CGSize)size { |
+ DCHECK(image.scale == 1); |
+ CGFloat screenScale = [[UIScreen mainScreen] scale]; |
+ CGSize pixelSize = size; |
+ pixelSize.width *= screenScale; |
+ pixelSize.height *= screenScale; |
+ UIImage* resizedSnapshot = |
+ ResizeImage(image, pixelSize, ProjectionMode::kAspectFillNoClipping, YES); |
+ // Creates a new image with the correct |scale| attribute. |
+ return [[[UIImage alloc] initWithCGImage:resizedSnapshot.CGImage |
+ scale:screenScale |
+ orientation:UIImageOrientationUp] autorelease]; |
+} |
+ |
+- (void)lowMemoryWarningReceived { |
+ [_cache removeAllObjects]; |
+} |
+ |
+- (void)setMainTabModel:(TabModel*)mainTabModel |
+ otrTabModel:(TabModel*)otrTabModel { |
+ if (mainTabModel != _mainTabModel) |
+ [self replaceOldTabModel:&_mainTabModel withTabModel:mainTabModel]; |
+ if (otrTabModel != _otrTabModel) |
+ [self replaceOldTabModel:&_otrTabModel withTabModel:otrTabModel]; |
+} |
+ |
+- (void)replaceOldTabModel:(TabModel**)oldTabModel |
+ withTabModel:(TabModel*)newTabModel { |
+ [*oldTabModel removeObserver:self]; |
+ *oldTabModel = newTabModel; |
+ [newTabModel addObserver:self]; |
+} |
+ |
+#pragma mark - TabModelObserver |
+ |
+- (void)tabModel:(TabModel*)model |
+ didRemoveTab:(Tab*)tab |
+ atIndex:(NSUInteger)index { |
+ [self removePendingSnapshotRequestForTab:tab]; |
+ [_cache removeObjectForKey:[self keyForTab:tab]]; |
+} |
+ |
+- (void)tabModel:(TabModel*)model |
+ didChangeTabSnapshot:(Tab*)tab |
+ withImage:image { |
+ [self removePendingSnapshotRequestForTab:tab]; |
+ [_cache removeObjectForKey:[self keyForTab:tab]]; |
+} |
+ |
+@end |