From 9aadd7cf9ff7800f61cefa46a7ed68de39c86f83 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Mon, 19 Nov 2012 11:41:06 -0500 Subject: [PATCH 1/5] Significant performance enhancement for -clearCachedResponsesForStoragePolicy: Instead of walking each file and deleting it on the current thread, we move the cache directory to a uniquely-named directory in the temp directory and delete its contents on a background thread. For whatever reason the iOS filesystem can perform a rename very quickly but deletes - especially for larger files, e.g. images - can be very slow (orders of magnitude slower) on old devices like the iPad 1. This approach should be safe even if the background deletion thread is unable to complete normally (e.g. the application is unexpected terminated during execution) because we can rely on the system to clean up for us eventually since we're operating on files in the temp directory. ASIHTTPRequest is a great library and I have used it to build some high-traffic apps. Unfortunately I was really bitten by the cache-clearing performance on older devices. Users were unable to start up the app due to being killed by the iOS watchdog since the deletes took so long to execute. In our app, this call might have taken 20 seconds in the pathological case (!) on an iPad 1. Now it takes about 100ms. Another way to address this would be to allow client code to specify ASIDownloadCache size/item limits, with some kind of LRU eviction. But this was a quicker, less invasive win for me. --- Classes/ASIDownloadCache.m | 91 ++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/Classes/ASIDownloadCache.m b/Classes/ASIDownloadCache.m index 93da36fb..1dad9403 100644 --- a/Classes/ASIDownloadCache.m +++ b/Classes/ASIDownloadCache.m @@ -19,6 +19,7 @@ @interface ASIDownloadCache () + (NSString *)keyForURL:(NSURL *)url; - (NSString *)pathToFile:(NSString *)file; +- (NSString *)generateUniqueIdentifier; @end @implementation ASIDownloadCache @@ -393,35 +394,69 @@ - (void)setDefaultCachePolicy:(ASICachePolicy)cachePolicy - (void)clearCachedResponsesForStoragePolicy:(ASICacheStoragePolicy)storagePolicy { - [[self accessLock] lock]; - if (![self storagePath]) { - [[self accessLock] unlock]; - return; - } - NSString *path = [[self storagePath] stringByAppendingPathComponent:(storagePolicy == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)]; - - NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; + [[self accessLock] lock]; + if (![self storagePath]) { + [[self accessLock] unlock]; + return; + } + NSString *path = [[self storagePath] stringByAppendingPathComponent:(storagePolicy == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)]; + + NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; + + BOOL isDirectory = NO; + BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; + if (!exists || !isDirectory) { + [[self accessLock] unlock]; + return; + } + + // It is significantly faster to perform a move than a delete on the iOS filesystem, especially on older devices. + // We move the existing cache directory so it lives in the temp directory and has a unique name. + // We clear the contents of the moved directory in a background thread. + NSString *renamedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[self generateUniqueIdentifier]]; + + NSError *error = nil; + BOOL renamed = [fileManager moveItemAtPath:path toPath:renamedPath error:&error]; + if (!renamed) { + [[self accessLock] unlock]; + [NSException raise:@"FailedToRenameCacheDirectory" format:@"Renaming cache directory failed at path '%@'",path]; + } + + BOOL recreated = [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; + if (!recreated) { + [[self accessLock] unlock]; + [NSException raise:@"FailedToRecreateCacheDirectory" format:@"Recreating cache directory failed at path '%@'",path]; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { + NSError *backgroundError = nil; + NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:renamedPath error:&backgroundError]; + if (backgroundError) { + [NSException raise:@"FailedToTraverseCacheDirectory" format:@"Listing cache directory failed at path '%@'",renamedPath]; + } + for (NSString *file in cacheFiles) { + [fileManager removeItemAtPath:[renamedPath stringByAppendingPathComponent:file] error:&backgroundError]; + if (backgroundError) { + [NSException raise:@"FailedToRemoveCacheFile" format:@"Failed to remove cached data at path '%@'",renamedPath]; + } + } + + // Remove the now-empty temporary directory + [fileManager removeItemAtPath:renamedPath error:&backgroundError]; + if (backgroundError) { + [NSException raise:@"FailedToRemoveCacheDirectory" format:@"Failed to remove cached directory at path '%@'",renamedPath]; + } + }); + + [[self accessLock] unlock]; +} - BOOL isDirectory = NO; - BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; - if (!exists || !isDirectory) { - [[self accessLock] unlock]; - return; - } - NSError *error = nil; - NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:path error:&error]; - if (error) { - [[self accessLock] unlock]; - [NSException raise:@"FailedToTraverseCacheDirectory" format:@"Listing cache directory failed at path '%@'",path]; - } - for (NSString *file in cacheFiles) { - [fileManager removeItemAtPath:[path stringByAppendingPathComponent:file] error:&error]; - if (error) { - [[self accessLock] unlock]; - [NSException raise:@"FailedToRemoveCacheFile" format:@"Failed to remove cached data at path '%@'",path]; - } - } - [[self accessLock] unlock]; +- (NSString *)generateUniqueIdentifier +{ + CFUUIDRef uniqueIdentifier = CFUUIDCreate(NULL); + CFStringRef uniqueIdentifierString = CFUUIDCreateString(NULL, uniqueIdentifier); + CFRelease(uniqueIdentifier); + return [(NSString *)uniqueIdentifierString autorelease]; } + (BOOL)serverAllowsResponseCachingForRequest:(ASIHTTPRequest *)request From 93ca247e9e653762bca6dba6b17117db5cf2b955 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Mon, 30 Sep 2013 16:38:22 -0400 Subject: [PATCH 2/5] NSStreamEventErrorOccurred -> NSStreamStatusError fix --- Classes/ASIDataCompressor.m | 4 ++-- Classes/ASIDataDecompressor.m | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Classes/ASIDataCompressor.m b/Classes/ASIDataCompressor.m index f4b930a6..c9a5cd55 100644 --- a/Classes/ASIDataCompressor.m +++ b/Classes/ASIDataCompressor.m @@ -161,7 +161,7 @@ + (BOOL)compressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinati readLength = [inputStream read:inputData maxLength:DATA_CHUNK_SIZE]; // Make sure nothing went wrong - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) { + if ([inputStream streamStatus] == NSStreamStatusError) { if (err) { *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Compression of %@ failed because we were unable to read from the source data file",sourcePath],NSLocalizedDescriptionKey,[inputStream streamError],NSUnderlyingErrorKey,nil]]; } @@ -187,7 +187,7 @@ + (BOOL)compressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinati [outputStream write:(const uint8_t *)[outputData bytes] maxLength:[outputData length]]; // Make sure nothing went wrong - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) { + if ([inputStream streamStatus] == NSStreamStatusError) { if (err) { *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Compression of %@ failed because we were unable to write to the destination data file at %@",sourcePath,destinationPath],NSLocalizedDescriptionKey,[outputStream streamError],NSUnderlyingErrorKey,nil]]; } diff --git a/Classes/ASIDataDecompressor.m b/Classes/ASIDataDecompressor.m index 3bb60a5b..e84a1e31 100644 --- a/Classes/ASIDataDecompressor.m +++ b/Classes/ASIDataDecompressor.m @@ -158,7 +158,7 @@ + (BOOL)uncompressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destina readLength = [inputStream read:inputData maxLength:DATA_CHUNK_SIZE]; // Make sure nothing went wrong - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) { + if ([inputStream streamStatus] == NSStreamStatusError) { if (err) { *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Decompression of %@ failed because we were unable to read from the source data file",sourcePath],NSLocalizedDescriptionKey,[inputStream streamError],NSUnderlyingErrorKey,nil]]; } @@ -184,7 +184,7 @@ + (BOOL)uncompressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destina [outputStream write:(Bytef*)[outputData bytes] maxLength:[outputData length]]; // Make sure nothing went wrong - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) { + if ([inputStream streamStatus] == NSStreamStatusError) { if (err) { *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Decompression of %@ failed because we were unable to write to the destination data file at %@",sourcePath,destinationPath],NSLocalizedDescriptionKey,[outputStream streamError],NSUnderlyingErrorKey,nil]]; } From 3a1eac4248eb6009afc560c98e017bf1cca7e8cc Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Mon, 30 Sep 2013 16:43:09 -0400 Subject: [PATCH 3/5] Deprecated NSDate -addTimeInterval: replaced with -dateByAddingTimeInterval: --- Classes/ASIHTTPRequest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/ASIHTTPRequest.m b/Classes/ASIHTTPRequest.m index 8dd162c3..c78d6f01 100644 --- a/Classes/ASIHTTPRequest.m +++ b/Classes/ASIHTTPRequest.m @@ -4866,7 +4866,7 @@ + (NSDate *)expiryDateForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterva // RFC 2612 says max-age must override any Expires header if (maxAge) { - return [[NSDate date] addTimeInterval:maxAge]; + return [[NSDate date] dateByAddingTimeInterval:maxAge]; } else { NSString *expires = [responseHeaders objectForKey:@"Expires"]; if (expires) { From 7cf10bf288d8787654f4f628f6e36a07457c873c Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Tue, 1 Oct 2013 14:56:28 -0400 Subject: [PATCH 4/5] Fix for deprecated methods --- Classes/ASIAuthenticationDialog.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Classes/ASIAuthenticationDialog.m b/Classes/ASIAuthenticationDialog.m index 255a6849..1629f7d0 100644 --- a/Classes/ASIAuthenticationDialog.m +++ b/Classes/ASIAuthenticationDialog.m @@ -217,9 +217,9 @@ - (UITextField *)domainField + (void)dismiss { if ([sharedDialog respondsToSelector:@selector(presentingViewController)]) - [[sharedDialog presentingViewController] dismissModalViewControllerAnimated:YES]; + [[sharedDialog presentingViewController] dismissViewControllerAnimated:YES completion:^{}]; else - [[sharedDialog parentViewController] dismissModalViewControllerAnimated:YES]; + [[sharedDialog parentViewController] dismissViewControllerAnimated:YES completion:^{}]; } - (void)viewDidDisappear:(BOOL)animated @@ -237,9 +237,9 @@ - (void)dismiss [[self class] dismiss]; } else { if ([self respondsToSelector:@selector(presentingViewController)]) - [[self presentingViewController] dismissModalViewControllerAnimated:YES]; + [[self presentingViewController] dismissViewControllerAnimated:YES completion:^{}]; else - [[self parentViewController] dismissModalViewControllerAnimated:YES]; + [[self parentViewController] dismissViewControllerAnimated:YES completion:^{}]; } } @@ -315,7 +315,7 @@ - (void)show } #endif - [[self presentingController] presentModalViewController:self animated:YES]; + [[self presentingController] presentViewController:self animated:YES completion:^{}]; } #pragma mark button callbacks From 14d10ce105393f5f0038e1f878ada9c35f7b6fb4 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Tue, 7 Oct 2014 17:31:30 -0400 Subject: [PATCH 5/5] Comment out deprecated "ignore invalid certs" code since we don't use it anyway --- Classes/ASIHTTPRequest.m | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Classes/ASIHTTPRequest.m b/Classes/ASIHTTPRequest.m index c78d6f01..eed0d173 100644 --- a/Classes/ASIHTTPRequest.m +++ b/Classes/ASIHTTPRequest.m @@ -1211,17 +1211,19 @@ - (void)startRequest if (![self validatesSecureCertificate]) { // see: http://iphonedevelopment.blogspot.com/2010/05/nsstream-tcp-and-ssl.html - NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys: - [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates, - [NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot, - [NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain, - kCFNull,kCFStreamSSLPeerName, - nil]; + // This is deprecated and we don't need it for now + + // NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys: + // [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates, + // [NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot, + // [NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain, + // kCFNull,kCFStreamSSLPeerName, + // nil]; - CFReadStreamSetProperty((CFReadStreamRef)[self readStream], - kCFStreamPropertySSLSettings, - (CFTypeRef)sslProperties); - [sslProperties release]; + // CFReadStreamSetProperty((CFReadStreamRef)[self readStream], + // kCFStreamPropertySSLSettings, + // (CFTypeRef)sslProperties); + // [sslProperties release]; } // Tell CFNetwork to use a client certificate