FLEXNetworkRecorder.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. //
  2. // FLEXNetworkRecorder.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 2/4/15.
  6. // Copyright (c) 2015 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXNetworkRecorder.h"
  9. #import "FLEXNetworkCurlLogger.h"
  10. #import "FLEXNetworkTransaction.h"
  11. #import "FLEXUtility.h"
  12. #import "FLEXResources.h"
  13. NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
  14. NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
  15. NSString *const kFLEXNetworkRecorderUserInfoTransactionKey = @"transaction";
  16. NSString *const kFLEXNetworkRecorderTransactionsClearedNotification = @"kFLEXNetworkRecorderTransactionsClearedNotification";
  17. NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.responseCacheLimit";
  18. @interface FLEXNetworkRecorder ()
  19. @property (nonatomic) NSCache *responseCache;
  20. @property (nonatomic) NSMutableArray<FLEXNetworkTransaction *> *orderedTransactions;
  21. @property (nonatomic) NSMutableDictionary<NSString *, FLEXNetworkTransaction *> *networkTransactionsForRequestIdentifiers;
  22. @property (nonatomic) dispatch_queue_t queue;
  23. @end
  24. @implementation FLEXNetworkRecorder
  25. - (instancetype)init {
  26. self = [super init];
  27. if (self) {
  28. self.responseCache = [NSCache new];
  29. NSUInteger responseCacheLimit = [[NSUserDefaults.standardUserDefaults
  30. objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue
  31. ];
  32. // Default to 25 MB max. The cache will purge earlier if there is memory pressure.
  33. self.responseCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024;
  34. [self.responseCache setTotalCostLimit:responseCacheLimit];
  35. self.orderedTransactions = [NSMutableArray new];
  36. self.networkTransactionsForRequestIdentifiers = [NSMutableDictionary new];
  37. // Serial queue used because we use mutable objects that are not thread safe
  38. self.queue = dispatch_queue_create("com.flex.FLEXNetworkRecorder", DISPATCH_QUEUE_SERIAL);
  39. }
  40. return self;
  41. }
  42. + (instancetype)defaultRecorder {
  43. static FLEXNetworkRecorder *defaultRecorder = nil;
  44. static dispatch_once_t onceToken;
  45. dispatch_once(&onceToken, ^{
  46. defaultRecorder = [self new];
  47. });
  48. return defaultRecorder;
  49. }
  50. #pragma mark - Public Data Access
  51. - (NSUInteger)responseCacheByteLimit {
  52. return self.responseCache.totalCostLimit;
  53. }
  54. - (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit {
  55. self.responseCache.totalCostLimit = responseCacheByteLimit;
  56. [NSUserDefaults.standardUserDefaults
  57. setObject:@(responseCacheByteLimit)
  58. forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey
  59. ];
  60. }
  61. - (NSArray<FLEXNetworkTransaction *> *)networkTransactions {
  62. __block NSArray<FLEXNetworkTransaction *> *transactions = nil;
  63. dispatch_sync(self.queue, ^{
  64. transactions = self.orderedTransactions.copy;
  65. });
  66. return transactions;
  67. }
  68. - (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction {
  69. return [self.responseCache objectForKey:transaction.requestID];
  70. }
  71. - (void)clearRecordedActivity {
  72. dispatch_async(self.queue, ^{
  73. [self.responseCache removeAllObjects];
  74. [self.orderedTransactions removeAllObjects];
  75. [self.networkTransactionsForRequestIdentifiers removeAllObjects];
  76. [self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil];
  77. });
  78. }
  79. #pragma mark - Network Events
  80. - (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID
  81. request:(NSURLRequest *)request
  82. redirectResponse:(NSURLResponse *)redirectResponse {
  83. for (NSString *host in self.hostBlacklist) {
  84. if ([request.URL.host hasSuffix:host]) {
  85. return;
  86. }
  87. }
  88. // Before async block to stay accurate
  89. NSDate *startDate = NSDate.date;
  90. if (redirectResponse) {
  91. [self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
  92. [self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
  93. }
  94. dispatch_async(self.queue, ^{
  95. FLEXNetworkTransaction *transaction = [FLEXNetworkTransaction new];
  96. transaction.requestID = requestID;
  97. transaction.request = request;
  98. transaction.startTime = startDate;
  99. [self.orderedTransactions insertObject:transaction atIndex:0];
  100. [self.networkTransactionsForRequestIdentifiers setObject:transaction forKey:requestID];
  101. transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse;
  102. [self postNewTransactionNotificationWithTransaction:transaction];
  103. });
  104. }
  105. - (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response {
  106. // Before async block to stay accurate
  107. NSDate *responseDate = NSDate.date;
  108. dispatch_async(self.queue, ^{
  109. FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
  110. if (!transaction) {
  111. return;
  112. }
  113. transaction.response = response;
  114. transaction.transactionState = FLEXNetworkTransactionStateReceivingData;
  115. transaction.latency = -[transaction.startTime timeIntervalSinceDate:responseDate];
  116. [self postUpdateNotificationForTransaction:transaction];
  117. });
  118. }
  119. - (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength {
  120. dispatch_async(self.queue, ^{
  121. FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
  122. if (!transaction) {
  123. return;
  124. }
  125. transaction.receivedDataLength += dataLength;
  126. [self postUpdateNotificationForTransaction:transaction];
  127. });
  128. }
  129. - (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody {
  130. NSDate *finishedDate = [NSDate date];
  131. dispatch_async(self.queue, ^{
  132. FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
  133. if (!transaction) {
  134. return;
  135. }
  136. transaction.transactionState = FLEXNetworkTransactionStateFinished;
  137. transaction.duration = -[transaction.startTime timeIntervalSinceDate:finishedDate];
  138. BOOL shouldCache = responseBody.length > 0;
  139. if (!self.shouldCacheMediaResponses) {
  140. NSArray<NSString *> *ignoredMIMETypePrefixes = @[ @"audio", @"image", @"video" ];
  141. for (NSString *ignoredPrefix in ignoredMIMETypePrefixes) {
  142. shouldCache = shouldCache && ![transaction.response.MIMEType hasPrefix:ignoredPrefix];
  143. }
  144. }
  145. if (shouldCache) {
  146. [self.responseCache setObject:responseBody forKey:requestID cost:responseBody.length];
  147. }
  148. NSString *mimeType = transaction.response.MIMEType;
  149. if ([mimeType hasPrefix:@"image/"] && responseBody.length > 0) {
  150. // Thumbnail image previews on a separate background queue
  151. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  152. NSInteger maxPixelDimension = UIScreen.mainScreen.scale * 32.0;
  153. transaction.responseThumbnail = [FLEXUtility
  154. thumbnailedImageWithMaxPixelDimension:maxPixelDimension
  155. fromImageData:responseBody
  156. ];
  157. [self postUpdateNotificationForTransaction:transaction];
  158. });
  159. } else if ([mimeType isEqual:@"application/json"]) {
  160. transaction.responseThumbnail = FLEXResources.jsonIcon;
  161. } else if ([mimeType isEqual:@"text/plain"]){
  162. transaction.responseThumbnail = FLEXResources.textPlainIcon;
  163. } else if ([mimeType isEqual:@"text/html"]) {
  164. transaction.responseThumbnail = FLEXResources.htmlIcon;
  165. } else if ([mimeType isEqual:@"application/x-plist"]) {
  166. transaction.responseThumbnail = FLEXResources.plistIcon;
  167. } else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) {
  168. transaction.responseThumbnail = FLEXResources.binaryIcon;
  169. } else if ([mimeType containsString:@"javascript"]) {
  170. transaction.responseThumbnail = FLEXResources.jsIcon;
  171. } else if ([mimeType containsString:@"xml"]) {
  172. transaction.responseThumbnail = FLEXResources.xmlIcon;
  173. } else if ([mimeType hasPrefix:@"audio"]) {
  174. transaction.responseThumbnail = FLEXResources.audioIcon;
  175. } else if ([mimeType hasPrefix:@"video"]) {
  176. transaction.responseThumbnail = FLEXResources.videoIcon;
  177. } else if ([mimeType hasPrefix:@"text"]) {
  178. transaction.responseThumbnail = FLEXResources.textIcon;
  179. }
  180. [self postUpdateNotificationForTransaction:transaction];
  181. });
  182. }
  183. - (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error {
  184. dispatch_async(self.queue, ^{
  185. FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
  186. if (!transaction) {
  187. return;
  188. }
  189. transaction.transactionState = FLEXNetworkTransactionStateFailed;
  190. transaction.duration = -[transaction.startTime timeIntervalSinceNow];
  191. transaction.error = error;
  192. [self postUpdateNotificationForTransaction:transaction];
  193. });
  194. }
  195. - (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID {
  196. dispatch_async(self.queue, ^{
  197. FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
  198. if (!transaction) {
  199. return;
  200. }
  201. transaction.requestMechanism = mechanism;
  202. [self postUpdateNotificationForTransaction:transaction];
  203. });
  204. }
  205. #pragma mark Notification Posting
  206. - (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction {
  207. [self notify:kFLEXNetworkRecorderNewTransactionNotification transaction:transaction];
  208. }
  209. - (void)postUpdateNotificationForTransaction:(FLEXNetworkTransaction *)transaction {
  210. [self notify:kFLEXNetworkRecorderTransactionUpdatedNotification transaction:transaction];
  211. }
  212. - (void)notify:(NSString *)name transaction:(FLEXNetworkTransaction *)transaction {
  213. NSDictionary *userInfo = nil;
  214. if (transaction) {
  215. userInfo = @{ kFLEXNetworkRecorderUserInfoTransactionKey : transaction };
  216. }
  217. dispatch_async(dispatch_get_main_queue(), ^{
  218. [NSNotificationCenter.defaultCenter postNotificationName:name object:self userInfo:userInfo];
  219. });
  220. }
  221. @end