-
Notifications
You must be signed in to change notification settings - Fork 232
Commit
Observationally, we've seen the NSURL loading system fail to insert items into the NSURLCache when using NSURLSession and iOS 7/8. NSURLConnection works fine on all these, and NSURLSession works fine on iOS 9. Best guess is there's a bug in the NSURLProtocol implementation that fails to insert into the cache. So, here we are creating a workaround for that specific case. CocoaSPDY will buffer the data internally, up to a certain size limit based on the cache size, and will manually insert into the cache when the response is done. This could potentially be expanded in the future for unclaimed, finished push responses, but for now those are excluded. This change also adds a few additional caching-related unit tests.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,6 +72,10 @@ @implementation SPDYStream | |
|
||
NSURLRequest *_pushRequest; // stored because we need a strong reference, _request is weak. | ||
NSHTTPURLResponse *_response; | ||
|
||
NSURLCacheStoragePolicy _cachePolicy; | ||
NSMutableData *_cacheDataBuffer; // only used for manual caching. | ||
NSInteger _cacheMaxItemSize; | ||
} | ||
|
||
- (instancetype)initWithProtocol:(SPDYProtocol *)protocol | ||
|
@@ -332,6 +336,9 @@ - (void)_close | |
[self markUnblocked]; // just in case. safe if already unblocked. | ||
_metadata.blockedMs = _blockedElapsed * 1000; | ||
|
||
// Manually make the willCacheResponse callback when needed | ||
[self _storeCacheResponse]; | ||
|
||
[_client URLProtocolDidFinishLoading:_protocol]; | ||
|
||
if (_delegate && [_delegate respondsToSelector:@selector(streamClosed:)]) { | ||
|
@@ -585,11 +592,24 @@ - (void)didReceiveResponse | |
return; | ||
} | ||
|
||
if (_client) { | ||
NSURLCacheStoragePolicy cachePolicy = SPDYCacheStoragePolicy(_request, _response); | ||
[_client URLProtocol:_protocol | ||
didReceiveResponse:_response | ||
cacheStoragePolicy:cachePolicy]; | ||
_cachePolicy = SPDYCacheStoragePolicy(_request, _response); | ||
[_client URLProtocol:_protocol | ||
didReceiveResponse:_response | ||
cacheStoragePolicy:_cachePolicy]; | ||
|
||
// Prepare for manual caching, if needed | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
kgoodier
Author
Contributor
|
||
NSURLCache *cache = _protocol.associatedSession.configuration.URLCache; | ||
if (_cachePolicy != NSURLCacheStorageNotAllowed && // cacheable? | ||
[self _shouldUseManualCaching] && // hack needed and NSURLSession used? | ||
cache != nil && // cache configured (NSURLSession-specific)? | ||
_local) { // not a push request? | ||
|
||
// The NSURLCache has a heuristic to limit the max size of items based on the capacity of the | ||
// cache. This is our attempt to mimic that behavior and prevent unlimited buffering of large | ||
// responses. These numbers were found by manually experimenting and are only approximate. | ||
// See SPDYNSURLCachingTest testResponseNearItemCacheSize_DoesUseCache. | ||
_cacheDataBuffer = [[NSMutableData alloc] init]; | ||
_cacheMaxItemSize = MAX(cache.memoryCapacity * 0.05, cache.diskCapacity * 0.01); | ||
This comment has been minimized.
Sorry, something went wrong.
NSProgrammer
Collaborator
|
||
} | ||
} | ||
|
||
|
@@ -689,7 +709,7 @@ - (void)didLoadData:(NSData *)data | |
NSUInteger inflatedLength = DECOMPRESSED_CHUNK_LENGTH - _zlibStream.avail_out; | ||
inflatedData.length = inflatedLength; | ||
if (inflatedLength > 0) { | ||
[_client URLProtocol:_protocol didLoadData:inflatedData]; | ||
[self _didLoadDataChunk:inflatedData]; | ||
} | ||
|
||
// This can happen if the decompressed data is size N * DECOMPRESSED_CHUNK_LENGTH, | ||
|
@@ -711,7 +731,77 @@ - (void)didLoadData:(NSData *)data | |
} | ||
} else { | ||
NSData *dataCopy = [[NSData alloc] initWithBytes:data.bytes length:dataLength]; | ||
[_client URLProtocol:_protocol didLoadData:dataCopy]; | ||
[self _didLoadDataChunk:dataCopy]; | ||
} | ||
} | ||
|
||
- (void)_didLoadDataChunk:(NSData *)data | ||
{ | ||
[_client URLProtocol:_protocol didLoadData:data]; | ||
|
||
if (_cacheDataBuffer) { | ||
NSUInteger bufferSize = _cacheDataBuffer.length + data.length; | ||
if (bufferSize < _cacheMaxItemSize) { | ||
This comment has been minimized.
Sorry, something went wrong.
NSProgrammer
Collaborator
|
||
[_cacheDataBuffer appendData:data]; | ||
} else { | ||
// Throw away anything already buffered, it's going to be too big | ||
_cacheDataBuffer = nil; | ||
} | ||
} | ||
} | ||
|
||
// NSURLSession on iOS 8 and iOS 7 does not cache items. Seems to be a bug with its interation | ||
// with NSURLProtocol. This flag represents whether our workaround, to insert into the cache | ||
// ourselves, is turned on. | ||
- (bool)_shouldUseManualCaching | ||
This comment has been minimized.
Sorry, something went wrong.
NSProgrammer
Collaborator
|
||
{ | ||
NSInteger osVersion = 0; | ||
NSProcessInfo *processInfo = [NSProcessInfo processInfo]; | ||
if ([processInfo respondsToSelector:@selector(operatingSystemVersion)]) { | ||
osVersion = [processInfo operatingSystemVersion].majorVersion; | ||
} | ||
|
||
// iOS 8 and earlier and using NSURLSession | ||
return (osVersion <= 8 && _protocol.associatedSession != nil && _protocol.associatedSessionTask != nil); | ||
} | ||
|
||
- (void)_storeCacheResponse | ||
{ | ||
if (_cacheDataBuffer == nil) { | ||
return; | ||
} | ||
|
||
NSCachedURLResponse *cachedResponse; | ||
cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:_response | ||
data:_cacheDataBuffer | ||
userInfo:nil | ||
storagePolicy:_cachePolicy]; | ||
NSURLCache *cache = _protocol.associatedSession.configuration.URLCache; | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
NSURLSessionDataTask *dataTask = (NSURLSessionDataTask *)_protocol.associatedSessionTask; | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
// Make "official" willCacheResponse callback to app, bypassing the NSURL loading system. | ||
id<NSURLSessionDataDelegate> delegate = (id)_protocol.associatedSession.delegate; | ||
if ([delegate respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) { | ||
NSOperationQueue *queue = _protocol.associatedSession.delegateQueue; | ||
This comment has been minimized.
Sorry, something went wrong.
NSProgrammer
Collaborator
|
||
[(queue) ?: [NSOperationQueue mainQueue] addOperationWithBlock:^{ | ||
This comment has been minimized.
Sorry, something went wrong.
NSProgrammer
Collaborator
|
||
[delegate URLSession:_protocol.associatedSession dataTask:dataTask willCacheResponse:cachedResponse completionHandler:^(NSCachedURLResponse * cachedResponse) { | ||
// This block may execute asynchronously at any time. No need to come back to the SPDY/NSURL thread | ||
if (cachedResponse) { | ||
if ([cache respondsToSelector:@selector(storeCachedResponse:forDataTask:)]) { | ||
[cache storeCachedResponse:cachedResponse forDataTask:dataTask]; | ||
} else { | ||
[cache storeCachedResponse:cachedResponse forRequest:_request]; | ||
} | ||
} | ||
}]; | ||
}]; | ||
} else { | ||
// willCacheResponse delegate not implemented. Default behavior is to cache. | ||
if ([cache respondsToSelector:@selector(storeCachedResponse:forDataTask:)]) { | ||
[cache storeCachedResponse:cachedResponse forDataTask:dataTask]; | ||
} else { | ||
[cache storeCachedResponse:cachedResponse forRequest:_request]; | ||
} | ||
} | ||
} | ||
|
||
|
getting the NSURLSession's cache every time when this could be an NSURLSession or NSURLConnection request is confusing me. Though it will be more verbose, it will also appear more explicit if we break it up a little: