diff --git a/SDURLCache.h b/SDURLCache.h index adbcebd..d005eb5 100644 --- a/SDURLCache.h +++ b/SDURLCache.h @@ -8,6 +8,13 @@ #import +// wrapper pattern +@interface SDCachedURLResponse : NSObject ++ (id)cachedURLResponseWithNSCachedURLResponse:(NSCachedURLResponse*)url_response; + +@property (nonatomic, retain) NSCachedURLResponse *cached_response; +@end + @interface SDURLCache : NSURLCache { @private @@ -50,4 +57,9 @@ */ - (BOOL)isCached:(NSURL *)url; +/* + * Returns the hash key for the url +*/ ++ (NSString *)cacheKeyForURL:(NSURL *)url; + @end diff --git a/SDURLCache.m b/SDURLCache.m index 5117a3e..db525cc 100644 --- a/SDURLCache.m +++ b/SDURLCache.m @@ -14,43 +14,76 @@ static NSString *const kSDURLCacheInfoDiskUsageKey = @"diskUsage"; static NSString *const kSDURLCacheInfoAccessesKey = @"accesses"; static NSString *const kSDURLCacheInfoSizesKey = @"sizes"; + +// The removal of the NSCachedURLResponse category means that NSKeyedArchiver +// will throw an EXC_BAD_ACCESS when attempting to load NSCachedURLResponse +// data. +// This means that this change requires a cache refresh, and a new cache key +// namespace that will prevent this from happening. +// Old cache keys will eventually be evicted from the system as new keys are +// populated. +static NSString *const kSDURLCacheVersion = @"V2"; + static float const kSDURLCacheLastModFraction = 0.1f; // 10% since Last-Modified suggested by RFC2616 section 13.2.4 static float const kSDURLCacheDefault = 3600; // Default cache expiration delay if none defined (1 hour) static NSDateFormatter* CreateDateFormatter(NSString *format) { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; - - [dateFormatter setLocale:locale]; + [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease]]; [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]]; [dateFormatter setDateFormat:format]; - [locale release]; - return [dateFormatter autorelease]; } -@implementation NSCachedURLResponse(NSCoder) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +@implementation SDCachedURLResponse +@synthesize cached_response; + ++ (id)cachedURLResponseWithNSCachedURLResponse:(NSCachedURLResponse *)url_response { + SDCachedURLResponse *response = [[SDCachedURLResponse alloc] init]; + response.cached_response = url_response; + + return [response autorelease]; +} +#pragma mark NSCopying Methods +- (id)copyWithZone:(NSZone *)zone { + SDCachedURLResponse *newResponse = [[[self class] allocWithZone:zone] init]; + if (newResponse) { + newResponse.cached_response = [[self.cached_response copyWithZone:zone] autorelease]; + } + + return newResponse; +} + +#pragma mark NSCoding Methods - (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"]; + // force write the data of underlying cached response + [coder encodeDataObject:self.cached_response.data]; + [coder encodeObject:self.cached_response.response forKey:@"response"]; + [coder encodeObject:self.cached_response.userInfo forKey:@"userInfo"]; + [coder encodeInt:self.cached_response.storagePolicy forKey:@"storagePolicy"]; } - (id)initWithCoder:(NSCoder *)coder { - return [self initWithResponse:[coder decodeObjectForKey:@"response"] - data:[coder decodeDataObject] - userInfo:[coder decodeObjectForKey:@"userInfo"] - storagePolicy:[coder decodeIntForKey:@"storagePolicy"]]; + self = [super init]; + + if (self) { + self.cached_response = [[[NSCachedURLResponse alloc] initWithResponse:[coder decodeObjectForKey:@"response"] + data:[coder decodeDataObject] + userInfo:[coder decodeObjectForKey:@"userInfo"] + storagePolicy:[coder decodeIntForKey:@"storagePolicy"]] autorelease]; + } + + return self; } -#pragma clang diagnostic pop +- (void)dealloc { + [super dealloc]; + [cached_response release], cached_response = nil; +} @end @@ -87,8 +120,8 @@ + (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]]; + return [NSString stringWithFormat:@"%@_%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + kSDURLCacheVersion, 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]]; } /* @@ -167,12 +200,11 @@ + (NSDate *)expirationDateFromHeaders:(NSDictionary *)headers withStatusCode:(NS } NSInteger maxAge; - foundRange = [cacheControl rangeOfString:@"max-age"]; + foundRange = [cacheControl rangeOfString:@"max-age="]; if (foundRange.length > 0) { NSScanner *cacheControlScanner = [NSScanner scannerWithString:cacheControl]; [cacheControlScanner setScanLocation:foundRange.location + foundRange.length]; - [cacheControlScanner scanString:@"=" intoString:nil]; if ([cacheControlScanner scanInteger:&maxAge]) { if (maxAge > 0) @@ -315,7 +347,7 @@ - (void)removeCachedResponseForCachedKeys:(NSArray *)cacheKeys { NSMutableDictionary *accesses = [self.diskCacheInfo objectForKey:kSDURLCacheInfoAccessesKey]; NSMutableDictionary *sizes = [self.diskCacheInfo objectForKey:kSDURLCacheInfoSizesKey]; - NSFileManager *fileManager = [[NSFileManager alloc] init]; + NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; while ((cacheKey = [enumerator nextObject])) { @@ -327,7 +359,6 @@ - (void)removeCachedResponseForCachedKeys:(NSArray *)cacheKeys diskCacheUsage -= cacheItemSize; [self.diskCacheInfo setObject:[NSNumber numberWithUnsignedInteger:diskCacheUsage] forKey:kSDURLCacheInfoDiskUsageKey]; } - [fileManager release]; } [pool drain]; @@ -368,7 +399,10 @@ - (void)balanceDiskUsage - (void)storeToDisk:(NSDictionary *)context { NSURLRequest *request = [context objectForKey:@"request"]; - NSCachedURLResponse *cachedResponse = [context objectForKey:@"cachedResponse"]; + + // use wrapper to ensure we save appropriate fields.. + SDCachedURLResponse *cachedResponse = [SDCachedURLResponse + cachedURLResponseWithNSCachedURLResponse:[context objectForKey:@"cachedResponse"]]; NSString *cacheKey = [SDURLCache cacheKeyForURL:request.URL]; NSString *cacheFilePath = [diskCachePath stringByAppendingPathComponent:cacheKey]; @@ -402,25 +436,21 @@ - (void)storeToDisk:(NSDictionary *)context - (void)periodicMaintenance { - // If another maintenance operation is already sceduled, cancel it so this new operation will be executed after other + // If another same maintenance operation is already scheduled, cancel it so this new operation will be executed after other // operations of the queue, so we can group more work together [periodicMaintenanceOperation cancel]; self.periodicMaintenanceOperation = nil; - // If disk usage exceeds capacity, run the cache eviction operation and if cacheInfo dictionary is dirty, save it in an operation + // If disk usage outrich capacity, run the cache eviction operation and if cacheInfo dictionnary is dirty, save it in an operation if (diskCacheUsage > self.diskCapacity) { - NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(balanceDiskUsage) object:nil]; - self.periodicMaintenanceOperation = operation; + self.periodicMaintenanceOperation = [[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(balanceDiskUsage) object:nil] autorelease]; [ioQueue addOperation:periodicMaintenanceOperation]; - [operation release]; } else if (diskCacheInfoDirty) { - NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(saveCacheInfo) object:nil]; - self.periodicMaintenanceOperation = operation; + self.periodicMaintenanceOperation = [[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(saveCacheInfo) object:nil] autorelease]; [ioQueue addOperation:periodicMaintenanceOperation]; - [operation release]; } } @@ -442,11 +472,9 @@ - (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger self.diskCachePath = path; // Init the operation queue - NSOperationQueue *queue = [[NSOperationQueue alloc] init]; - self.ioQueue = queue; - [queue release]; - + self.ioQueue = [[[NSOperationQueue alloc] init] autorelease]; ioQueue.maxConcurrentOperationCount = 1; // used to streamline operations in a separate thread + self.ignoreMemoryOnlyStoragePolicy = YES; } @@ -487,14 +515,10 @@ - (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NS } } - NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self - selector:@selector(storeToDisk:) - object:[NSDictionary dictionaryWithObjectsAndKeys: - cachedResponse, @"cachedResponse", - request, @"request", - nil]]; - [ioQueue addOperation:operation]; - [operation release]; + [ioQueue addOperation:[[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(storeToDisk:) + object:[NSDictionary dictionaryWithObjectsAndKeys: + cachedResponse, @"cachedResponse", + request, @"request", nil]] autorelease]]; } } @@ -504,9 +528,7 @@ - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request NSCachedURLResponse *memoryResponse = [super cachedResponseForRequest:request]; if (memoryResponse) - { return memoryResponse; - } NSString *cacheKey = [SDURLCache cacheKeyForURL:request.URL]; @@ -515,9 +537,15 @@ - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request @synchronized(self.diskCacheInfo) { NSMutableDictionary *accesses = [self.diskCacheInfo objectForKey:kSDURLCacheInfoAccessesKey]; - if ([accesses objectForKey:cacheKey]) // OPTI: Check for cache-hit in a in-memory dictionary before hitting the file system + if ([accesses objectForKey:cacheKey]) // OPTI: Check for cache-hit in a in-memory dictionnary before to hit the FS { - NSCachedURLResponse *diskResponse = [NSKeyedUnarchiver unarchiveObjectWithFile:[diskCachePath stringByAppendingPathComponent:cacheKey]]; + + // load wrapper + SDCachedURLResponse *diskResponseWrapper = [NSKeyedUnarchiver unarchiveObjectWithFile: + [diskCachePath stringByAppendingPathComponent:cacheKey]]; + + NSCachedURLResponse *diskResponse = diskResponseWrapper.cached_response; + if (diskResponse) { // OPTI: Log the entry last access time for LRU cache eviction algorithm but don't save the dictionary @@ -527,7 +555,8 @@ - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request // OPTI: Store the response to memory cache for potential future requests [super storeCachedResponse:diskResponse forRequest:request]; - + + // SRK: Work around an interesting retainCount bug in CFNetwork on iOS << 3.2. if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iPhoneOS_3_2) { @@ -541,7 +570,7 @@ - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request } } } - + return nil; } @@ -566,11 +595,8 @@ - (void)removeCachedResponseForRequest:(NSURLRequest *)request - (void)removeAllCachedResponses { [super removeAllCachedResponses]; - - NSFileManager *fileManager = [[NSFileManager alloc] init]; + NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; [fileManager removeItemAtPath:diskCachePath error:NULL]; - [fileManager release]; - @synchronized(self) { [diskCacheInfo release], diskCacheInfo = nil; @@ -586,15 +612,9 @@ - (BOOL)isCached:(NSURL *)url { return YES; } - NSString *cacheKey = [SDURLCache cacheKeyForURL:url]; NSString *cacheFile = [diskCachePath stringByAppendingPathComponent:cacheKey]; - NSFileManager *manager = [[NSFileManager alloc] init]; - - BOOL exists = [manager fileExistsAtPath:cacheFile]; - [manager release]; - - if (exists) + if ([[[[NSFileManager alloc] init] autorelease] fileExistsAtPath:cacheFile]) { return YES; }