FLEXLiveObjectsController.m 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. //
  2. // FLEXLiveObjectsController.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 5/28/14.
  6. // Copyright (c) 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FLEXLiveObjectsController.h"
  9. #import "FLEXHeapEnumerator.h"
  10. #import "FLEXObjectListViewController.h"
  11. #import "FLEXUtility.h"
  12. #import "FLEXScopeCarousel.h"
  13. #import "FLEXTableView.h"
  14. #import <objc/runtime.h>
  15. static const NSInteger kFLEXLiveObjectsSortAlphabeticallyIndex = 0;
  16. static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
  17. static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
  18. @interface FLEXLiveObjectsController ()
  19. @property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
  20. @property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
  21. @property (nonatomic, readonly) NSArray<NSString *> *allClassNames;
  22. @property (nonatomic) NSArray<NSString *> *filteredClassNames;
  23. @property (nonatomic) NSString *headerTitle;
  24. @end
  25. @implementation FLEXLiveObjectsController
  26. - (void)viewDidLoad {
  27. [super viewDidLoad];
  28. self.showsSearchBar = YES;
  29. self.showSearchBarInitially = YES;
  30. self.searchBarDebounceInterval = kFLEXDebounceInstant;
  31. self.showsCarousel = YES;
  32. self.carousel.items = @[@"A→Z", @"Count", @"Size"];
  33. #if !TARGET_OS_TV
  34. self.refreshControl = [UIRefreshControl new];
  35. [self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
  36. #endif
  37. [self reloadTableData];
  38. }
  39. - (void)viewDidAppear:(BOOL)animated {
  40. [super viewDidAppear:animated];
  41. dispatch_async(dispatch_get_main_queue(), ^{
  42. // This doesn't work unless it's wrapped in this dispatch_async call
  43. [self.searchController.searchBar becomeFirstResponder];
  44. });
  45. }
  46. - (NSArray<NSString *> *)allClassNames {
  47. return self.instanceCountsForClassNames.allKeys;
  48. }
  49. - (void)reloadTableData {
  50. // Set up a CFMutableDictionary with class pointer keys and NSUInteger values.
  51. // We abuse CFMutableDictionary a little to have primitive keys through judicious casting, but it gets the job done.
  52. // The dictionary is intialized with a 0 count for each class so that it doesn't have to expand during enumeration.
  53. // While it might be a little cleaner to populate an NSMutableDictionary with class name string keys to NSNumber counts,
  54. // we choose the CF/primitives approach because it lets us enumerate the objects in the heap without allocating any memory during enumeration.
  55. // The alternative of creating one NSString/NSNumber per object on the heap ends up polluting the count of live objects quite a bit.
  56. unsigned int classCount = 0;
  57. Class *classes = objc_copyClassList(&classCount);
  58. CFMutableDictionaryRef mutableCountsForClasses = CFDictionaryCreateMutable(NULL, classCount, NULL, NULL);
  59. for (unsigned int i = 0; i < classCount; i++) {
  60. CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)classes[i], (const void *)0);
  61. }
  62. // Enumerate all objects on the heap to build the counts of instances for each class.
  63. [FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
  64. NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)actualClass);
  65. instanceCount++;
  66. CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)actualClass, (const void *)instanceCount);
  67. }];
  68. // Convert our CF primitive dictionary into a nicer mapping of class name strings to counts that we will use as the table's model.
  69. NSMutableDictionary<NSString *, NSNumber *> *mutableCountsForClassNames = [NSMutableDictionary new];
  70. NSMutableDictionary<NSString *, NSNumber *> *mutableSizesForClassNames = [NSMutableDictionary new];
  71. for (unsigned int i = 0; i < classCount; i++) {
  72. Class class = classes[i];
  73. NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
  74. NSString *className = @(class_getName(class));
  75. if (instanceCount > 0) {
  76. [mutableCountsForClassNames setObject:@(instanceCount) forKey:className];
  77. }
  78. [mutableSizesForClassNames setObject:@(class_getInstanceSize(class)) forKey:className];
  79. }
  80. free(classes);
  81. self.instanceCountsForClassNames = mutableCountsForClassNames;
  82. self.instanceSizesForClassNames = mutableSizesForClassNames;
  83. [self updateSearchResults:nil];
  84. }
  85. - (void)refreshControlDidRefresh:(id)sender {
  86. [self reloadTableData];
  87. #if !TARGET_OS_TV
  88. [self.refreshControl endRefreshing];
  89. #endif
  90. }
  91. - (void)updateHeaderTitle {
  92. NSUInteger totalCount = 0;
  93. NSUInteger totalSize = 0;
  94. for (NSString *className in self.allClassNames) {
  95. NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
  96. totalCount += count;
  97. totalSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
  98. }
  99. NSUInteger filteredCount = 0;
  100. NSUInteger filteredSize = 0;
  101. for (NSString *className in self.filteredClassNames) {
  102. NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
  103. filteredCount += count;
  104. filteredSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
  105. }
  106. if (filteredCount == totalCount) {
  107. // Unfiltered
  108. self.headerTitle = [NSString
  109. stringWithFormat:@"%@ objects, %@",
  110. @(totalCount), [NSByteCountFormatter
  111. stringFromByteCount:totalSize
  112. countStyle:NSByteCountFormatterCountStyleFile
  113. ]
  114. ];
  115. } else {
  116. self.headerTitle = [NSString
  117. stringWithFormat:@"%@ of %@ objects, %@",
  118. @(filteredCount), @(totalCount), [NSByteCountFormatter
  119. stringFromByteCount:filteredSize
  120. countStyle:NSByteCountFormatterCountStyleFile
  121. ]
  122. ];
  123. }
  124. }
  125. #pragma mark - FLEXGlobalsEntry
  126. + (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
  127. return @"💩 Heap Objects";
  128. }
  129. + (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
  130. FLEXLiveObjectsController *liveObjectsViewController = [self new];
  131. liveObjectsViewController.title = [self globalsEntryTitle:row];
  132. return liveObjectsViewController;
  133. }
  134. #pragma mark - Search bar
  135. - (void)updateSearchResults:(NSString *)filter {
  136. NSInteger selectedScope = self.selectedScope;
  137. if (filter.length) {
  138. NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", filter];
  139. self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPredicate];
  140. } else {
  141. self.filteredClassNames = self.allClassNames;
  142. }
  143. if (selectedScope == kFLEXLiveObjectsSortAlphabeticallyIndex) {
  144. self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
  145. } else if (selectedScope == kFLEXLiveObjectsSortByCountIndex) {
  146. self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
  147. NSNumber *count1 = self.instanceCountsForClassNames[className1];
  148. NSNumber *count2 = self.instanceCountsForClassNames[className2];
  149. // Reversed for descending counts.
  150. return [count2 compare:count1];
  151. }];
  152. } else if (selectedScope == kFLEXLiveObjectsSortBySizeIndex) {
  153. self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
  154. NSNumber *count1 = self.instanceCountsForClassNames[className1];
  155. NSNumber *count2 = self.instanceCountsForClassNames[className2];
  156. NSNumber *size1 = self.instanceSizesForClassNames[className1];
  157. NSNumber *size2 = self.instanceSizesForClassNames[className2];
  158. // Reversed for descending sizes.
  159. return [@(count2.integerValue * size2.integerValue) compare:@(count1.integerValue * size1.integerValue)];
  160. }];
  161. }
  162. [self updateHeaderTitle];
  163. [self.tableView reloadData];
  164. }
  165. #pragma mark - Table view data source
  166. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
  167. return 1;
  168. }
  169. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  170. return self.filteredClassNames.count;
  171. }
  172. - (UITableViewCell *)tableView:(__kindof UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  173. UITableViewCell *cell = [tableView
  174. dequeueReusableCellWithIdentifier:kFLEXDefaultCell
  175. forIndexPath:indexPath
  176. ];
  177. NSString *className = self.filteredClassNames[indexPath.row];
  178. NSNumber *count = self.instanceCountsForClassNames[className];
  179. NSNumber *size = self.instanceSizesForClassNames[className];
  180. unsigned long totalSize = count.unsignedIntegerValue * size.unsignedIntegerValue;
  181. cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  182. cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld, %@)",
  183. className, (long)[count integerValue],
  184. [NSByteCountFormatter
  185. stringFromByteCount:totalSize
  186. countStyle:NSByteCountFormatterCountStyleFile
  187. ]
  188. ];
  189. return cell;
  190. }
  191. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
  192. return self.headerTitle;
  193. }
  194. #pragma mark - Table view delegate
  195. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  196. NSString *className = self.filteredClassNames[indexPath.row];
  197. UIViewController *instances = [FLEXObjectListViewController instancesOfClassWithName:className];
  198. [self.navigationController pushViewController:instances animated:YES];
  199. }
  200. @end