FLEXNetworkRecorder.m 11 KB

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