Files
iOS-boilerplate/IOSBoilerplate/AFURLCache.m
Alberto Gimeno Brieba e40d3ce146 Replaced ASIHTTPRequest by AFNetworking mainly because ASIHTTPRequest has been discontinued by his lead developer.
But also because AFNetworking is faster, has a minimal codebase and has less dependencies.

This is a major change that has required a modification of all the examples, but with good benefits:

- ImageManager no longer depends on the ApplicationDelegate.
- ImageManager code does not depend on the navigation structure of view controllers.
- Examples need less code.

The Place class has been also removed and the code now uses the builtin MKPointAnnotation class.
2011-10-28 17:58:32 +02:00

560 lines
23 KiB
Objective-C

// AFURLCache.m
//
// Copyright (c) 2010-2011 Olivier Poitrey <rs@dailymotion.com>
// Modernized to use GCD by Peter Steinberger <steipete@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is furnished
// to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#import "AFURLCache.h"
#import <CommonCrypto/CommonDigest.h>
#define kAFURLCachePath @"AFNetworkingURLCache"
#define kAFURLCacheMaintenanceTime 5ull
static NSTimeInterval const kAFURLCacheInfoDefaultMinCacheInterval = 5 * 60; // 5 minute
static NSString *const kAFURLCacheInfoFileName = @"cacheInfo.plist";
static NSString *const kAFURLCacheInfoDiskUsageKey = @"diskUsage";
static NSString *const kAFURLCacheInfoAccessesKey = @"accesses";
static NSString *const kAFURLCacheInfoSizesKey = @"sizes";
static float const kAFURLCacheLastModFraction = 0.1f; // 10% since Last-Modified suggested by RFC2616 section 13.2.4
static float const kAFURLCacheDefault = 3600; // Default cache expiration delay if none defined (1 hour)
static NSDateFormatter* CreateDateFormatter(NSString *format) {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease]];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]];
[dateFormatter setDateFormat:format];
return dateFormatter;
}
@implementation NSCachedURLResponse(NSCoder)
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeDataObject:self.data];
[coder encodeObject:self.response forKey:@"response"];
[coder encodeObject:self.userInfo forKey:@"userInfo"];
[coder encodeInt:self.storagePolicy forKey:@"storagePolicy"];
}
- (id)initWithCoder:(NSCoder *)coder {
return [self initWithResponse:[coder decodeObjectForKey:@"response"]
data:[coder decodeDataObject]
userInfo:[coder decodeObjectForKey:@"userInfo"]
storagePolicy:[coder decodeIntForKey:@"storagePolicy"]];
}
@end
// deadlock-free variant of dispatch_sync
void dispatch_sync_afreentrant(dispatch_queue_t queue, dispatch_block_t block);
inline void dispatch_sync_afreentrant(dispatch_queue_t queue, dispatch_block_t block) {
dispatch_get_current_queue() == queue ? block() : dispatch_sync(queue, block);
}
void dispatch_async_afreentrant(dispatch_queue_t queue, dispatch_block_t block);
inline void dispatch_async_afreentrant(dispatch_queue_t queue, dispatch_block_t block) {
dispatch_get_current_queue() == queue ? block() : dispatch_async(queue, block);
}
@interface AFURLCache ()
@property (nonatomic, retain) NSString *diskCachePath;
@property (nonatomic, retain) NSMutableDictionary *diskCacheInfo;
- (void)periodicMaintenance;
@end
@implementation AFURLCache
#pragma mark AFURLCache (tools)
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSString *string = request.URL.absoluteString;
NSRange hash = [string rangeOfString:@"#"];
if (hash.location == NSNotFound)
return request;
NSMutableURLRequest *copy = [[request mutableCopy] autorelease];
copy.URL = [NSURL URLWithString:[string substringToIndex:hash.location]];
return copy;
}
+ (NSString *)cacheKeyForURL:(NSURL *)url {
const char *str = [url.absoluteString UTF8String];
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, strlen(str), r);
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];
}
#pragma mark AFURLCache (private)
static dispatch_queue_t get_date_formatter_queue() {
static dispatch_queue_t _dateFormatterQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_dateFormatterQueue = dispatch_queue_create("com.alamofire.disk-cache.dateformatter", NULL);
});
return _dateFormatterQueue;
}
static dispatch_queue_t get_disk_cache_queue() {
static dispatch_once_t onceToken;
static dispatch_queue_t _diskCacheQueue;
dispatch_once(&onceToken, ^{
_diskCacheQueue = dispatch_queue_create("com.alamofire.disk-cache.processing", NULL);
});
return _diskCacheQueue;
}
static dispatch_queue_t get_disk_io_queue() {
static dispatch_queue_t _diskIOQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_diskIOQueue = dispatch_queue_create("com.alamofire.disk-cache.io", NULL);
});
return _diskIOQueue;
}
- (dispatch_source_t)maintenanceTimer {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_maintenanceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (_maintenanceTimer) {
dispatch_source_set_timer(_maintenanceTimer, dispatch_walltime(DISPATCH_TIME_NOW, kAFURLCacheMaintenanceTime * NSEC_PER_SEC),
kAFURLCacheMaintenanceTime * NSEC_PER_SEC, kAFURLCacheMaintenanceTime/2 * NSEC_PER_SEC);
__block AFURLCache *blockSelf = self;
dispatch_source_set_event_handler(_maintenanceTimer, ^{
[blockSelf periodicMaintenance];
// will abuse cache queue to lock timer
dispatch_async_afreentrant(get_disk_cache_queue(), ^{
dispatch_suspend(_maintenanceTimer); // pause timer
_timerPaused = YES;
});
});
// initially wake up timer
dispatch_resume(_maintenanceTimer);
}
});
return _maintenanceTimer;
}
/*
* Parse HTTP Date: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
*/
+ (NSDate *)dateFromHttpDateString:(NSString *)httpDate {
static dispatch_once_t onceToken;
static NSDateFormatter *_FC1123DateFormatter;
static NSDateFormatter *_ANSICDateFormatter;
static NSDateFormatter *_RFC850DateFormatter;
dispatch_once(&onceToken, ^{
_FC1123DateFormatter = CreateDateFormatter(@"EEE, dd MMM yyyy HH:mm:ss z");
_ANSICDateFormatter = CreateDateFormatter(@"EEE MMM d HH:mm:ss yyyy");
_RFC850DateFormatter = CreateDateFormatter(@"EEEE, dd-MMM-yy HH:mm:ss z");
});
__block NSDate *date = nil;
dispatch_sync(get_date_formatter_queue(), ^{
date = [_FC1123DateFormatter dateFromString:httpDate];
if (!date) {
// ANSI C date format - Sun Nov 6 08:49:37 1994
date = [_ANSICDateFormatter dateFromString:httpDate];
if (!date) {
// RFC 850 date format - Sunday, 06-Nov-94 08:49:37 GMT
date = [_RFC850DateFormatter dateFromString:httpDate];
}
}
});
return date;
}
/*
* This method tries to determine the expiration date based on a response headers dictionary.
*/
+ (NSDate *)expirationDateFromHeaders:(NSDictionary *)headers withStatusCode:(NSInteger)status {
if (status != 200 && status != 203 && status != 300 && status != 301 && status != 302 && status != 307 && status != 410) {
// Uncacheable response status code
return nil;
}
// Check Pragma: no-cache
NSString *pragma = [headers objectForKey:@"Pragma"];
if (pragma && [pragma isEqualToString:@"no-cache"]) {
// Uncacheable response
return nil;
}
// Define "now" based on the request
NSString *date = [headers objectForKey:@"Date"];
// If no Date: header, define now from local clock
NSDate *now = date ? [AFURLCache dateFromHttpDateString:date] : [NSDate date];
// Look at info from the Cache-Control: max-age=n header
NSString *cacheControl = [headers objectForKey:@"Cache-Control"];
if (cacheControl) {
NSRange foundRange = [cacheControl rangeOfString:@"no-store"];
if (foundRange.length > 0) {
// Can't be cached
return nil;
}
NSInteger maxAge;
foundRange = [cacheControl rangeOfString:@"max-age="];
if (foundRange.length > 0) {
NSScanner *cacheControlScanner = [NSScanner scannerWithString:cacheControl];
[cacheControlScanner setScanLocation:foundRange.location + foundRange.length];
if ([cacheControlScanner scanInteger:&maxAge]) {
return maxAge > 0 ? [[[NSDate alloc] initWithTimeInterval:maxAge sinceDate:now] autorelease] : nil;
}
}
}
// If not Cache-Control found, look at the Expires header
NSString *expires = [headers objectForKey:@"Expires"];
if (expires) {
NSTimeInterval expirationInterval = 0;
NSDate *expirationDate = [AFURLCache dateFromHttpDateString:expires];
if (expirationDate) {
expirationInterval = [expirationDate timeIntervalSinceDate:now];
}
if (expirationInterval > 0) {
// Convert remote expiration date to local expiration date
return [NSDate dateWithTimeIntervalSinceNow:expirationInterval];
}
else {
// If the Expires header can't be parsed or is expired, do not cache
return nil;
}
}
if (status == 302 || status == 307) {
// If not explict cache control defined, do not cache those status
return nil;
}
// If no cache control defined, try some heristic to determine an expiration date
NSString *lastModified = [headers objectForKey:@"Last-Modified"];
if (lastModified) {
NSTimeInterval age = 0;
NSDate *lastModifiedDate = [AFURLCache dateFromHttpDateString:lastModified];
if (lastModifiedDate) {
// Define the age of the document by comparing the Date header with the Last-Modified header
age = [now timeIntervalSinceDate:lastModifiedDate];
}
return age > 0 ? [NSDate dateWithTimeIntervalSinceNow:(age * kAFURLCacheLastModFraction)] : nil;
}
// If nothing permitted to define the cache expiration delay nor to restrict its cacheability, use a default cache expiration delay
return [[[NSDate alloc] initWithTimeInterval:kAFURLCacheDefault sinceDate:now] autorelease];
}
- (NSMutableDictionary *)diskCacheInfo {
if (!_diskCacheInfo) {
dispatch_sync_afreentrant(get_disk_cache_queue(), ^{
if (!_diskCacheInfo) { // Check again, maybe another thread created it while waiting for the mutex
_diskCacheInfo = [[NSMutableDictionary alloc] initWithContentsOfFile:[_diskCachePath stringByAppendingPathComponent:kAFURLCacheInfoFileName]];
if (!_diskCacheInfo) {
_diskCacheInfo = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:0], kAFURLCacheInfoDiskUsageKey,
[NSMutableDictionary dictionary], kAFURLCacheInfoAccessesKey,
[NSMutableDictionary dictionary], kAFURLCacheInfoSizesKey,
nil];
}
_diskCacheInfoDirty = NO;
_diskCacheUsage = [[_diskCacheInfo objectForKey:kAFURLCacheInfoDiskUsageKey] unsignedIntValue];
// create maintenance timer
[self maintenanceTimer];
}
});
}
return _diskCacheInfo;
}
- (void)createDiskCachePath {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSFileManager *fileManager = [[NSFileManager alloc] init];
if (![fileManager fileExistsAtPath:_diskCachePath]) {
[fileManager createDirectoryAtPath:_diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
}
[fileManager release];
});
}
- (void)saveCacheInfo {
[self createDiskCachePath];
dispatch_async_afreentrant(get_disk_cache_queue(), ^{
NSData *data = [NSPropertyListSerialization dataFromPropertyList:self.diskCacheInfo format:NSPropertyListBinaryFormat_v1_0 errorDescription:NULL];
if (data) {
[data writeToFile:[_diskCachePath stringByAppendingPathComponent:kAFURLCacheInfoFileName] atomically:YES];
}
_diskCacheInfoDirty = NO;
});
}
- (void)removeCachedResponseForCachedKeys:(NSArray *)cacheKeys {
dispatch_async_afreentrant(get_disk_cache_queue(), ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSEnumerator *enumerator = [cacheKeys objectEnumerator];
NSString *cacheKey;
NSMutableDictionary *accesses = [self.diskCacheInfo objectForKey:kAFURLCacheInfoAccessesKey];
NSMutableDictionary *sizes = [self.diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey];
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
while ((cacheKey = [enumerator nextObject])) {
NSUInteger cacheItemSize = [[sizes objectForKey:cacheKey] unsignedIntegerValue];
[accesses removeObjectForKey:cacheKey];
[sizes removeObjectForKey:cacheKey];
[fileManager removeItemAtPath:[_diskCachePath stringByAppendingPathComponent:cacheKey] error:NULL];
_diskCacheUsage -= cacheItemSize;
[self.diskCacheInfo setObject:[NSNumber numberWithUnsignedInteger:_diskCacheUsage] forKey:kAFURLCacheInfoDiskUsageKey];
}
[pool drain];
});
}
- (void)balanceDiskUsage {
if (_diskCacheUsage < self.diskCapacity) {
return; // Already done
}
dispatch_async_afreentrant(get_disk_cache_queue(), ^{
NSMutableArray *keysToRemove = [NSMutableArray array];
// Apply LRU cache eviction algorithm while disk usage outreach capacity
NSDictionary *sizes = [self.diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey];
NSInteger capacityToSave = _diskCacheUsage - self.diskCapacity;
NSArray *sortedKeys = [[self.diskCacheInfo objectForKey:kAFURLCacheInfoAccessesKey] keysSortedByValueUsingSelector:@selector(compare:)];
NSEnumerator *enumerator = [sortedKeys objectEnumerator];
NSString *cacheKey;
while (capacityToSave > 0 && (cacheKey = [enumerator nextObject])) {
[keysToRemove addObject:cacheKey];
capacityToSave -= [(NSNumber *)[sizes objectForKey:cacheKey] unsignedIntegerValue];
}
[self removeCachedResponseForCachedKeys:keysToRemove];
[self saveCacheInfo];
});
}
- (void)storeRequestToDisk:(NSURLRequest *)request response:(NSCachedURLResponse *)cachedResponse {
NSString *cacheKey = [AFURLCache cacheKeyForURL:request.URL];
NSString *cacheFilePath = [_diskCachePath stringByAppendingPathComponent:cacheKey];
[self createDiskCachePath];
// Archive the cached response on disk
if (![NSKeyedArchiver archiveRootObject:cachedResponse toFile:cacheFilePath]) {
// Caching failed for some reason
return;
}
// Update disk usage info
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSNumber *cacheItemSize = [[fileManager attributesOfItemAtPath:cacheFilePath error:NULL] objectForKey:NSFileSize];
[fileManager release];
dispatch_async_afreentrant(get_disk_cache_queue(), ^{
_diskCacheUsage += [cacheItemSize unsignedIntegerValue];
[self.diskCacheInfo setObject:[NSNumber numberWithUnsignedInteger:_diskCacheUsage] forKey:kAFURLCacheInfoDiskUsageKey];
// Update cache info for the stored item
[(NSMutableDictionary *)[self.diskCacheInfo objectForKey:kAFURLCacheInfoAccessesKey] setObject:[NSDate date] forKey:cacheKey];
[(NSMutableDictionary *)[self.diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey] setObject:cacheItemSize forKey:cacheKey];
[self saveCacheInfo];
// start timer for cleanup (rely on fact that dispatch_suspend syncs with disk cache queue)
if (_timerPaused) {
_timerPaused = NO;
dispatch_resume([self maintenanceTimer]);
}
});
}
// called in NSTimer
- (void)periodicMaintenance {
if (_diskCacheUsage > self.diskCapacity) {
dispatch_async(get_disk_io_queue(), ^{
[self balanceDiskUsage];
});
}
else if (_diskCacheInfoDirty) {
dispatch_async(get_disk_io_queue(), ^{
[self saveCacheInfo];
});
}
}
#pragma mark AFURLCache
+ (NSString *)defaultCachePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return [[paths objectAtIndex:0] stringByAppendingPathComponent:kAFURLCachePath];
}
#pragma mark NSURLCache
- (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)path {
if ((self = [super initWithMemoryCapacity:memoryCapacity diskCapacity:diskCapacity diskPath:path])) {
self.minCacheInterval = kAFURLCacheInfoDefaultMinCacheInterval;
self.diskCachePath = path;
self.ignoreMemoryOnlyStoragePolicy = NO;
}
return self;
}
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request {
request = [AFURLCache canonicalRequestForRequest:request];
if (request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData
|| request.cachePolicy == NSURLRequestReloadIgnoringLocalAndRemoteCacheData
|| request.cachePolicy == NSURLRequestReloadIgnoringCacheData) {
// When cache is ignored for read, it's a good idea not to store the result as well as this option
// have big chance to be used every times in the future for the same request.
// NOTE: This is a change regarding default URLCache behavior
return;
}
[super storeCachedResponse:cachedResponse forRequest:request];
NSURLCacheStoragePolicy storagePolicy = cachedResponse.storagePolicy;
if ((storagePolicy == NSURLCacheStorageAllowed || (storagePolicy == NSURLCacheStorageAllowedInMemoryOnly && _ignoreMemoryOnlyStoragePolicy))
&& [cachedResponse.response isKindOfClass:[NSHTTPURLResponse self]]
&& cachedResponse.data.length < self.diskCapacity) {
NSDictionary *headers = [(NSHTTPURLResponse *)cachedResponse.response allHeaderFields];
// RFC 2616 section 13.3.4 says clients MUST use Etag in any cache-conditional request if provided by server
if (![headers objectForKey:@"Etag"]) {
NSDate *expirationDate = [AFURLCache expirationDateFromHeaders:headers
withStatusCode:((NSHTTPURLResponse *)cachedResponse.response).statusCode];
if (!expirationDate || [expirationDate timeIntervalSinceNow] - _minCacheInterval <= 0) {
// This response is not cacheable, headers said
return;
}
}
dispatch_async(get_disk_io_queue(), ^{
[self storeRequestToDisk:request response:cachedResponse];
});
}
}
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
request = [AFURLCache canonicalRequestForRequest:request];
NSCachedURLResponse *memoryResponse = [super cachedResponseForRequest:request];
if (memoryResponse) {
return memoryResponse;
}
NSString *cacheKey = [AFURLCache cacheKeyForURL:request.URL];
// NOTE: We don't handle expiration here as even staled cache data is necessary for NSURLConnection to handle cache revalidation.
// Staled cache data is also needed for cachePolicies which force the use of the cache.
__block NSCachedURLResponse *response = nil;
dispatch_sync(get_disk_cache_queue(), ^{
NSMutableDictionary *accesses = [self.diskCacheInfo objectForKey:kAFURLCacheInfoAccessesKey];
if ([accesses objectForKey:cacheKey]) { // OPTI: Check for cache-hit in a in-memory dictionnary before to hit the FS
response = [NSKeyedUnarchiver unarchiveObjectWithFile:[_diskCachePath stringByAppendingPathComponent:cacheKey]];
if (response) {
// OPTI: Log the entry last access time for LRU cache eviction algorithm but don't save the dictionary
// on disk now in order to save IO and time
[accesses setObject:[NSDate date] forKey:cacheKey];
_diskCacheInfoDirty = YES;
}
}
});
// OPTI: Store the response to memory cache for potential future requests
if (response) {
[super storeCachedResponse:response forRequest:request];
}
return response;
}
- (NSUInteger)currentDiskUsage {
if (!_diskCacheInfo) {
[self diskCacheInfo];
}
return _diskCacheUsage;
}
- (void)removeCachedResponseForRequest:(NSURLRequest *)request {
request = [AFURLCache canonicalRequestForRequest:request];
[super removeCachedResponseForRequest:request];
[self removeCachedResponseForCachedKeys:[NSArray arrayWithObject:[AFURLCache cacheKeyForURL:request.URL]]];
[self saveCacheInfo];
}
- (void)removeAllCachedResponses {
[super removeAllCachedResponses];
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
[fileManager removeItemAtPath:_diskCachePath error:NULL];
dispatch_async_afreentrant(get_disk_cache_queue(), ^{
self.diskCacheInfo = nil;
});
}
- (BOOL)isCached:(NSURL *)url {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
request = [AFURLCache canonicalRequestForRequest:request];
if ([super cachedResponseForRequest:request]) {
return YES;
}
NSString *cacheKey = [AFURLCache cacheKeyForURL:url];
NSString *cacheFile = [_diskCachePath stringByAppendingPathComponent:cacheKey];
BOOL isCached = [[[[NSFileManager alloc] init] autorelease] fileExistsAtPath:cacheFile];
return isCached;
}
#pragma mark NSObject
- (void)dealloc {
dispatch_source_cancel(_maintenanceTimer);
dispatch_release(_maintenanceTimer);
[_diskCachePath release], _diskCachePath = nil;
[_diskCacheInfo release], _diskCacheInfo = nil;
[super dealloc];
}
@synthesize minCacheInterval = _minCacheInterval;
@synthesize ignoreMemoryOnlyStoragePolicy = _ignoreMemoryOnlyStoragePolicy;
@synthesize diskCachePath = _diskCachePath;
@synthesize diskCacheInfo = _diskCacheInfo;
@end