blob: 775e25ca9699eaf901e4f390760ac60158bcb11d [file] [log] [blame]
// 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>
#include "base/logging.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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
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 {
NSCache* _cache;
dispatch_queue_t _cacheQueue;
// The tab models.
__weak TabModel* _mainTabModel;
__weak TabModel* _otrTabModel;
// 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 = [[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];
[_mainTabModel removeObserver:self];
[_otrTabModel removeObserver:self];
}
- (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.tabId;
}
- (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.webState)
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];
}
- (void)lowMemoryWarningReceived {
[_cache removeAllObjects];
}
- (void)setMainTabModel:(TabModel*)mainTabModel {
if (mainTabModel == _mainTabModel) {
return;
}
[_mainTabModel removeObserver:self];
_mainTabModel = mainTabModel;
[_mainTabModel addObserver:self];
}
- (void)setOTRTabModel:(TabModel*)otrTabModel {
if (_otrTabModel == otrTabModel) {
return;
}
[_otrTabModel removeObserver:self];
_otrTabModel = otrTabModel;
[_otrTabModel addObserver:self];
}
- (void)setMainTabModel:(TabModel*)mainTabModel
otrTabModel:(TabModel*)otrTabModel {
[self setMainTabModel:mainTabModel];
[self setOTRTabModel:otrTabModel];
}
#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