Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd695ed106 | |||
| 69c1719159 | |||
| 4019518bf5 | |||
| 3fd8e7c77d | |||
| 99c3bcb8c5 | |||
| b587e96e70 | |||
| ef8f0a303e | |||
| cfb1e4caab | |||
| 2411c331cd | |||
| fd4b38f46d | |||
| 269e31894c | |||
| 2f2da50aed | |||
| d87779212c | |||
| 5db6a12c6e | |||
| 6d0f776102 | |||
| 6c83ddc2c7 | |||
| b510d24e13 | |||
| 6cdf2e61dc | |||
| 9b0ed83ff5 | |||
| dbe1b93f48 | |||
| 06444f1576 | |||
| 9bbf1d0d48 | |||
| 0562f15cd0 | |||
| eb63c91481 | |||
| f916174070 | |||
| 60e23e126e | |||
| afeff1b562 | |||
| 5acb33005b | |||
| 3446eff353 | |||
| ad1f1f579e | |||
| e03b5f7e5d | |||
| d010c82dd0 | |||
| 652d03c39a | |||
| 1d39669a52 | |||
| d558ca6852 | |||
| ad32ca0f05 |
@@ -74,7 +74,7 @@
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText {
|
||||
NSArray *(^filter)() = ^NSArray *{
|
||||
NSArray *(^filter)(void) = ^NSArray *{
|
||||
self.filterText = newText;
|
||||
|
||||
// Sections will adjust data based on this property
|
||||
|
||||
@@ -96,7 +96,10 @@
|
||||
- (void)dismissAnimated {
|
||||
// Tabs are only closed if the done button is pressed; this
|
||||
// allows you to leave a tab open by dragging down to dismiss
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
}
|
||||
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
@@ -559,7 +559,7 @@ static UITextField *kDummyTextField = nil;
|
||||
[self.debounceTimer invalidate];
|
||||
NSString *text = searchController.searchBar.text;
|
||||
|
||||
void (^updateSearchResults)() = ^{
|
||||
void (^updateSearchResults)(void) = ^{
|
||||
if (self.searchResultsUpdater) {
|
||||
[self.searchResultsUpdater updateSearchResults:text];
|
||||
} else {
|
||||
|
||||
@@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXDefaultEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(nullable id)currentValue;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
@implementation FLEXDefaultEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit {
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXDefaultEditorViewController *editor = [self target:defaults data:key commitHandler:onCommit];
|
||||
editor.title = @"Edit Default";
|
||||
return editor;
|
||||
|
||||
@@ -15,9 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
/// @return nil if the property is readonly or if the type is unsupported
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @return nil if the ivar type is unsupported
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
|
||||
|
||||
@@ -30,19 +30,14 @@
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)())onCommit {
|
||||
id value = [property getValue:target];
|
||||
if (![self canEditProperty:property onObject:target currentValue:value]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:property commitHandler:onCommit];
|
||||
editor.title = [@"Property: " stringByAppendingString:property.name];
|
||||
editor.property = property;
|
||||
return editor;
|
||||
}
|
||||
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit {
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit];
|
||||
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
|
||||
editor.ivar = ivar;
|
||||
@@ -151,14 +146,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditProperty:(FLEXProperty *)property onObject:(id)object currentValue:(id)value {
|
||||
const FLEXTypeEncoding *typeEncoding = property.attributes.typeEncoding.UTF8String;
|
||||
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
|
||||
return canEditType && [object respondsToSelector:property.likelySetter];
|
||||
}
|
||||
|
||||
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value {
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -21,17 +21,17 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@protected
|
||||
id _target;
|
||||
_Nullable id _data;
|
||||
void (^_Nullable _commitHandler)();
|
||||
void (^_Nullable _commitHandler)(void);
|
||||
}
|
||||
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit;
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
@property (nonatomic, readonly) id target;
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
return [[self alloc] initWithTarget:target data:data commitHandler:onCommit];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_target = target;
|
||||
|
||||
@@ -460,17 +460,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
|
||||
];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
|
||||
// Swipe gestures for selecting deeper / higher views at a point
|
||||
UIPanGestureRecognizer *leftSwipe = [[UIPanGestureRecognizer alloc]
|
||||
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
// UIPanGestureRecognizer *rightSwipe = [[UIPanGestureRecognizer alloc]
|
||||
// initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
// ];
|
||||
// leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
// rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
|
||||
// [toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:panGesture];
|
||||
|
||||
// Long press gesture to present tabs manager
|
||||
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
|
||||
- (void)showRevertOrDismissAlert:(void(^)(void))revertBlock {
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
[self reloadData];
|
||||
[self.tableView reloadData];
|
||||
|
||||
@@ -80,10 +80,10 @@
|
||||
|
||||
- (void)closeTab:(UINavigationController *)tab {
|
||||
NSParameterAssert(tab);
|
||||
NSParameterAssert([self.openTabs containsObject:tab]);
|
||||
NSInteger idx = [self.openTabs indexOfObject:tab];
|
||||
|
||||
[self closeTabAtIndex:idx];
|
||||
if (idx != NSNotFound) {
|
||||
[self closeTabAtIndex:idx];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeTabAtIndex:(NSInteger)idx {
|
||||
|
||||
@@ -226,7 +226,10 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
UIViewController *instances = [FLEXObjectListViewController instancesOfClassWithName:className];
|
||||
UIViewController *instances = [FLEXObjectListViewController
|
||||
instancesOfClassWithName:className
|
||||
retained:YES
|
||||
];
|
||||
[self.navigationController pushViewController:instances animated:YES];
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
/// This will either return a list of the instances, or take you straight
|
||||
/// to the explorer itself if there is only one instance.
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className;
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain;
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)className;
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object;
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain;
|
||||
|
||||
@end
|
||||
|
||||
@@ -141,45 +141,33 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className {
|
||||
const char *classNameCString = className.UTF8String;
|
||||
NSMutableArray *instances = [NSMutableArray new];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
|
||||
if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
|
||||
// Note: objects of certain classes crash when retain is called.
|
||||
// It is up to the user to avoid tapping into instance lists for these classes.
|
||||
// Ex. OS_dispatch_queue_specific_queue
|
||||
// In the future, we could provide some kind of warning for classes that are known to be problematic.
|
||||
if (malloc_size((__bridge const void *)(object)) > 0) {
|
||||
[instances addObject:object];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain {
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator
|
||||
instancesOfClassWithName:className retained:retain
|
||||
];
|
||||
|
||||
if (references.count == 1) {
|
||||
return [FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:references.firstObject.object
|
||||
explorerViewControllerForObject:references.firstObject.object
|
||||
];
|
||||
}
|
||||
|
||||
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
|
||||
controller.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
|
||||
controller.title = [NSString stringWithFormat:@"%@ (%@)", className, @(references.count)];
|
||||
return controller;
|
||||
}
|
||||
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)className {
|
||||
NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator subclassesOfClassWithName:className];
|
||||
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
|
||||
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%lu)",
|
||||
className, (unsigned long)classes.count
|
||||
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%@)",
|
||||
className, @(references.count)
|
||||
];
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object {
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain {
|
||||
static Class SwiftObjectClass = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -189,35 +177,9 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
|
||||
}
|
||||
});
|
||||
|
||||
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray new];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
|
||||
// Get all the ivars on the object. Start with the class and and travel up the inheritance chain.
|
||||
// Once we find a match, record it and move on to the next object. There's no reason to find multiple matches within the same object.
|
||||
Class tryClass = actualClass;
|
||||
while (tryClass) {
|
||||
unsigned int ivarCount = 0;
|
||||
Ivar *ivars = class_copyIvarList(tryClass, &ivarCount);
|
||||
|
||||
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
|
||||
Ivar ivar = ivars[ivarIndex];
|
||||
NSString *typeEncoding = @(ivar_getTypeEncoding(ivar) ?: "");
|
||||
|
||||
if (typeEncoding.flex_typeIsObjectOrClass) {
|
||||
ptrdiff_t offset = ivar_getOffset(ivar);
|
||||
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
|
||||
|
||||
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
|
||||
NSString *ivarName = @(ivar_getName(ivar) ?: "???");
|
||||
[instances addObject:[FLEXObjectRef referencing:tryObject ivar:ivarName]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(ivars);
|
||||
tryClass = class_getSuperclass(tryClass);
|
||||
}
|
||||
}];
|
||||
NSArray<FLEXObjectRef *> *instances = [FLEXHeapEnumerator
|
||||
objectsWithReferencesToObject:object retained:retain
|
||||
];
|
||||
|
||||
FLEXObjectListViewController *viewController = [[self alloc]
|
||||
initWithReferences:instances
|
||||
|
||||
@@ -10,10 +10,19 @@
|
||||
|
||||
@interface FLEXObjectRef : NSObject
|
||||
|
||||
+ (instancetype)referencing:(id)object;
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName;
|
||||
/// Reference an object without affecting its lifespan or or emitting reference-counting operations.
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object;
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName;
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects;
|
||||
/// Reference an object and control its lifespan.
|
||||
+ (instancetype)retained:(id)object;
|
||||
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName;
|
||||
|
||||
/// Reference an object and conditionally choose to retain it or not.
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain;
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain;
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain;
|
||||
/// Classes do not have a summary, and the reference is just the class name.
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes;
|
||||
|
||||
@@ -22,6 +31,11 @@
|
||||
/// For instances, this is the result of -[FLEXRuntimeUtility summaryForObject:]
|
||||
/// For classes, there is no summary.
|
||||
@property (nonatomic, readonly) NSString *summary;
|
||||
@property (nonatomic, readonly) id object;
|
||||
@property (nonatomic, readonly, unsafe_unretained) id object;
|
||||
|
||||
/// Retains the referenced object if it is not already retained
|
||||
- (void)retainObject;
|
||||
/// Releases the referenced object if it is already retained
|
||||
- (void)releaseObject;
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,42 +10,68 @@
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
|
||||
@interface FLEXObjectRef ()
|
||||
@interface FLEXObjectRef () {
|
||||
/// Used to retain the object if desired
|
||||
id _retainer;
|
||||
}
|
||||
@property (nonatomic, readonly) BOOL wantsSummary;
|
||||
@end
|
||||
|
||||
@implementation FLEXObjectRef
|
||||
@synthesize summary = _summary;
|
||||
|
||||
+ (instancetype)referencing:(id)object {
|
||||
return [self referencing:object showSummary:YES];
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object {
|
||||
return [self referencing:object showSummary:YES retained:NO];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(id)object showSummary:(BOOL)showSummary {
|
||||
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary];
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:NO];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES];
|
||||
+ (instancetype)retained:(id)object {
|
||||
return [self referencing:object showSummary:YES retained:YES];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects {
|
||||
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:YES];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain {
|
||||
return retain ? [self retained:object] : [self unretained:object];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain {
|
||||
return retain ? [self retained:object ivar:ivarName] : [self unretained:object ivar:ivarName];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object showSummary:(BOOL)showSummary retained:(BOOL)retain {
|
||||
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary retained:retain];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain {
|
||||
return [objects flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:YES];
|
||||
return [self referencing:obj showSummary:YES retained:retain];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes {
|
||||
return [classes flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:NO];
|
||||
return [self referencing:obj showSummary:NO retained:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)initWithObject:(id)object ivarName:(NSString *)ivar showSummary:(BOOL)showSummary {
|
||||
- (id)initWithObject:(__unsafe_unretained id)object
|
||||
ivarName:(NSString *)ivar
|
||||
showSummary:(BOOL)showSummary
|
||||
retained:(BOOL)retain {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_object = object;
|
||||
_wantsSummary = showSummary;
|
||||
|
||||
if (retain) {
|
||||
_retainer = object;
|
||||
}
|
||||
|
||||
NSString *class = [FLEXRuntimeUtility safeClassNameForObject:object];
|
||||
if (ivar) {
|
||||
@@ -73,4 +99,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)retainObject {
|
||||
if (!_retainer) {
|
||||
_retainer = _object;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)releaseObject {
|
||||
_retainer = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
self = [self initWithNibName:nil bundle:nil];
|
||||
if (self) {
|
||||
self.originalText = text;
|
||||
NSString *htmlString = [NSString stringWithFormat:@"<head><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
|
||||
NSString *htmlString = [NSString stringWithFormat:@"<head><style>:root{ color-scheme: light dark; }</style><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
|
||||
[self.webView loadHTMLString:htmlString baseURL:nil];
|
||||
}
|
||||
return self;
|
||||
|
||||
@@ -195,7 +195,7 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
|
||||
"/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy",
|
||||
RTLD_LAZY
|
||||
);
|
||||
void (*WebKitInitialize)() = dlsym(handle, "WebKitInitialize");
|
||||
void (*WebKitInitialize)(void) = dlsym(handle, "WebKitInitialize");
|
||||
if (WebKitInitialize) {
|
||||
NSAssert(NSThread.isMainThread,
|
||||
@"WebKitInitialize can only be called on the main thread"
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
#import "FLEXKeyPathSearchController.h"
|
||||
#import "FLEXRuntimeBrowserToolbar.h"
|
||||
#import "UIGestureRecognizer+Blocks.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXRuntimeClient.h"
|
||||
#import <dlfcn.h>
|
||||
|
||||
@interface FLEXObjcRuntimeViewController () <FLEXKeyPathSearchControllerDelegate>
|
||||
|
||||
@@ -43,6 +45,8 @@
|
||||
]
|
||||
];
|
||||
|
||||
[self addToolbarItems:@[FLEXBarButtonItem(@"dlopen()", self, @selector(dlopenPressed:))]];
|
||||
|
||||
// Search bar stuff, must be first because this creates self.searchController
|
||||
self.showsSearchBar = YES;
|
||||
self.showSearchBarInitially = YES;
|
||||
@@ -74,6 +78,63 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark dlopen
|
||||
|
||||
/// Prompt user for dlopen shortcuts to choose from
|
||||
- (void)dlopenPressed:(id)sender {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Dynamically Open Library");
|
||||
make.message(@"Invoke dlopen() with the given path. Choose an option below.");
|
||||
|
||||
make.button(@"System Framework").handler(^(NSArray<NSString *> *_) {
|
||||
[self dlopenWithFormat:@"/System/Library/Frameworks/%@.framework/%@"];
|
||||
});
|
||||
make.button(@"System Private Framework").handler(^(NSArray<NSString *> *_) {
|
||||
[self dlopenWithFormat:@"/System/Library/PrivateFrameworks/%@.framework/%@"];
|
||||
});
|
||||
make.button(@"Arbitrary Binary").handler(^(NSArray<NSString *> *_) {
|
||||
[self dlopenWithFormat:nil];
|
||||
});
|
||||
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
/// Prompt user for input and dlopen
|
||||
- (void)dlopenWithFormat:(NSString *)format {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Dynamically Open Library");
|
||||
if (format) {
|
||||
make.message(@"Pass in a framework name, such as CarKit or FrontBoard.");
|
||||
} else {
|
||||
make.message(@"Pass in an absolute path to a binary.");
|
||||
}
|
||||
|
||||
make.textField(format ? @"ARKit" : @"/System/Library/Frameworks/ARKit.framework/ARKit");
|
||||
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
make.button(@"Open").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *path = strings[0];
|
||||
|
||||
if (path.length < 2) {
|
||||
[self dlopenInvalidPath];
|
||||
} else if (format) {
|
||||
path = [NSString stringWithFormat:format, path, path];
|
||||
}
|
||||
|
||||
dlopen(path.UTF8String, RTLD_NOW);
|
||||
});
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)dlopenInvalidPath {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert * _Nonnull make) {
|
||||
make.title(@"Path or Name Too Short");
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Delegate stuff
|
||||
|
||||
- (void)didSelectImagePath:(NSString *)path shortName:(NSString *)shortName {
|
||||
|
||||
@@ -168,11 +168,10 @@ static uint8_t (*OSLogGetType)(void *);
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSince1970:log_message->tv_gmt.tv_sec];
|
||||
|
||||
// Get log message text
|
||||
const char *messageText = OSLogCopyFormattedMessage(log_message);
|
||||
// https://github.com/limneos/oslog/issues/1
|
||||
if (entry->log_message.format && !(strcmp(entry->log_message.format, messageText))) {
|
||||
messageText = (char *)entry->log_message.format;
|
||||
}
|
||||
// https://github.com/FLEXTool/FLEX/issues/564
|
||||
const char *messageText = OSLogCopyFormattedMessage(log_message) ?: "";
|
||||
|
||||
// move messageText from stack to heap
|
||||
NSString *msg = [NSString stringWithUTF8String:messageText];
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXHTTPTransactionDetailController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/10/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXHTTPTransaction;
|
||||
|
||||
@interface FLEXHTTPTransactionDetailController : UITableViewController
|
||||
|
||||
+ (instancetype)withTransaction:(FLEXHTTPTransaction *)transaction;
|
||||
|
||||
@end
|
||||
+38
-33
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXNetworkTransactionDetailController.h"
|
||||
#import "FLEXHTTPTransactionDetailController.h"
|
||||
#import "FLEXNetworkCurlLogger.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
@@ -22,64 +22,63 @@
|
||||
typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
|
||||
@interface FLEXNetworkDetailRow : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *detailText;
|
||||
@property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkDetailRow
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXNetworkDetailSection : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkDetailRow *> *rows;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkDetailSection
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXNetworkTransactionDetailController ()
|
||||
@interface FLEXHTTPTransactionDetailController ()
|
||||
|
||||
@property (nonatomic, readonly) FLEXHTTPTransaction *transaction;
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkDetailSection *> *sections;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkTransactionDetailController
|
||||
@implementation FLEXHTTPTransactionDetailController
|
||||
|
||||
+ (instancetype)withTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXHTTPTransactionDetailController *controller = [self new];
|
||||
controller.transaction = transaction;
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewStyle)style {
|
||||
// Force grouped style
|
||||
self = [super initWithStyle:UITableViewStyleGrouped];
|
||||
if (self) {
|
||||
[NSNotificationCenter.defaultCenter addObserver:self
|
||||
selector:@selector(handleTransactionUpdatedNotification:)
|
||||
name:kFLEXNetworkRecorderTransactionUpdatedNotification
|
||||
object:nil
|
||||
];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[UIBarButtonItem
|
||||
flex_itemWithTitle:@"Copy curl"
|
||||
target:self
|
||||
action:@selector(copyButtonPressed:)
|
||||
]
|
||||
];
|
||||
}
|
||||
return self;
|
||||
return [super initWithStyle:UITableViewStyleGrouped];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
[NSNotificationCenter.defaultCenter addObserver:self
|
||||
selector:@selector(handleTransactionUpdatedNotification:)
|
||||
name:kFLEXNetworkRecorderTransactionUpdatedNotification
|
||||
object:nil
|
||||
];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[UIBarButtonItem
|
||||
flex_itemWithTitle:@"Copy curl"
|
||||
target:self
|
||||
action:@selector(copyButtonPressed:)
|
||||
]
|
||||
];
|
||||
|
||||
[self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineCell];
|
||||
}
|
||||
|
||||
- (void)setTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
- (void)setTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
if (![_transaction isEqual:transaction]) {
|
||||
_transaction = transaction;
|
||||
self.title = [transaction.request.URL lastPathComponent];
|
||||
@@ -189,6 +188,12 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
|
||||
return [NSArray flex_forEachUpTo:self.sections.count map:^id(NSUInteger i) {
|
||||
return @"⦁";
|
||||
}];
|
||||
}
|
||||
|
||||
- (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXNetworkDetailSection *sectionModel = self.sections[indexPath.section];
|
||||
return sectionModel.rows[indexPath.row];
|
||||
@@ -253,7 +258,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
|
||||
#pragma mark - Table Data Generation
|
||||
|
||||
+ (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
|
||||
|
||||
FLEXNetworkDetailRow *requestURLRow = [FLEXNetworkDetailRow new];
|
||||
@@ -407,7 +412,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return generalSection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXNetworkDetailSection *requestHeadersSection = [FLEXNetworkDetailSection new];
|
||||
requestHeadersSection.title = @"Request Headers";
|
||||
requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
|
||||
@@ -415,7 +420,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return requestHeadersSection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXNetworkDetailSection *postBodySection = [FLEXNetworkDetailSection new];
|
||||
postBodySection.title = @"Request Body Parameters";
|
||||
if (transaction.cachedRequestBody.length > 0) {
|
||||
@@ -429,7 +434,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return postBodySection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
NSArray<NSURLQueryItem *> *queries = [FLEXUtility itemsFromQueryString:transaction.request.URL.query];
|
||||
FLEXNetworkDetailSection *querySection = [FLEXNetworkDetailSection new];
|
||||
querySection.title = @"Query Parameters";
|
||||
@@ -438,7 +443,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return querySection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXNetworkDetailSection *responseHeadersSection = [FLEXNetworkDetailSection new];
|
||||
responseHeadersSection.title = @"Response Headers";
|
||||
if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
@@ -516,7 +521,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return detailViewController;
|
||||
}
|
||||
|
||||
+ (NSData *)postBodyDataForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (NSData *)postBodyDataForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
NSData *bodyData = transaction.cachedRequestBody;
|
||||
if (bodyData.length > 0) {
|
||||
NSString *contentEncoding = [transaction.request valueForHTTPHeaderField:@"Content-Encoding"];
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// FLEXMITMDataSource.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/22/21.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXMITMDataSource<__covariant TransactionType> : NSObject
|
||||
|
||||
+ (instancetype)dataSourceWithProvider:(NSArray<TransactionType> *(^)(void))future;
|
||||
|
||||
@property (nonatomic, readonly) NSArray<TransactionType> *transactions;
|
||||
@property (nonatomic, readonly) NSArray<TransactionType> *allTransactions;
|
||||
/// Equal to \c allTransactions if not filtered
|
||||
@property (nonatomic, readonly) NSArray<TransactionType> *filteredTransactions;
|
||||
|
||||
/// Use this instead of either of the other two as it updates based on whether we have a filter or not
|
||||
@property (nonatomic) NSInteger bytesReceived;
|
||||
@property (nonatomic) NSInteger totalBytesReceived;
|
||||
/// Equal to \c totalBytesReceived if not filtered
|
||||
@property (nonatomic) NSInteger filteredBytesReceived;
|
||||
|
||||
- (void)reloadByteCounts;
|
||||
- (void)reloadData:(void (^_Nullable)(FLEXMITMDataSource *dataSource))completion;
|
||||
- (void)filter:(NSString *)searchString completion:(void(^_Nullable)(FLEXMITMDataSource *dataSource))completion;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// FLEXMITMDataSource.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/22/21.
|
||||
//
|
||||
|
||||
#import "FLEXMITMDataSource.h"
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMITMDataSource ()
|
||||
@property (nonatomic, readonly) NSArray *(^dataProvider)(void);
|
||||
@property (nonatomic) NSString *filterString;
|
||||
@end
|
||||
|
||||
@implementation FLEXMITMDataSource
|
||||
|
||||
+ (instancetype)dataSourceWithProvider:(NSArray<id> *(^)(void))future {
|
||||
FLEXMITMDataSource *ds = [self new];
|
||||
ds->_dataProvider = future;
|
||||
[ds reloadData:nil];
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
- (NSArray *)transactions {
|
||||
return _filteredTransactions;
|
||||
}
|
||||
|
||||
- (NSInteger)bytesReceived {
|
||||
return _filteredBytesReceived;
|
||||
}
|
||||
|
||||
- (void)reloadByteCounts {
|
||||
[self updateBytesReceived];
|
||||
[self updateFilteredBytesReceived];
|
||||
}
|
||||
|
||||
- (void)reloadData:(void (^)(FLEXMITMDataSource *dataSource))completion {
|
||||
self.allTransactions = self.dataProvider();
|
||||
[self filter:self.filterString completion:completion];
|
||||
}
|
||||
|
||||
- (void)filter:(NSString *)searchString completion:(void (^)(FLEXMITMDataSource *dataSource))completion {
|
||||
self.filterString = searchString;
|
||||
|
||||
if (!searchString.length) {
|
||||
self.filteredTransactions = self.allTransactions;
|
||||
if (completion) completion(self);
|
||||
} else {
|
||||
[self onBackgroundQueue:^NSArray *{
|
||||
return [self.allTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *entry, NSUInteger idx) {
|
||||
return [entry matchesQuery:searchString];
|
||||
}];
|
||||
} thenOnMainQueue:^(NSArray *filteredNetworkTransactions) {
|
||||
if ([self.filterString isEqual:searchString]) {
|
||||
self.filteredTransactions = filteredNetworkTransactions;
|
||||
if (completion) completion(self);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAllTransactions:(NSArray *)transactions {
|
||||
_allTransactions = transactions;
|
||||
[self updateBytesReceived];
|
||||
}
|
||||
|
||||
- (void)setFilteredTransactions:(NSArray *)filteredTransactions {
|
||||
_filteredTransactions = filteredTransactions;
|
||||
[self updateFilteredBytesReceived];
|
||||
}
|
||||
|
||||
- (void)updateBytesReceived {
|
||||
NSInteger bytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.transactions) {
|
||||
bytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
|
||||
self.bytesReceived = bytesReceived;
|
||||
}
|
||||
|
||||
- (void)updateFilteredBytesReceived {
|
||||
NSInteger filteredBytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.filteredTransactions) {
|
||||
filteredBytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
|
||||
self.filteredBytesReceived = filteredBytesReceived;
|
||||
}
|
||||
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *items = backgroundBlock();
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
mainBlock(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -9,6 +9,7 @@
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
/// The main screen for the network observer, which displays a list of network transactions.
|
||||
@interface FLEXNetworkMITMViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,29 +8,36 @@
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXMITMDataSource.h"
|
||||
#import "FLEXNetworkMITMViewController.h"
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
#import "FLEXNetworkObserver.h"
|
||||
#import "FLEXNetworkTransactionCell.h"
|
||||
#import "FLEXNetworkTransactionDetailController.h"
|
||||
#import "FLEXHTTPTransactionDetailController.h"
|
||||
#import "FLEXNetworkSettingsController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXGlobalsViewController.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXResources.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
|
||||
FLEXNetworkObserverModeREST = 0,
|
||||
FLEXNetworkObserverModeWebsockets,
|
||||
};
|
||||
|
||||
@interface FLEXNetworkMITMViewController ()
|
||||
|
||||
/// Backing model
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkTransaction *> *networkTransactions;
|
||||
@property (nonatomic) long long bytesReceived;
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkTransaction *> *filteredNetworkTransactions;
|
||||
@property (nonatomic) long long filteredBytesReceived;
|
||||
|
||||
@property (nonatomic) BOOL rowInsertInProgress;
|
||||
@property (nonatomic) BOOL isPresentingSearch;
|
||||
@property (nonatomic) BOOL updateInProgress;
|
||||
@property (nonatomic) BOOL pendingReload;
|
||||
|
||||
@property (nonatomic, readonly) FLEXNetworkObserverMode mode;
|
||||
|
||||
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXNetworkTransaction *> *dataSource;
|
||||
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXHTTPTransaction *> *HTTPDataSource;
|
||||
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXWebsocketTransaction *> *websocketDataSource;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkMITMViewController
|
||||
@@ -47,6 +54,18 @@
|
||||
self.showsSearchBar = YES;
|
||||
self.showSearchBarInitially = NO;
|
||||
|
||||
_HTTPDataSource = [FLEXMITMDataSource dataSourceWithProvider:^NSArray * {
|
||||
return FLEXNetworkRecorder.defaultRecorder.HTTPTransactions;
|
||||
}];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self.searchController.searchBar.showsScopeBar = YES;
|
||||
self.searchController.searchBar.scopeButtonTitles = @[@"REST", @"Websockets"];
|
||||
_websocketDataSource = [FLEXMITMDataSource dataSourceWithProvider:^NSArray * {
|
||||
return FLEXNetworkRecorder.defaultRecorder.websocketTransactions;
|
||||
}];
|
||||
}
|
||||
|
||||
[self addToolbarItems:@[
|
||||
[UIBarButtonItem
|
||||
flex_itemWithImage:FLEXResources.gearIcon
|
||||
@@ -61,14 +80,14 @@
|
||||
]];
|
||||
|
||||
[self.tableView
|
||||
registerClass:[FLEXNetworkTransactionCell class]
|
||||
forCellReuseIdentifier:kFLEXNetworkTransactionCellIdentifier
|
||||
registerClass:FLEXNetworkTransactionCell.class
|
||||
forCellReuseIdentifier:FLEXNetworkTransactionCell.reuseID
|
||||
];
|
||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.tableView.rowHeight = FLEXNetworkTransactionCell.preferredCellHeight;
|
||||
|
||||
[self registerForNotifications];
|
||||
[self updateTransactions];
|
||||
[self updateTransactions:nil];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
@@ -137,44 +156,36 @@
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Transactions
|
||||
|
||||
- (void)updateTransactions {
|
||||
self.networkTransactions = [FLEXNetworkRecorder.defaultRecorder networkTransactions];
|
||||
- (FLEXNetworkObserverMode)mode {
|
||||
return self.searchController.searchBar.selectedScopeButtonIndex;
|
||||
}
|
||||
|
||||
- (void)setNetworkTransactions:(NSArray<FLEXNetworkTransaction *> *)networkTransactions {
|
||||
if (![_networkTransactions isEqual:networkTransactions]) {
|
||||
_networkTransactions = networkTransactions;
|
||||
[self updateBytesReceived];
|
||||
[self updateFilteredBytesReceived];
|
||||
- (FLEXMITMDataSource<FLEXNetworkTransaction *> *)dataSource {
|
||||
switch (self.mode) {
|
||||
case FLEXNetworkObserverModeREST:
|
||||
return self.HTTPDataSource;
|
||||
case FLEXNetworkObserverModeWebsockets:
|
||||
return self.websocketDataSource;
|
||||
|
||||
default:
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateBytesReceived {
|
||||
long long bytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.networkTransactions) {
|
||||
bytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
self.bytesReceived = bytesReceived;
|
||||
[self updateFirstSectionHeader];
|
||||
- (void)updateTransactions:(void(^)(void))callback {
|
||||
id completion = ^(FLEXMITMDataSource *dataSource) {
|
||||
// Update byte count
|
||||
[self updateFirstSectionHeader];
|
||||
if (callback && dataSource == self.dataSource) callback();
|
||||
};
|
||||
|
||||
[self.HTTPDataSource reloadData:completion];
|
||||
[self.websocketDataSource reloadData:completion];
|
||||
}
|
||||
|
||||
- (void)setFilteredNetworkTransactions:(NSArray<FLEXNetworkTransaction *> *)networkTransactions {
|
||||
if (![_filteredNetworkTransactions isEqual:networkTransactions]) {
|
||||
_filteredNetworkTransactions = networkTransactions;
|
||||
[self updateFilteredBytesReceived];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateFilteredBytesReceived {
|
||||
long long filteredBytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.filteredNetworkTransactions) {
|
||||
filteredBytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
self.filteredBytesReceived = filteredBytesReceived;
|
||||
[self updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
#pragma mark Header
|
||||
|
||||
@@ -191,11 +202,11 @@
|
||||
long long bytesReceived = 0;
|
||||
NSInteger totalRequests = 0;
|
||||
if (self.searchController.isActive) {
|
||||
bytesReceived = self.filteredBytesReceived;
|
||||
totalRequests = self.filteredNetworkTransactions.count;
|
||||
bytesReceived = self.dataSource.filteredBytesReceived;
|
||||
totalRequests = self.dataSource.transactions.count;
|
||||
} else {
|
||||
bytesReceived = self.bytesReceived;
|
||||
totalRequests = self.networkTransactions.count;
|
||||
bytesReceived = self.dataSource.bytesReceived;
|
||||
totalRequests = self.dataSource.transactions.count;
|
||||
}
|
||||
|
||||
NSString *byteCountText = [NSByteCountFormatter
|
||||
@@ -253,7 +264,7 @@
|
||||
- (void)tryUpdateTransactions {
|
||||
// Don't do any view updating if we aren't in the view hierarchy
|
||||
if (!self.viewIfLoaded.window) {
|
||||
[self updateTransactions];
|
||||
[self updateTransactions:nil];
|
||||
self.pendingReload = YES;
|
||||
return;
|
||||
}
|
||||
@@ -261,57 +272,71 @@
|
||||
// Let the previous row insert animation finish before starting a new one to avoid stomping.
|
||||
// We'll try calling the method again when the insertion completes,
|
||||
// and we properly no-op if there haven't been changes.
|
||||
if (self.rowInsertInProgress) {
|
||||
if (self.updateInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.searchController.isActive) {
|
||||
[self updateTransactions];
|
||||
[self updateSearchResults:self.searchText];
|
||||
return;
|
||||
}
|
||||
self.updateInProgress = YES;
|
||||
|
||||
NSInteger existingRowCount = self.networkTransactions.count;
|
||||
[self updateTransactions];
|
||||
NSInteger newRowCount = self.networkTransactions.count;
|
||||
NSInteger addedRowCount = newRowCount - existingRowCount;
|
||||
|
||||
if (addedRowCount != 0 && !self.isPresentingSearch) {
|
||||
// Insert animation if we're at the top.
|
||||
if (self.tableView.contentOffset.y <= 0.0 && addedRowCount > 0) {
|
||||
[CATransaction begin];
|
||||
|
||||
self.rowInsertInProgress = YES;
|
||||
[CATransaction setCompletionBlock:^{
|
||||
self.rowInsertInProgress = NO;
|
||||
[self tryUpdateTransactions];
|
||||
}];
|
||||
|
||||
NSMutableArray<NSIndexPath *> *indexPathsToReload = [NSMutableArray new];
|
||||
for (NSInteger row = 0; row < addedRowCount; row++) {
|
||||
[indexPathsToReload addObject:[NSIndexPath indexPathForRow:row inSection:0]];
|
||||
}
|
||||
[self.tableView insertRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
|
||||
[CATransaction commit];
|
||||
} else {
|
||||
// Maintain the user's position if they've scrolled down.
|
||||
CGSize existingContentSize = self.tableView.contentSize;
|
||||
[self.tableView reloadData];
|
||||
CGFloat contentHeightChange = self.tableView.contentSize.height - existingContentSize.height;
|
||||
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, self.tableView.contentOffset.y + contentHeightChange);
|
||||
// Get state before update
|
||||
NSString *currentFilter = self.searchText;
|
||||
FLEXNetworkObserverMode currentMode = self.mode;
|
||||
NSInteger existingRowCount = self.dataSource.transactions.count;
|
||||
|
||||
[self updateTransactions:^{
|
||||
// Compare to state after update
|
||||
NSString *newFilter = self.searchText;
|
||||
FLEXNetworkObserverMode newMode = self.mode;
|
||||
NSInteger newRowCount = self.dataSource.transactions.count;
|
||||
NSInteger rowCountDiff = newRowCount - existingRowCount;
|
||||
|
||||
// Abort if the observation mode changed, or if the search field text changed
|
||||
if (newMode != currentMode || ![currentFilter isEqualToString:newFilter]) {
|
||||
self.updateInProgress = NO;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (rowCountDiff) {
|
||||
// Insert animation if we're at the top.
|
||||
if (self.tableView.contentOffset.y <= 0.0 && rowCountDiff > 0) {
|
||||
[CATransaction begin];
|
||||
|
||||
[CATransaction setCompletionBlock:^{
|
||||
self.updateInProgress = NO;
|
||||
// This isn't an infinite loop, it won't run a third time
|
||||
// if there were no new transactions the second time
|
||||
[self tryUpdateTransactions];
|
||||
}];
|
||||
|
||||
NSMutableArray<NSIndexPath *> *indexPathsToReload = [NSMutableArray new];
|
||||
for (NSInteger row = 0; row < rowCountDiff; row++) {
|
||||
[indexPathsToReload addObject:[NSIndexPath indexPathForRow:row inSection:0]];
|
||||
}
|
||||
|
||||
[self.tableView insertRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
[CATransaction commit];
|
||||
} else {
|
||||
// Maintain the user's position if they've scrolled down.
|
||||
CGSize existingContentSize = self.tableView.contentSize;
|
||||
[self.tableView reloadData];
|
||||
CGFloat contentHeightChange = self.tableView.contentSize.height - existingContentSize.height;
|
||||
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, self.tableView.contentOffset.y + contentHeightChange);
|
||||
self.updateInProgress = NO;
|
||||
}
|
||||
} else {
|
||||
self.updateInProgress = NO;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification {
|
||||
[self updateBytesReceived];
|
||||
[self updateFilteredBytesReceived];
|
||||
[self.HTTPDataSource reloadByteCounts];
|
||||
[self.websocketDataSource reloadByteCounts];
|
||||
|
||||
FLEXNetworkTransaction *transaction = notification.userInfo[kFLEXNetworkRecorderUserInfoTransactionKey];
|
||||
|
||||
// Update both the main table view and search table view if needed.
|
||||
for (FLEXNetworkTransactionCell *cell in [self.tableView visibleCells]) {
|
||||
for (FLEXNetworkTransactionCell *cell in self.tableView.visibleCells) {
|
||||
if ([cell.transaction isEqual:transaction]) {
|
||||
// Using -[UITableView reloadRowsAtIndexPaths:withRowAnimation:] is overkill here and kicks off a lot of
|
||||
// work that can make the table view somewhat unresponsive when lots of updates are streaming in.
|
||||
@@ -320,12 +345,14 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[self updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
- (void)handleTransactionsClearedNotification:(NSNotification *)notification {
|
||||
[self updateTransactions];
|
||||
[self.tableView reloadData];
|
||||
[self updateTransactions:^{
|
||||
[self.tableView reloadData];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleNetworkObserverEnabledStateChangedNotification:(NSNotification *)notification {
|
||||
@@ -337,7 +364,7 @@
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.searchController.isActive ? self.filteredNetworkTransactions.count : self.networkTransactions.count;
|
||||
return self.dataSource.transactions.count;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
@@ -352,7 +379,11 @@
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXNetworkTransactionCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXNetworkTransactionCellIdentifier forIndexPath:indexPath];
|
||||
FLEXNetworkTransactionCell *cell = [tableView
|
||||
dequeueReusableCellWithIdentifier:FLEXNetworkTransactionCell.reuseID
|
||||
forIndexPath:indexPath
|
||||
];
|
||||
|
||||
cell.transaction = [self transactionAtIndexPath:indexPath];
|
||||
|
||||
// Since we insert from the top, assign background colors bottom up to keep them consistent for each transaction.
|
||||
@@ -367,9 +398,33 @@
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXNetworkTransactionDetailController *detailViewController = [FLEXNetworkTransactionDetailController new];
|
||||
detailViewController.transaction = [self transactionAtIndexPath:indexPath];
|
||||
[self.navigationController pushViewController:detailViewController animated:YES];
|
||||
switch (self.mode) {
|
||||
case FLEXNetworkObserverModeREST: {
|
||||
FLEXHTTPTransaction *transaction = [self HTTPTransactionAtIndexPath:indexPath];
|
||||
UIViewController *details = [FLEXHTTPTransactionDetailController withTransaction:transaction];
|
||||
[self.navigationController pushViewController:details animated:YES];
|
||||
break;
|
||||
}
|
||||
|
||||
case FLEXNetworkObserverModeWebsockets: {
|
||||
if (@available(iOS 13.0, *)) { // This check will never fail
|
||||
FLEXWebsocketTransaction *transaction = [self websocketTransactionAtIndexPath:indexPath];
|
||||
|
||||
UIViewController *details = nil;
|
||||
if (transaction.message.type == NSURLSessionWebSocketMessageTypeData) {
|
||||
details = [FLEXObjectExplorerFactory explorerViewControllerForObject:transaction.message.data];
|
||||
} else {
|
||||
details = [[FLEXWebViewController alloc] initWithText:transaction.message.string];
|
||||
}
|
||||
|
||||
[self.navigationController pushViewController:details animated:YES];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -385,80 +440,83 @@
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
|
||||
if (action == @selector(copy:)) {
|
||||
NSURLRequest *request = [self transactionAtIndexPath:indexPath].request;
|
||||
UIPasteboard.generalPasteboard.string = request.URL.absoluteString ?: @"";
|
||||
UIPasteboard.generalPasteboard.string = [self transactionAtIndexPath:indexPath].copyString;
|
||||
}
|
||||
}
|
||||
|
||||
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
|
||||
NSURLRequest *request = [self transactionAtIndexPath:indexPath].request;
|
||||
|
||||
FLEXNetworkTransaction *transaction = [self transactionAtIndexPath:indexPath];
|
||||
|
||||
return [UIContextMenuConfiguration
|
||||
configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
|
||||
UIAction *copy = [UIAction
|
||||
actionWithTitle:@"Copy"
|
||||
actionWithTitle:@"Copy URL"
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
UIPasteboard.generalPasteboard.string = request.URL.absoluteString ?: @"";
|
||||
}
|
||||
];
|
||||
UIAction *denylist = [UIAction
|
||||
actionWithTitle:[NSString stringWithFormat:@"Exclude '%@'", request.URL.host]
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
NSMutableArray *denylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist;
|
||||
[denylist addObject:request.URL.host];
|
||||
[FLEXNetworkRecorder.defaultRecorder clearExcludedTransactions];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
|
||||
[self tryUpdateTransactions];
|
||||
UIPasteboard.generalPasteboard.string = transaction.copyString;
|
||||
}
|
||||
];
|
||||
|
||||
NSArray *children = @[copy];
|
||||
if (self.mode == FLEXNetworkObserverModeREST) {
|
||||
NSURLRequest *request = [self HTTPTransactionAtIndexPath:indexPath].request;
|
||||
UIAction *denylist = [UIAction
|
||||
actionWithTitle:[NSString stringWithFormat:@"Exclude '%@'", request.URL.host]
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
NSMutableArray *denylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist;
|
||||
[denylist addObject:request.URL.host];
|
||||
[FLEXNetworkRecorder.defaultRecorder clearExcludedTransactions];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
|
||||
[self tryUpdateTransactions];
|
||||
}
|
||||
];
|
||||
|
||||
children = [children arrayByAddingObject:denylist];
|
||||
}
|
||||
return [UIMenu
|
||||
menuWithTitle:@"" image:nil identifier:nil
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[copy, denylist]
|
||||
children:children
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.searchController.isActive ? self.filteredNetworkTransactions[indexPath.row] : self.networkTransactions[indexPath.row];
|
||||
return self.dataSource.transactions[indexPath.row];
|
||||
}
|
||||
|
||||
- (FLEXHTTPTransaction *)HTTPTransactionAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.HTTPDataSource.transactions[indexPath.row];
|
||||
}
|
||||
|
||||
- (FLEXWebsocketTransaction *)websocketTransactionAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.websocketDataSource.transactions[indexPath.row];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
- (void)updateSearchResults:(NSString *)searchString {
|
||||
if (!searchString.length) {
|
||||
self.filteredNetworkTransactions = self.networkTransactions;
|
||||
[self.tableView reloadData];
|
||||
} else {
|
||||
[self onBackgroundQueue:^NSArray *{
|
||||
return [self.networkTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *entry, NSUInteger idx) {
|
||||
return [entry.request.URL.absoluteString localizedCaseInsensitiveContainsString:searchString];
|
||||
}];
|
||||
} thenOnMainQueue:^(NSArray *filteredNetworkTransactions) {
|
||||
if ([self.searchText isEqual:searchString]) {
|
||||
self.filteredNetworkTransactions = filteredNetworkTransactions;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
}];
|
||||
}
|
||||
id callback = ^(FLEXMITMDataSource *dataSource) {
|
||||
if (self.dataSource == dataSource) {
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
};
|
||||
|
||||
[self.HTTPDataSource filter:searchString completion:callback];
|
||||
[self.websocketDataSource filter:searchString completion:callback];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController {
|
||||
self.isPresentingSearch = YES;
|
||||
}
|
||||
|
||||
- (void)didPresentSearchController:(UISearchController *)searchController {
|
||||
self.isPresentingSearch = NO;
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
|
||||
[self updateFirstSectionHeader];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController {
|
||||
|
||||
@@ -14,7 +14,7 @@ extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification;
|
||||
extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey;
|
||||
extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
|
||||
|
||||
@class FLEXNetworkTransaction;
|
||||
@class FLEXNetworkTransaction, FLEXHTTPTransaction, FLEXWebsocketTransaction;
|
||||
|
||||
@interface FLEXNetworkRecorder : NSObject
|
||||
|
||||
@@ -37,19 +37,21 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
|
||||
- (void)synchronizeDenylist;
|
||||
|
||||
|
||||
// Accessing recorded network activity
|
||||
#pragma mark Accessing recorded network activity
|
||||
|
||||
/// Array of FLEXNetworkTransaction objects ordered by start time with the newest first.
|
||||
- (NSArray<FLEXNetworkTransaction *> *)networkTransactions;
|
||||
/// Array of FLEXHTTPTransaction objects ordered by start time with the newest first.
|
||||
@property (nonatomic, readonly) NSArray<FLEXHTTPTransaction *> *HTTPTransactions;
|
||||
/// Array of FLEXWebsocketTransaction objects ordered by start time with the newest first.
|
||||
@property (nonatomic, readonly) NSArray<FLEXWebsocketTransaction *> *websocketTransactions API_AVAILABLE(ios(13.0));
|
||||
|
||||
/// The full response data IFF it hasn't been purged due to memory pressure.
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction;
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction;
|
||||
|
||||
/// Dumps all network transactions and cached response bodies.
|
||||
- (void)clearRecordedActivity;
|
||||
|
||||
|
||||
// Recording network activity
|
||||
#pragma mark Recording network activity
|
||||
|
||||
/// Call when app is about to send HTTP request.
|
||||
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID
|
||||
@@ -72,4 +74,12 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
|
||||
/// This string can be set to anything useful about the API used to make the request.
|
||||
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID;
|
||||
|
||||
- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0));
|
||||
- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error API_AVAILABLE(ios(13.0));
|
||||
|
||||
- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0));
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "OSCache.h"
|
||||
|
||||
NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
|
||||
NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
|
||||
@@ -22,9 +23,10 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
@interface FLEXNetworkRecorder ()
|
||||
|
||||
@property (nonatomic) NSCache *responseCache;
|
||||
@property (nonatomic) NSMutableArray<FLEXNetworkTransaction *> *orderedTransactions;
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, FLEXNetworkTransaction *> *requestIDsToTransactions;
|
||||
@property (nonatomic) OSCache *restCache;
|
||||
@property (nonatomic) NSMutableArray<FLEXHTTPTransaction *> *orderedHTTPTransactions;
|
||||
@property (nonatomic) NSMutableArray<FLEXWebsocketTransaction *> *orderedWSTransactions;
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, FLEXHTTPTransaction *> *requestIDsToHTTPTransactions;
|
||||
@property (nonatomic) dispatch_queue_t queue;
|
||||
|
||||
@end
|
||||
@@ -34,17 +36,18 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.responseCache = [NSCache new];
|
||||
self.restCache = [OSCache new];
|
||||
NSUInteger responseCacheLimit = [[NSUserDefaults.standardUserDefaults
|
||||
objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue
|
||||
];
|
||||
|
||||
// Default to 25 MB max. The cache will purge earlier if there is memory pressure.
|
||||
self.responseCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024;
|
||||
[self.responseCache setTotalCostLimit:responseCacheLimit];
|
||||
self.restCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024;
|
||||
[self.restCache setTotalCostLimit:responseCacheLimit];
|
||||
|
||||
self.orderedTransactions = [NSMutableArray new];
|
||||
self.requestIDsToTransactions = [NSMutableDictionary new];
|
||||
self.orderedWSTransactions = [NSMutableArray new];
|
||||
self.orderedHTTPTransactions = [NSMutableArray new];
|
||||
self.requestIDsToHTTPTransactions = [NSMutableDictionary new];
|
||||
self.hostDenylist = NSUserDefaults.standardUserDefaults.flex_networkHostDenylist.mutableCopy;
|
||||
|
||||
// Serial queue used because we use mutable objects that are not thread safe
|
||||
@@ -67,34 +70,35 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
#pragma mark - Public Data Access
|
||||
|
||||
- (NSUInteger)responseCacheByteLimit {
|
||||
return self.responseCache.totalCostLimit;
|
||||
return self.restCache.totalCostLimit;
|
||||
}
|
||||
|
||||
- (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit {
|
||||
self.responseCache.totalCostLimit = responseCacheByteLimit;
|
||||
self.restCache.totalCostLimit = responseCacheByteLimit;
|
||||
[NSUserDefaults.standardUserDefaults
|
||||
setObject:@(responseCacheByteLimit)
|
||||
forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXNetworkTransaction *> *)networkTransactions {
|
||||
__block NSArray<FLEXNetworkTransaction *> *transactions = nil;
|
||||
dispatch_sync(self.queue, ^{
|
||||
transactions = self.orderedTransactions.copy;
|
||||
});
|
||||
return transactions;
|
||||
- (NSArray<FLEXHTTPTransaction *> *)HTTPTransactions {
|
||||
return self.orderedHTTPTransactions.copy;
|
||||
}
|
||||
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
return [self.responseCache objectForKey:transaction.requestID];
|
||||
- (NSArray<FLEXWebsocketTransaction *> *)websocketTransactions {
|
||||
return self.orderedWSTransactions.copy;
|
||||
}
|
||||
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
return [self.restCache objectForKey:transaction.requestID];
|
||||
}
|
||||
|
||||
- (void)clearRecordedActivity {
|
||||
dispatch_async(self.queue, ^{
|
||||
[self.responseCache removeAllObjects];
|
||||
[self.orderedTransactions removeAllObjects];
|
||||
[self.requestIDsToTransactions removeAllObjects];
|
||||
[self.restCache removeAllObjects];
|
||||
[self.orderedWSTransactions removeAllObjects];
|
||||
[self.orderedHTTPTransactions removeAllObjects];
|
||||
[self.requestIDsToHTTPTransactions removeAllObjects];
|
||||
|
||||
[self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil];
|
||||
});
|
||||
@@ -102,8 +106,8 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
- (void)clearExcludedTransactions {
|
||||
dispatch_sync(self.queue, ^{
|
||||
self.orderedTransactions = ({
|
||||
[self.orderedTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *ta, NSUInteger idx) {
|
||||
self.orderedHTTPTransactions = ({
|
||||
[self.orderedHTTPTransactions flex_filtered:^BOOL(FLEXHTTPTransaction *ta, NSUInteger idx) {
|
||||
NSString *host = ta.request.URL.host;
|
||||
for (NSString *excluded in self.hostDenylist) {
|
||||
if ([host hasSuffix:excluded]) {
|
||||
@@ -132,22 +136,17 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
}
|
||||
}
|
||||
|
||||
// Before async block to stay accurate
|
||||
NSDate *startDate = [NSDate date];
|
||||
FLEXHTTPTransaction *transaction = [FLEXHTTPTransaction request:request identifier:requestID];
|
||||
|
||||
// Before async block to keep times accurate
|
||||
if (redirectResponse) {
|
||||
[self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
|
||||
[self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
|
||||
}
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = [FLEXNetworkTransaction new];
|
||||
transaction.requestID = requestID;
|
||||
transaction.request = request;
|
||||
transaction.startTime = startDate;
|
||||
|
||||
[self.orderedTransactions insertObject:transaction atIndex:0];
|
||||
[self.requestIDsToTransactions setObject:transaction forKey:requestID];
|
||||
[self.orderedHTTPTransactions insertObject:transaction atIndex:0];
|
||||
[self.requestIDsToHTTPTransactions setObject:transaction forKey:requestID];
|
||||
transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse;
|
||||
|
||||
[self postNewTransactionNotificationWithTransaction:transaction];
|
||||
@@ -159,7 +158,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
NSDate *responseDate = [NSDate date];
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -174,7 +173,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +187,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
NSDate *finishedDate = [NSDate date];
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -205,7 +204,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
}
|
||||
|
||||
if (shouldCache) {
|
||||
[self.responseCache setObject:responseBody forKey:requestID cost:responseBody.length];
|
||||
[self.restCache setObject:responseBody forKey:requestID cost:responseBody.length];
|
||||
}
|
||||
|
||||
NSString *mimeType = transaction.response.MIMEType;
|
||||
@@ -213,32 +212,32 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
// Thumbnail image previews on a separate background queue
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSInteger maxPixelDimension = UIScreen.mainScreen.scale * 32.0;
|
||||
transaction.responseThumbnail = [FLEXUtility
|
||||
transaction.thumbnail = [FLEXUtility
|
||||
thumbnailedImageWithMaxPixelDimension:maxPixelDimension
|
||||
fromImageData:responseBody
|
||||
];
|
||||
[self postUpdateNotificationForTransaction:transaction];
|
||||
});
|
||||
} else if ([mimeType isEqual:@"application/json"]) {
|
||||
transaction.responseThumbnail = FLEXResources.jsonIcon;
|
||||
transaction.thumbnail = FLEXResources.jsonIcon;
|
||||
} else if ([mimeType isEqual:@"text/plain"]){
|
||||
transaction.responseThumbnail = FLEXResources.textPlainIcon;
|
||||
transaction.thumbnail = FLEXResources.textPlainIcon;
|
||||
} else if ([mimeType isEqual:@"text/html"]) {
|
||||
transaction.responseThumbnail = FLEXResources.htmlIcon;
|
||||
transaction.thumbnail = FLEXResources.htmlIcon;
|
||||
} else if ([mimeType isEqual:@"application/x-plist"]) {
|
||||
transaction.responseThumbnail = FLEXResources.plistIcon;
|
||||
transaction.thumbnail = FLEXResources.plistIcon;
|
||||
} else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) {
|
||||
transaction.responseThumbnail = FLEXResources.binaryIcon;
|
||||
transaction.thumbnail = FLEXResources.binaryIcon;
|
||||
} else if ([mimeType containsString:@"javascript"]) {
|
||||
transaction.responseThumbnail = FLEXResources.jsIcon;
|
||||
transaction.thumbnail = FLEXResources.jsIcon;
|
||||
} else if ([mimeType containsString:@"xml"]) {
|
||||
transaction.responseThumbnail = FLEXResources.xmlIcon;
|
||||
transaction.thumbnail = FLEXResources.xmlIcon;
|
||||
} else if ([mimeType hasPrefix:@"audio"]) {
|
||||
transaction.responseThumbnail = FLEXResources.audioIcon;
|
||||
transaction.thumbnail = FLEXResources.audioIcon;
|
||||
} else if ([mimeType hasPrefix:@"video"]) {
|
||||
transaction.responseThumbnail = FLEXResources.videoIcon;
|
||||
transaction.thumbnail = FLEXResources.videoIcon;
|
||||
} else if ([mimeType hasPrefix:@"text"]) {
|
||||
transaction.responseThumbnail = FLEXResources.textIcon;
|
||||
transaction.thumbnail = FLEXResources.textIcon;
|
||||
}
|
||||
|
||||
[self postUpdateNotificationForTransaction:transaction];
|
||||
@@ -247,7 +246,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -262,7 +261,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -272,6 +271,42 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Websocket Events
|
||||
|
||||
- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXWebsocketTransaction *send = [FLEXWebsocketTransaction
|
||||
withMessage:message task:task direction:FLEXWebsocketOutgoing
|
||||
];
|
||||
|
||||
[self.orderedWSTransactions addObject:send];
|
||||
[self postNewTransactionNotificationWithTransaction:send];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message error:(NSError *)error {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXWebsocketTransaction *send = [self.orderedWSTransactions flex_firstWhere:^BOOL(FLEXWebsocketTransaction *t) {
|
||||
return t.message == message;
|
||||
}];
|
||||
send.error = error;
|
||||
send.transactionState = error ? FLEXNetworkTransactionStateFailed : FLEXNetworkTransactionStateFinished;
|
||||
|
||||
[self postUpdateNotificationForTransaction:send];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXWebsocketTransaction *receive = [FLEXWebsocketTransaction
|
||||
withMessage:message task:task direction:FLEXWebsocketIncoming
|
||||
];
|
||||
|
||||
[self.orderedWSTransactions addObject:receive];
|
||||
[self postNewTransactionNotificationWithTransaction:receive];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark Notification Posting
|
||||
|
||||
- (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
|
||||
@@ -17,28 +17,91 @@ typedef NS_ENUM(NSInteger, FLEXNetworkTransactionState) {
|
||||
FLEXNetworkTransactionStateFailed
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
|
||||
FLEXWebsocketIncoming = 1,
|
||||
FLEXWebsocketOutgoing,
|
||||
};
|
||||
|
||||
/// The shared base class for all types of network transactions.
|
||||
/// Subclasses should implement the descriptions and details properties, and assign a thumbnail.
|
||||
@interface FLEXNetworkTransaction : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *requestID;
|
||||
+ (instancetype)withStartTime:(NSDate *)startTime;
|
||||
|
||||
@property (nonatomic) NSURLRequest *request;
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
|
||||
|
||||
@property (nonatomic) NSError *error;
|
||||
/// Subclasses can override to provide error state based on response data as well
|
||||
@property (nonatomic, readonly) BOOL displayAsError;
|
||||
@property (nonatomic, readonly) NSDate *startTime;
|
||||
|
||||
@property (nonatomic) FLEXNetworkTransactionState transactionState;
|
||||
@property (nonatomic) int64_t receivedDataLength;
|
||||
/// A small thumbnail to preview the type of/the response
|
||||
@property (nonatomic) UIImage *thumbnail;
|
||||
|
||||
/// The most prominent line of the cell. Typically a URL endpoint or other distinguishing attribute.
|
||||
/// This line turns red when the transaction indicates an error.
|
||||
@property (nonatomic, readonly) NSString *primaryDescription;
|
||||
/// Something less important, such as a blob of data or the URL's domain.
|
||||
@property (nonatomic, readonly) NSString *secondaryDescription;
|
||||
/// Minor details to display at the bottom of the cell, such as a timestamp, HTTP method, or status.
|
||||
@property (nonatomic, readonly) NSString *tertiaryDescription;
|
||||
|
||||
/// Subclasses should implement for when the transaction is complete
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *details;
|
||||
|
||||
/// The string to copy when the user selects the "copy" action
|
||||
@property (nonatomic, readonly) NSString *copyString;
|
||||
|
||||
/// Whether or not this request should show up when the user searches for a given string
|
||||
- (BOOL)matchesQuery:(NSString *)filterString;
|
||||
|
||||
@end
|
||||
|
||||
/// The shared base class for all NSURL-API-related transactions.
|
||||
/// Descriptions are generated by this class using the URL provided by subclasses.
|
||||
@interface FLEXURLTransaction : FLEXNetworkTransaction
|
||||
|
||||
+ (instancetype)withRequest:(NSURLRequest *)request startTime:(NSDate *)startTime;
|
||||
|
||||
@property (nonatomic, readonly) NSURLRequest *request;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXHTTPTransaction : FLEXURLTransaction
|
||||
|
||||
+ (instancetype)request:(NSURLRequest *)request identifier:(NSString *)requestID;
|
||||
|
||||
@property (nonatomic, readonly) NSString *requestID;
|
||||
@property (nonatomic) NSURLResponse *response;
|
||||
@property (nonatomic, copy) NSString *requestMechanism;
|
||||
@property (nonatomic) FLEXNetworkTransactionState transactionState;
|
||||
@property (nonatomic) NSError *error;
|
||||
|
||||
@property (nonatomic) NSDate *startTime;
|
||||
@property (nonatomic) NSTimeInterval latency;
|
||||
@property (nonatomic) NSTimeInterval duration;
|
||||
|
||||
@property (nonatomic) int64_t receivedDataLength;
|
||||
|
||||
/// Only applicable for image downloads. A small thumbnail to preview the full response.
|
||||
@property (nonatomic) UIImage *responseThumbnail;
|
||||
|
||||
/// Populated lazily. Handles both normal HTTPBody data and HTTPBodyStreams.
|
||||
@property (nonatomic, readonly) NSData *cachedRequestBody;
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXWebsocketTransaction : FLEXURLTransaction
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction API_AVAILABLE(ios(13.0));
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction
|
||||
startTime:(NSDate *)started API_AVAILABLE(ios(13.0));
|
||||
|
||||
//@property (nonatomic, readonly) NSURLSessionWebSocketTask *task;
|
||||
@property (nonatomic, readonly) NSURLSessionWebSocketMessage *message API_AVAILABLE(ios(13.0));
|
||||
@property (nonatomic, readonly) FLEXWebsocketMessageDirection direction API_AVAILABLE(ios(13.0));
|
||||
|
||||
@property (nonatomic, readonly) int64_t dataLength API_AVAILABLE(ios(13.0));
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,23 +7,191 @@
|
||||
//
|
||||
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXNetworkTransaction ()
|
||||
|
||||
@property (nonatomic, readwrite) NSData *cachedRequestBody;
|
||||
@interface FLEXNetworkTransaction () {
|
||||
@protected
|
||||
|
||||
NSString *_primaryDescription;
|
||||
NSString *_secondaryDescription;
|
||||
NSString *_tertiaryDescription;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkTransaction
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state {
|
||||
NSString *readableString = nil;
|
||||
switch (state) {
|
||||
case FLEXNetworkTransactionStateUnstarted:
|
||||
readableString = @"Unstarted";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateAwaitingResponse:
|
||||
readableString = @"Awaiting Response";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateReceivingData:
|
||||
readableString = @"Receiving Data";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFinished:
|
||||
readableString = @"Finished";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFailed:
|
||||
readableString = @"Failed";
|
||||
break;
|
||||
}
|
||||
return readableString;
|
||||
}
|
||||
|
||||
+ (instancetype)withStartTime:(NSDate *)startTime {
|
||||
FLEXNetworkTransaction *transaction = [self new];
|
||||
transaction->_startTime = startTime;
|
||||
return transaction;
|
||||
}
|
||||
|
||||
- (BOOL)displayAsError {
|
||||
return _error != nil;
|
||||
}
|
||||
|
||||
- (NSString *)copyString {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)matchesQuery:(NSString *)filterString {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXURLTransaction ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXURLTransaction
|
||||
|
||||
+ (instancetype)withRequest:(NSURLRequest *)request startTime:(NSDate *)startTime {
|
||||
FLEXURLTransaction *transaction = [self withStartTime:startTime];
|
||||
transaction->_request = request;
|
||||
return transaction;
|
||||
}
|
||||
|
||||
- (NSString *)timestampStringFromRequestDate:(NSDate *)date {
|
||||
static NSDateFormatter *dateFormatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [NSDateFormatter new];
|
||||
dateFormatter.dateFormat = @"HH:mm:ss";
|
||||
});
|
||||
|
||||
return [dateFormatter stringFromDate:date];
|
||||
}
|
||||
|
||||
- (NSString *)primaryDescription {
|
||||
if (!_primaryDescription) {
|
||||
NSString *name = self.request.URL.lastPathComponent;
|
||||
if (!name.length) {
|
||||
name = @"/";
|
||||
}
|
||||
|
||||
if (_request.URL.query) {
|
||||
name = [name stringByAppendingFormat:@"?%@", self.request.URL.query];
|
||||
}
|
||||
|
||||
_primaryDescription = name;
|
||||
}
|
||||
|
||||
return _primaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)secondaryDescription {
|
||||
if (!_secondaryDescription) {
|
||||
NSMutableArray<NSString *> *mutablePathComponents = self.request.URL.pathComponents.mutableCopy;
|
||||
if (mutablePathComponents.count > 0) {
|
||||
[mutablePathComponents removeLastObject];
|
||||
}
|
||||
|
||||
NSString *path = self.request.URL.host;
|
||||
for (NSString *pathComponent in mutablePathComponents) {
|
||||
path = [path stringByAppendingPathComponent:pathComponent];
|
||||
}
|
||||
|
||||
_secondaryDescription = path;
|
||||
}
|
||||
|
||||
return _secondaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)tertiaryDescription {
|
||||
if (!_tertiaryDescription) {
|
||||
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
|
||||
|
||||
NSString *timestamp = [self timestampStringFromRequestDate:self.startTime];
|
||||
if (timestamp.length > 0) {
|
||||
[detailComponents addObject:timestamp];
|
||||
}
|
||||
|
||||
// Omit method for GET (assumed as default)
|
||||
NSString *httpMethod = self.request.HTTPMethod;
|
||||
if (httpMethod.length > 0) {
|
||||
[detailComponents addObject:httpMethod];
|
||||
}
|
||||
|
||||
if (self.transactionState == FLEXNetworkTransactionStateFinished || self.transactionState == FLEXNetworkTransactionStateFailed) {
|
||||
[detailComponents addObjectsFromArray:self.details];
|
||||
} else {
|
||||
// Unstarted, Awaiting Response, Receiving Data, etc.
|
||||
NSString *state = [self.class readableStringFromTransactionState:self.transactionState];
|
||||
[detailComponents addObject:state];
|
||||
}
|
||||
|
||||
_tertiaryDescription = [detailComponents componentsJoinedByString:@" ・ "];
|
||||
}
|
||||
|
||||
return _tertiaryDescription;
|
||||
}
|
||||
|
||||
- (void)setTransactionState:(FLEXNetworkTransactionState)transactionState {
|
||||
super.transactionState = transactionState;
|
||||
// Reset bottom description
|
||||
_tertiaryDescription = nil;
|
||||
}
|
||||
|
||||
- (NSString *)copyString {
|
||||
return self.request.URL.absoluteString;
|
||||
}
|
||||
|
||||
- (BOOL)matchesQuery:(NSString *)filterString {
|
||||
return [self.request.URL.absoluteString localizedCaseInsensitiveContainsString:filterString];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXHTTPTransaction ()
|
||||
@property (nonatomic, readwrite) NSData *cachedRequestBody;
|
||||
@end
|
||||
|
||||
@implementation FLEXHTTPTransaction
|
||||
|
||||
+ (instancetype)request:(NSURLRequest *)request identifier:(NSString *)requestID {
|
||||
FLEXHTTPTransaction *httpt = [self withRequest:request startTime:NSDate.date];
|
||||
httpt->_requestID = requestID;
|
||||
return httpt;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
NSString *description = [super description];
|
||||
|
||||
|
||||
description = [description stringByAppendingFormat:@" id = %@;", self.requestID];
|
||||
description = [description stringByAppendingFormat:@" url = %@;", self.request.URL];
|
||||
description = [description stringByAppendingFormat:@" duration = %f;", self.duration];
|
||||
description = [description stringByAppendingFormat:@" receivedDataLength = %lld", self.receivedDataLength];
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@@ -49,30 +217,89 @@
|
||||
return _cachedRequestBody;
|
||||
}
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state {
|
||||
NSString *readableString = nil;
|
||||
switch (state) {
|
||||
case FLEXNetworkTransactionStateUnstarted:
|
||||
readableString = @"Unstarted";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateAwaitingResponse:
|
||||
readableString = @"Awaiting Response";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateReceivingData:
|
||||
readableString = @"Receiving Data";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFinished:
|
||||
readableString = @"Finished";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFailed:
|
||||
readableString = @"Failed";
|
||||
break;
|
||||
- (NSArray *)detailString {
|
||||
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
|
||||
|
||||
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:self.response];
|
||||
if (statusCodeString.length > 0) {
|
||||
[detailComponents addObject:statusCodeString];
|
||||
}
|
||||
return readableString;
|
||||
|
||||
if (self.receivedDataLength > 0) {
|
||||
NSString *responseSize = [NSByteCountFormatter
|
||||
stringFromByteCount:self.receivedDataLength
|
||||
countStyle:NSByteCountFormatterCountStyleBinary
|
||||
];
|
||||
[detailComponents addObject:responseSize];
|
||||
}
|
||||
|
||||
NSString *totalDuration = [FLEXUtility stringFromRequestDuration:self.duration];
|
||||
NSString *latency = [FLEXUtility stringFromRequestDuration:self.latency];
|
||||
NSString *duration = [NSString stringWithFormat:@"%@ (%@)", totalDuration, latency];
|
||||
[detailComponents addObject:duration];
|
||||
|
||||
return detailComponents;
|
||||
}
|
||||
|
||||
- (BOOL)displayAsError {
|
||||
return [FLEXUtility isErrorStatusCodeFromURLResponse:self.response] || super.displayAsError;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXWebsocketTransaction
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction
|
||||
startTime:(NSDate *)started {
|
||||
FLEXWebsocketTransaction *wst = [self withRequest:task.originalRequest startTime:started];
|
||||
wst->_message = message;
|
||||
wst->_direction = direction;
|
||||
|
||||
// Populate receivedDataLength
|
||||
if (direction == FLEXWebsocketIncoming) {
|
||||
wst.receivedDataLength = wst.dataLength;
|
||||
wst.transactionState = FLEXNetworkTransactionStateFinished;
|
||||
}
|
||||
|
||||
// Populate thumbnail image
|
||||
if (message.type == NSURLSessionWebSocketMessageTypeData) {
|
||||
wst.thumbnail = FLEXResources.binaryIcon;
|
||||
} else {
|
||||
wst.thumbnail = FLEXResources.textIcon;
|
||||
}
|
||||
|
||||
return wst;
|
||||
}
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction {
|
||||
return [self withMessage:message task:task direction:direction startTime:NSDate.date];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)details API_AVAILABLE(ios(13.0)) {
|
||||
return @[
|
||||
self.direction == FLEXWebsocketOutgoing ? @"SENT →" : @"→ RECEIVED",
|
||||
[NSByteCountFormatter
|
||||
stringFromByteCount:self.dataLength
|
||||
countStyle:NSByteCountFormatterCountStyleBinary
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
- (int64_t)dataLength {
|
||||
if (self.message) {
|
||||
if (self.message.type == NSURLSessionWebSocketMessageTypeString) {
|
||||
return self.message.string.length;
|
||||
}
|
||||
|
||||
return self.message.data.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,14 +8,13 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
extern NSString * const kFLEXNetworkTransactionCellIdentifier;
|
||||
|
||||
@class FLEXNetworkTransaction;
|
||||
|
||||
@interface FLEXNetworkTransactionCell : UITableViewCell
|
||||
|
||||
@property (nonatomic) FLEXNetworkTransaction *transaction;
|
||||
|
||||
+ (CGFloat)preferredCellHeight;
|
||||
@property (nonatomic, readonly, class) NSString *reuseID;
|
||||
@property (nonatomic, readonly, class) CGFloat preferredCellHeight;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXResources.h"
|
||||
|
||||
NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
|
||||
NSString * const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
|
||||
|
||||
@interface FLEXNetworkTransactionCell ()
|
||||
|
||||
@@ -69,7 +69,7 @@ NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactio
|
||||
|
||||
CGFloat thumbnailOriginY = round((self.contentView.bounds.size.height - kImageDimension) / 2.0);
|
||||
self.thumbnailImageView.frame = CGRectMake(kLeftPadding, thumbnailOriginY, kImageDimension, kImageDimension);
|
||||
self.thumbnailImageView.image = self.transaction.responseThumbnail;
|
||||
self.thumbnailImageView.image = self.transaction.thumbnail;
|
||||
|
||||
CGFloat textOriginX = CGRectGetMaxX(self.thumbnailImageView.frame) + kLeftPadding;
|
||||
CGFloat availableTextWidth = self.contentView.bounds.size.width - textOriginX;
|
||||
@@ -77,7 +77,7 @@ NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactio
|
||||
self.nameLabel.text = [self nameLabelText];
|
||||
CGSize nameLabelPreferredSize = [self.nameLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
|
||||
self.nameLabel.frame = CGRectMake(textOriginX, kVerticalPadding, availableTextWidth, nameLabelPreferredSize.height);
|
||||
self.nameLabel.textColor = (self.transaction.error || [FLEXUtility isErrorStatusCodeFromURLResponse:self.transaction.response]) ? UIColor.redColor : FLEXColor.primaryTextColor;
|
||||
self.nameLabel.textColor = self.transaction.displayAsError ? UIColor.redColor : FLEXColor.primaryTextColor;
|
||||
|
||||
self.pathLabel.text = [self pathLabelText];
|
||||
CGSize pathLabelPreferredSize = [self.pathLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
|
||||
@@ -93,81 +93,23 @@ NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactio
|
||||
}
|
||||
|
||||
- (NSString *)nameLabelText {
|
||||
NSURL *url = self.transaction.request.URL;
|
||||
NSString *name = [url lastPathComponent];
|
||||
if (name.length == 0) {
|
||||
name = @"/";
|
||||
}
|
||||
NSString *query = [url query];
|
||||
if (query) {
|
||||
name = [name stringByAppendingFormat:@"?%@", query];
|
||||
}
|
||||
return name;
|
||||
return self.transaction.primaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)pathLabelText {
|
||||
NSURL *url = self.transaction.request.URL;
|
||||
NSMutableArray<NSString *> *mutablePathComponents = url.pathComponents.mutableCopy;
|
||||
if (mutablePathComponents.count > 0) {
|
||||
[mutablePathComponents removeLastObject];
|
||||
}
|
||||
NSString *path = [url host];
|
||||
for (NSString *pathComponent in mutablePathComponents) {
|
||||
path = [path stringByAppendingPathComponent:pathComponent];
|
||||
}
|
||||
return path;
|
||||
return self.transaction.secondaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)transactionDetailsLabelText {
|
||||
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
|
||||
|
||||
NSString *timestamp = [[self class] timestampStringFromRequestDate:self.transaction.startTime];
|
||||
if (timestamp.length > 0) {
|
||||
[detailComponents addObject:timestamp];
|
||||
}
|
||||
|
||||
// Omit method for GET (assumed as default)
|
||||
NSString *httpMethod = self.transaction.request.HTTPMethod;
|
||||
if (httpMethod.length > 0) {
|
||||
[detailComponents addObject:httpMethod];
|
||||
}
|
||||
|
||||
if (self.transaction.transactionState == FLEXNetworkTransactionStateFinished || self.transaction.transactionState == FLEXNetworkTransactionStateFailed) {
|
||||
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:self.transaction.response];
|
||||
if (statusCodeString.length > 0) {
|
||||
[detailComponents addObject:statusCodeString];
|
||||
}
|
||||
|
||||
if (self.transaction.receivedDataLength > 0) {
|
||||
NSString *responseSize = [NSByteCountFormatter stringFromByteCount:self.transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
|
||||
[detailComponents addObject:responseSize];
|
||||
}
|
||||
|
||||
NSString *totalDuration = [FLEXUtility stringFromRequestDuration:self.transaction.duration];
|
||||
NSString *latency = [FLEXUtility stringFromRequestDuration:self.transaction.latency];
|
||||
NSString *duration = [NSString stringWithFormat:@"%@ (%@)", totalDuration, latency];
|
||||
[detailComponents addObject:duration];
|
||||
} else {
|
||||
// Unstarted, Awaiting Response, Receiving Data, etc.
|
||||
NSString *state = [FLEXNetworkTransaction readableStringFromTransactionState:self.transaction.transactionState];
|
||||
[detailComponents addObject:state];
|
||||
}
|
||||
|
||||
return [detailComponents componentsJoinedByString:@" ・ "];
|
||||
}
|
||||
|
||||
+ (NSString *)timestampStringFromRequestDate:(NSDate *)date {
|
||||
static NSDateFormatter *dateFormatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [NSDateFormatter new];
|
||||
dateFormatter.dateFormat = @"HH:mm:ss";
|
||||
});
|
||||
return [dateFormatter stringFromDate:date];
|
||||
return self.transaction.tertiaryDescription;
|
||||
}
|
||||
|
||||
+ (CGFloat)preferredCellHeight {
|
||||
return 65.0;
|
||||
}
|
||||
|
||||
+ (NSString *)reuseID {
|
||||
return kFLEXNetworkTransactionCellIdentifier;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// FLEXNetworkTransactionDetailController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/10/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXNetworkTransaction;
|
||||
|
||||
@interface FLEXNetworkTransactionDetailController : UITableViewController
|
||||
|
||||
@property (nonatomic) FLEXNetworkTransaction *transaction;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,20 @@
|
||||
OSCache
|
||||
version 1.2.1, Decembet 18th, 2015
|
||||
|
||||
Copyright (C) 2014 Charcoal Design
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// OSCache.h
|
||||
//
|
||||
// Version 1.2.1
|
||||
//
|
||||
// Created by Nick Lockwood on 01/01/2014.
|
||||
// Copyright (C) 2014 Charcoal Design
|
||||
//
|
||||
// Distributed under the permissive zlib License
|
||||
// Get the latest version from here:
|
||||
//
|
||||
// https://github.com/nicklockwood/OSCache
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgment in the product documentation would be
|
||||
// appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such, and must not be
|
||||
// misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source distribution.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OSCache <KeyType, ObjectType> : NSCache <NSFastEnumeration>
|
||||
|
||||
@property (nonatomic, readonly) NSUInteger count;
|
||||
@property (nonatomic, readonly) NSUInteger totalCost;
|
||||
|
||||
- (id)objectForKeyedSubscript:(KeyType <NSCopying>)key;
|
||||
- (void)setObject:(ObjectType)obj forKeyedSubscript:(KeyType <NSCopying>)key;
|
||||
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ObjectType obj, BOOL *stop))block;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol OSCacheDelegate <NSCacheDelegate>
|
||||
@optional
|
||||
|
||||
- (BOOL)cache:(OSCache *)cache shouldEvictObject:(id)entry;
|
||||
- (void)cache:(OSCache *)cache willEvictObject:(id)entry;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,409 @@
|
||||
//
|
||||
// OSCache.m
|
||||
//
|
||||
// Version 1.2.1
|
||||
//
|
||||
// Created by Nick Lockwood on 01/01/2014.
|
||||
// Copyright (C) 2014 Charcoal Design
|
||||
//
|
||||
// Distributed under the permissive zlib License
|
||||
// Get the latest version from here:
|
||||
//
|
||||
// https://github.com/nicklockwood/OSCache
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgment in the product documentation would be
|
||||
// appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such, and must not be
|
||||
// misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source distribution.
|
||||
//
|
||||
|
||||
#import "OSCache.h"
|
||||
#import <TargetConditionals.h>
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
#endif
|
||||
|
||||
|
||||
#import <Availability.h>
|
||||
#if !__has_feature(objc_arc)
|
||||
#error This class requires automatic reference counting
|
||||
#endif
|
||||
|
||||
|
||||
#pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis"
|
||||
#pragma GCC diagnostic ignored "-Wdirect-ivar-access"
|
||||
#pragma GCC diagnostic ignored "-Wgnu"
|
||||
|
||||
|
||||
@interface OSCacheEntry : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSObject *object;
|
||||
@property (nonatomic, assign) NSUInteger cost;
|
||||
@property (nonatomic, assign) NSInteger sequenceNumber;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation OSCacheEntry
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface OSCache_Private : NSObject
|
||||
|
||||
@property (nonatomic, unsafe_unretained) id<OSCacheDelegate> delegate;
|
||||
@property (nonatomic, assign) NSUInteger countLimit;
|
||||
@property (nonatomic, assign) NSUInteger totalCostLimit;
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary *cache;
|
||||
@property (nonatomic, assign) NSUInteger totalCost;
|
||||
@property (nonatomic, assign) NSInteger sequenceNumber;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation OSCache_Private
|
||||
{
|
||||
BOOL _delegateRespondsToWillEvictObject;
|
||||
BOOL _delegateRespondsToShouldEvictObject;
|
||||
BOOL _currentlyCleaning;
|
||||
NSMutableArray *_entryPool;
|
||||
NSLock *_lock;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
//create storage
|
||||
_cache = [[NSMutableDictionary alloc] init];
|
||||
_entryPool = [[NSMutableArray alloc] init];
|
||||
_lock = [[NSLock alloc] init];
|
||||
_totalCost = 0;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
//clean up in the event of a memory warning
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cleanUpAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setDelegate:(id<OSCacheDelegate>)delegate
|
||||
{
|
||||
_delegate = delegate;
|
||||
_delegateRespondsToShouldEvictObject = [delegate respondsToSelector:@selector(cache:shouldEvictObject:)];
|
||||
_delegateRespondsToWillEvictObject = [delegate respondsToSelector:@selector(cache:willEvictObject:)];
|
||||
}
|
||||
|
||||
- (void)setCountLimit:(NSUInteger)countLimit
|
||||
{
|
||||
[_lock lock];
|
||||
_countLimit = countLimit;
|
||||
[_lock unlock];
|
||||
[self cleanUp:NO];
|
||||
}
|
||||
|
||||
- (void)setTotalCostLimit:(NSUInteger)totalCostLimit
|
||||
{
|
||||
[_lock lock];
|
||||
_totalCostLimit = totalCostLimit;
|
||||
[_lock unlock];
|
||||
[self cleanUp:NO];
|
||||
}
|
||||
|
||||
- (NSUInteger)count
|
||||
{
|
||||
return [_cache count];
|
||||
}
|
||||
|
||||
- (void)cleanUp:(BOOL)keepEntries
|
||||
{
|
||||
[_lock lock];
|
||||
NSUInteger maxCount = _countLimit ?: INT_MAX;
|
||||
NSUInteger maxCost = _totalCostLimit ?: INT_MAX;
|
||||
NSUInteger totalCount = _cache.count;
|
||||
NSMutableArray *keys = [_cache.allKeys mutableCopy];
|
||||
while (totalCount > maxCount || _totalCost > maxCost)
|
||||
{
|
||||
NSInteger lowestSequenceNumber = INT_MAX;
|
||||
OSCacheEntry *lowestEntry = nil;
|
||||
id lowestKey = nil;
|
||||
|
||||
//remove oldest items until within limit
|
||||
for (id key in keys)
|
||||
{
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (entry.sequenceNumber < lowestSequenceNumber)
|
||||
{
|
||||
lowestSequenceNumber = entry.sequenceNumber;
|
||||
lowestEntry = entry;
|
||||
lowestKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestKey)
|
||||
{
|
||||
[keys removeObject:lowestKey];
|
||||
if (!_delegateRespondsToShouldEvictObject ||
|
||||
[_delegate cache:(OSCache *)self shouldEvictObject:lowestEntry.object])
|
||||
{
|
||||
if (_delegateRespondsToWillEvictObject)
|
||||
{
|
||||
_currentlyCleaning = YES;
|
||||
[self.delegate cache:(OSCache *)self willEvictObject:lowestEntry.object];
|
||||
_currentlyCleaning = NO;
|
||||
}
|
||||
[_cache removeObjectForKey:lowestKey];
|
||||
_totalCost -= lowestEntry.cost;
|
||||
totalCount --;
|
||||
if (keepEntries)
|
||||
{
|
||||
[_entryPool addObject:lowestEntry];
|
||||
lowestEntry.object = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)cleanUpAllObjects
|
||||
{
|
||||
[_lock lock];
|
||||
if (_delegateRespondsToShouldEvictObject || _delegateRespondsToWillEvictObject)
|
||||
{
|
||||
NSArray *keys = [_cache allKeys];
|
||||
if (_delegateRespondsToShouldEvictObject)
|
||||
{
|
||||
//sort, oldest first (in case we want to use that information in our eviction test)
|
||||
keys = [keys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) {
|
||||
OSCacheEntry *entry1 = self->_cache[key1];
|
||||
OSCacheEntry *entry2 = self->_cache[key2];
|
||||
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
|
||||
}];
|
||||
}
|
||||
|
||||
//remove all items individually
|
||||
for (id key in keys)
|
||||
{
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (!_delegateRespondsToShouldEvictObject || [_delegate cache:(OSCache *)self shouldEvictObject:entry.object])
|
||||
{
|
||||
if (_delegateRespondsToWillEvictObject)
|
||||
{
|
||||
_currentlyCleaning = YES;
|
||||
[_delegate cache:(OSCache *)self willEvictObject:entry.object];
|
||||
_currentlyCleaning = NO;
|
||||
}
|
||||
[_cache removeObjectForKey:key];
|
||||
_totalCost -= entry.cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_totalCost = 0;
|
||||
[_cache removeAllObjects];
|
||||
_sequenceNumber = 0;
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)resequence
|
||||
{
|
||||
//sort, oldest first
|
||||
NSArray *entries = [[_cache allValues] sortedArrayUsingComparator:^NSComparisonResult(OSCacheEntry *entry1, OSCacheEntry *entry2) {
|
||||
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
|
||||
}];
|
||||
|
||||
//renumber items
|
||||
NSInteger index = 0;
|
||||
for (OSCacheEntry *entry in entries)
|
||||
{
|
||||
entry.sequenceNumber = index++;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)objectForKey:(id)key
|
||||
{
|
||||
[_lock lock];
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
entry.sequenceNumber = _sequenceNumber++;
|
||||
if (_sequenceNumber < 0)
|
||||
{
|
||||
[self resequence];
|
||||
}
|
||||
id object = entry.object;
|
||||
[_lock unlock];
|
||||
return object;
|
||||
}
|
||||
|
||||
- (id)objectForKeyedSubscript:(id<NSCopying>)key
|
||||
{
|
||||
return [self objectForKey:key];
|
||||
}
|
||||
|
||||
- (void)setObject:(id)obj forKey:(id)key
|
||||
{
|
||||
[self setObject:obj forKey:key cost:0];
|
||||
}
|
||||
|
||||
- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key
|
||||
{
|
||||
[self setObject:obj forKey:key cost:0];
|
||||
}
|
||||
|
||||
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g
|
||||
{
|
||||
if (!obj)
|
||||
{
|
||||
[self removeObjectForKey:key];
|
||||
return;
|
||||
}
|
||||
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
|
||||
[_lock lock];
|
||||
_totalCost -= [_cache[key] cost];
|
||||
_totalCost += g;
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (!entry) {
|
||||
entry = [[OSCacheEntry alloc] init];
|
||||
_cache[key] = entry;
|
||||
}
|
||||
entry.object = obj;
|
||||
entry.cost = g;
|
||||
entry.sequenceNumber = _sequenceNumber++;
|
||||
if (_sequenceNumber < 0)
|
||||
{
|
||||
[self resequence];
|
||||
}
|
||||
[_lock unlock];
|
||||
[self cleanUp:YES];
|
||||
}
|
||||
|
||||
- (void)removeObjectForKey:(id)key
|
||||
{
|
||||
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
|
||||
[_lock lock];
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (entry) {
|
||||
_totalCost -= entry.cost;
|
||||
entry.object = nil;
|
||||
[_entryPool addObject:entry];
|
||||
[_cache removeObjectForKey:key];
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)removeAllObjects
|
||||
{
|
||||
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
|
||||
[_lock lock];
|
||||
_totalCost = 0;
|
||||
_sequenceNumber = 0;
|
||||
for (OSCacheEntry *entry in _cache.allValues)
|
||||
{
|
||||
entry.object = nil;
|
||||
[_entryPool addObject:entry];
|
||||
}
|
||||
[_cache removeAllObjects];
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
|
||||
objects:(id __unsafe_unretained [])buffer
|
||||
count:(NSUInteger)len
|
||||
{
|
||||
[_lock lock];
|
||||
NSUInteger count = [_cache countByEnumeratingWithState:state objects:buffer count:len];
|
||||
[_lock unlock];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block
|
||||
{
|
||||
if (block)
|
||||
{
|
||||
[_lock lock];
|
||||
[_cache enumerateKeysAndObjectsUsingBlock:^(id key, OSCacheEntry *entry, BOOL *stop) {
|
||||
block(key, entry.object, stop);
|
||||
}];
|
||||
[_lock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
//handle unimplemented methods
|
||||
|
||||
- (BOOL)isKindOfClass:(Class)aClass
|
||||
{
|
||||
//pretend that we're an NSCache if anyone asks
|
||||
if (aClass == [OSCache class] || aClass == [NSCache class])
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
return [super isKindOfClass:aClass];
|
||||
}
|
||||
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
|
||||
{
|
||||
//protect against calls to unimplemented NSCache methods
|
||||
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
|
||||
if (!signature)
|
||||
{
|
||||
signature = [NSCache instanceMethodSignatureForSelector:selector];
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnonnull"
|
||||
|
||||
[invocation invokeWithTarget:nil];
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation OSCache
|
||||
|
||||
+ (instancetype)allocWithZone:(struct _NSZone *)zone
|
||||
{
|
||||
return (OSCache *)[OSCache_Private allocWithZone:zone];
|
||||
}
|
||||
|
||||
- (id)objectForKeyedSubscript:(__unused id<NSCopying>)key { return nil; }
|
||||
- (void)setObject:(__unused id)obj forKeyedSubscript:(__unused id<NSCopying>)key {}
|
||||
- (void)enumerateKeysAndObjectsUsingBlock:(__unused void (^)(id, id, BOOL *))block { }
|
||||
- (NSUInteger)countByEnumeratingWithState:(__unused NSFastEnumerationState *)state
|
||||
objects:(__unused __unsafe_unretained id [])buffer
|
||||
count:(__unused NSUInteger)len { return 0; }
|
||||
|
||||
@end
|
||||
@@ -11,10 +11,14 @@
|
||||
// See the LICENSE file distributed with this work for the terms under
|
||||
// which Square, Inc. licenses this file to you.
|
||||
//
|
||||
// Heavily modified and added to by Tanner Bennett and various other contributors.
|
||||
// git blame details these modifications.
|
||||
//
|
||||
|
||||
#import "FLEXNetworkObserver.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "FLEXMethod.h"
|
||||
|
||||
@@ -24,7 +28,6 @@
|
||||
#include <dlfcn.h>
|
||||
|
||||
NSString *const kFLEXNetworkObserverEnabledStateChangedNotification = @"kFLEXNetworkObserverEnabledStateChangedNotification";
|
||||
static NSString *const kFLEXNetworkObserverEnabledDefaultsKey = @"com.flex.FLEXNetworkObserver.enableOnLaunch";
|
||||
|
||||
typedef void (^NSURLSessionAsyncCompletion)(id fileURLOrData, NSURLResponse *response, NSError *error);
|
||||
typedef NSURLSessionTask * (^NSURLSessionNewTaskMethod)(NSURLSession *, id, NSURLSessionAsyncCompletion);
|
||||
@@ -68,6 +71,15 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
|
||||
- (void)URLSessionTaskWillResume:(NSURLSessionTask *)task;
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
sendMessagage:(NSURLSessionWebSocketMessage *)message API_AVAILABLE(ios(13.0));
|
||||
- (void)websocketTaskMessageSendCompletion:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error API_AVAILABLE(ios(13.0));
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
receiveMessagage:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error API_AVAILABLE(ios(13.0));
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXNetworkObserver ()
|
||||
@@ -84,12 +96,12 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
+ (void)setEnabled:(BOOL)enabled {
|
||||
BOOL previouslyEnabled = [self isEnabled];
|
||||
|
||||
[NSUserDefaults.standardUserDefaults setBool:enabled forKey:kFLEXNetworkObserverEnabledDefaultsKey];
|
||||
NSUserDefaults.standardUserDefaults.flex_networkObserverEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
// Inject if needed. This injection is protected with a dispatch_once, so we're ok calling it multiple times.
|
||||
// By doing the injection lazily, we keep the impact of the tool lower when this feature isn't enabled.
|
||||
[self injectIntoAllNSURLConnectionDelegateClasses];
|
||||
[self injectIntoAllNSURLThings];
|
||||
}
|
||||
|
||||
if (previouslyEnabled != enabled) {
|
||||
@@ -98,14 +110,14 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
}
|
||||
|
||||
+ (BOOL)isEnabled {
|
||||
return [[NSUserDefaults.standardUserDefaults objectForKey:kFLEXNetworkObserverEnabledDefaultsKey] boolValue];
|
||||
return NSUserDefaults.standardUserDefaults.flex_networkObserverEnabled;
|
||||
}
|
||||
|
||||
+ (void)load {
|
||||
// We don't want to do the swizzling from +load because not all the classes may be loaded at this point.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([self isEnabled]) {
|
||||
[self injectIntoAllNSURLConnectionDelegateClasses];
|
||||
[self injectIntoAllNSURLThings];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -128,9 +140,11 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
#pragma mark Delegate Injection Convenience Methods
|
||||
|
||||
/// All swizzled delegate methods should make use of this guard.
|
||||
/// This will prevent duplicated sniffing when the original implementation calls up to a superclass implementation which we've also swizzled.
|
||||
/// The superclass implementation (and implementations in classes above that) will be executed without interference if called from the original implementation.
|
||||
+ (void)sniffWithoutDuplicationForObject:(NSObject *)object selector:(SEL)selector sniffingBlock:(void (^)(void))sniffingBlock originalImplementationBlock:(void (^)(void))originalImplementationBlock {
|
||||
/// This will prevent duplicated sniffing when the original implementation calls up to a superclass
|
||||
/// implementation which we've also swizzled. The superclass implementation (and implementations in
|
||||
/// classes above that) will be executed without interference if called from the original implementation.
|
||||
+ (void)sniffWithoutDuplicationForObject:(NSObject *)object selector:(SEL)selector
|
||||
sniffingBlock:(void (^)(void))sniffingBlock originalImplementationBlock:(void (^)(void))originalImplementationBlock {
|
||||
// If we don't have an object to detect nested calls on, just run the original implementation and bail.
|
||||
// This case can happen if someone besides the URL loading system calls the delegate methods directly.
|
||||
// See https://github.com/Flipboard/FLEX/issues/61 for an example.
|
||||
@@ -154,7 +168,7 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
|
||||
#pragma mark - Delegate Injection
|
||||
|
||||
+ (void)injectIntoAllNSURLConnectionDelegateClasses {
|
||||
+ (void)injectIntoAllNSURLThings {
|
||||
// Only allow swizzling once.
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -224,6 +238,15 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
|
||||
[self injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods];
|
||||
[self injectIntoNSURLSessionAsyncUploadTaskMethods];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
Class websocketTask = NSClassFromString(@"__NSURLSessionWebSocketTask");
|
||||
[self injectWebsocketSendMessage:websocketTask];
|
||||
[self injectWebsocketReceiveMessage:websocketTask];
|
||||
websocketTask = [NSURLSessionWebSocketTask class];
|
||||
[self injectWebsocketSendMessage:websocketTask];
|
||||
[self injectWebsocketReceiveMessage:websocketTask];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1266,7 +1289,76 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
implementationBlock:implementationBlock
|
||||
undefinedBlock:undefinedBlock
|
||||
];
|
||||
}
|
||||
|
||||
+ (void)injectWebsocketSendMessage:(Class)cls API_AVAILABLE(ios(13.0)) {
|
||||
SEL selector = @selector(sendMessage:completionHandler:);
|
||||
SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector];
|
||||
|
||||
typedef void (^SendMessageBlock)(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
NSURLSessionWebSocketMessage *message,
|
||||
void (^completion)(NSError *error)
|
||||
);
|
||||
|
||||
SendMessageBlock implementationBlock = ^(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
NSURLSessionWebSocketMessage *message,
|
||||
void (^completion)(NSError *error)
|
||||
) {
|
||||
[FLEXNetworkObserver.sharedObserver
|
||||
websocketTask:slf sendMessagage:message
|
||||
];
|
||||
completion = ^(NSError *error) {
|
||||
[FLEXNetworkObserver.sharedObserver
|
||||
websocketTaskMessageSendCompletion:message
|
||||
error:error
|
||||
];
|
||||
};
|
||||
|
||||
((void(*)(id, SEL, id, id))objc_msgSend)(
|
||||
slf, swizzledSelector, message, completion
|
||||
);
|
||||
};
|
||||
|
||||
[FLEXUtility replaceImplementationOfKnownSelector:selector
|
||||
onClass:cls
|
||||
withBlock:implementationBlock
|
||||
swizzledSelector:swizzledSelector
|
||||
];
|
||||
}
|
||||
|
||||
+ (void)injectWebsocketReceiveMessage:(Class)cls API_AVAILABLE(ios(13.0)) {
|
||||
SEL selector = @selector(receiveMessageWithCompletionHandler:);
|
||||
SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector];
|
||||
|
||||
typedef void (^SendMessageBlock)(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
void (^completion)(NSURLSessionWebSocketMessage *message, NSError *error)
|
||||
);
|
||||
|
||||
SendMessageBlock implementationBlock = ^(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
void (^completion)(NSURLSessionWebSocketMessage *message, NSError *error)
|
||||
) {
|
||||
id completionHook = ^(NSURLSessionWebSocketMessage *message, NSError *error) {
|
||||
[FLEXNetworkObserver.sharedObserver
|
||||
websocketTask:slf receiveMessagage:message error:error
|
||||
];
|
||||
completion(message, error);
|
||||
};
|
||||
|
||||
((void(*)(id, SEL, id))objc_msgSend)(
|
||||
slf, swizzledSelector, completionHook
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
[FLEXUtility replaceImplementationOfKnownSelector:selector
|
||||
onClass:cls
|
||||
withBlock:implementationBlock
|
||||
swizzledSelector:swizzledSelector
|
||||
];
|
||||
}
|
||||
|
||||
static char const * const kFLEXRequestIDKey = "kFLEXRequestIDKey";
|
||||
@@ -1589,4 +1681,35 @@ didFinishDownloadingToURL:(NSURL *)location data:(NSData *)data
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
sendMessagage:(NSURLSessionWebSocketMessage *)message {
|
||||
[self performBlock:^{
|
||||
// NSString *requestID = [[self class] requestIDForConnectionOrTask:task];
|
||||
[FLEXNetworkRecorder.defaultRecorder recordWebsocketMessageSend:message task:task];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)websocketTaskMessageSendCompletion:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error {
|
||||
[self performBlock:^{
|
||||
[FLEXNetworkRecorder.defaultRecorder
|
||||
recordWebsocketMessageSendCompletion:message
|
||||
error:error
|
||||
];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
receiveMessagage:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error {
|
||||
[self performBlock:^{
|
||||
if (!error && message) {
|
||||
[FLEXNetworkRecorder.defaultRecorder
|
||||
recordWebsocketMessageReceived:message
|
||||
task:task
|
||||
];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
referencesSection.selectionAction = ^(UIViewController *host) {
|
||||
UIViewController *references = [FLEXObjectListViewController
|
||||
objectsWithReferencesToObject:explorer.object
|
||||
retained:NO
|
||||
];
|
||||
[host.navigationController pushViewController:references animated:YES];
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
viewer:^UIViewController *(id obj) {
|
||||
return [FLEXObjectListViewController
|
||||
instancesOfClassWithName:NSStringFromClass(obj)
|
||||
retained:NO
|
||||
];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id obj) {
|
||||
|
||||
@@ -29,3 +29,5 @@
|
||||
@interface FLEXShortcutsFactory (WebKit_Safari) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Pasteboard) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (FirebaseFirestore) @end
|
||||
|
||||
@@ -441,3 +441,16 @@
|
||||
}];
|
||||
}
|
||||
@end
|
||||
|
||||
#pragma mark - Firebase Firestore
|
||||
|
||||
@implementation FLEXShortcutsFactory (FirebaseFirestore)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
Class FIRDocumentSnap = NSClassFromString(@"FIRDocumentSnapshot");
|
||||
if (FIRDocumentSnap) {
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, data, FIRDocumentSnap, NSDictionary, PropertyKey(ReadOnly));
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -154,7 +154,10 @@ FLEXObjectExplorerDefaultsImpl
|
||||
if (value) {
|
||||
NSString *title = @"List all references";
|
||||
[actions addObject:[UIAction actionWithTitle:title image:nil identifier:nil handler:^(UIAction *action) {
|
||||
UIViewController *list = [FLEXObjectListViewController objectsWithReferencesToObject:value];
|
||||
UIViewController *list = [FLEXObjectListViewController
|
||||
objectsWithReferencesToObject:value
|
||||
retained:NO
|
||||
];
|
||||
[sender.navigationController pushViewController:list animated:YES];
|
||||
}]];
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
- (instancetype)flex_sortedUsingSelector:(SEL)selector;
|
||||
|
||||
- (T)flex_firstWhere:(BOOL(^)(T obj))meetingCriteria;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSMutableArray<T> (Functional)
|
||||
|
||||
@@ -113,6 +113,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (id)flex_firstWhere:(BOOL (^)(id))meetsCriteria {
|
||||
for (id e in self) {
|
||||
if (meetsCriteria(e)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ extern NSString * const kFLEXDefaultsHidePropertyMethodsKey;
|
||||
extern NSString * const kFLEXDefaultsHidePrivateMethodsKey;
|
||||
extern NSString * const kFLEXDefaultsShowMethodOverridesKey;
|
||||
extern NSString * const kFLEXDefaultsHideVariablePreviewsKey;
|
||||
extern NSString * const kFLEXDefaultsNetworkObserverEnabledKey;
|
||||
extern NSString * const kFLEXDefaultsNetworkHostDenylistKey;
|
||||
extern NSString * const kFLEXDefaultsDisableOSLogForceASLKey;
|
||||
extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
|
||||
@@ -27,6 +28,7 @@ extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
|
||||
|
||||
@property (nonatomic) double flex_toolbarTopMargin;
|
||||
|
||||
@property (nonatomic) BOOL flex_networkObserverEnabled;
|
||||
// Not actually stored in defaults, but written to a file
|
||||
@property (nonatomic) NSArray<NSString *> *flex_networkHostDenylist;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ NSString * const kFLEXDefaultsHidePropertyMethodsKey = @"com.flipboard.FLEX.hide
|
||||
NSString * const kFLEXDefaultsHidePrivateMethodsKey = @"com.flipboard.FLEX.hide_private_or_namespaced_methods";
|
||||
NSString * const kFLEXDefaultsShowMethodOverridesKey = @"com.flipboard.FLEX.show_method_overrides";
|
||||
NSString * const kFLEXDefaultsHideVariablePreviewsKey = @"com.flipboard.FLEX.hide_variable_previews";
|
||||
NSString * const kFLEXDefaultsNetworkObserverEnabledKey = @"com.flex.FLEXNetworkObserver.enableOnLaunch";
|
||||
NSString * const kFLEXDefaultsNetworkHostDenylistKey = @"com.flipboard.FLEX.network_host_denylist";
|
||||
NSString * const kFLEXDefaultsDisableOSLogForceASLKey = @"com.flipboard.FLEX.try_disable_os_log";
|
||||
NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.view_json_as_object";
|
||||
@@ -62,6 +63,14 @@ NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.vie
|
||||
[self setDouble:margin forKey:kFLEXDefaultsToolbarTopMarginKey];
|
||||
}
|
||||
|
||||
- (BOOL)flex_networkObserverEnabled {
|
||||
return [self boolForKey:kFLEXDefaultsNetworkObserverEnabledKey];
|
||||
}
|
||||
|
||||
- (void)setFlex_networkObserverEnabled:(BOOL)enabled {
|
||||
[self setBool:enabled forKey:kFLEXDefaultsNetworkObserverEnabledKey];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)flex_networkHostDenylist {
|
||||
return [NSArray arrayWithContentsOfFile:[
|
||||
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostDenylistKey
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#define FLEXBarButtonItem(title, tgt, sel) \
|
||||
[UIBarButtonItem flex_itemWithTitle:title target:tgt action:sel]
|
||||
#define FLEXBarButtonItemSystem(item, tgt, sel) \
|
||||
[UIBarButtonItem flex_systemItem:UIBarButtonSystemItem##item target:tgt action:sel]
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@class FLEXObjectRef;
|
||||
|
||||
typedef void (^flex_object_enumeration_block_t)(__unsafe_unretained id object, __unsafe_unretained Class actualClass);
|
||||
|
||||
@@ -14,4 +15,11 @@ typedef void (^flex_object_enumeration_block_t)(__unsafe_unretained id object, _
|
||||
|
||||
+ (void)enumerateLiveObjectsUsingBlock:(flex_object_enumeration_block_t)block;
|
||||
|
||||
/// Returned references are not validated beyond containing a valid isa.
|
||||
/// To validate them yourself, pass each reference's object to \c FLEXPointerIsValidObjcObject
|
||||
+ (NSArray<FLEXObjectRef *> *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain;
|
||||
+ (NSArray<FLEXObjectRef *> *)subclassesOfClassWithName:(NSString *)className;
|
||||
/// Returned references have been validated via \c FLEXPointerIsValidObjcObject
|
||||
+ (NSArray<FLEXObjectRef *> *)objectsWithReferencesToObject:(id)object retained:(BOOL)retain;
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
#import "FLEXHeapEnumerator.h"
|
||||
#import "FLEXObjcInternal.h"
|
||||
#import "FLEXObjectRef.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "NSString+FLEX.h"
|
||||
#import <malloc/malloc.h>
|
||||
#import <mach/mach.h>
|
||||
#import <objc/runtime.h>
|
||||
@@ -116,4 +119,79 @@ static kern_return_t reader(__unused task_t remote_task, vm_address_t remote_add
|
||||
free(classes);
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain {
|
||||
const char *classNameCString = className.UTF8String;
|
||||
NSMutableArray *instances = [NSMutableArray new];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
|
||||
if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
|
||||
// Note: objects of certain classes crash when retain is called.
|
||||
// It is up to the user to avoid tapping into instance lists for these classes.
|
||||
// Ex. OS_dispatch_queue_specific_queue
|
||||
// In the future, we could provide some kind of warning for classes that are known to be problematic.
|
||||
if (malloc_size((__bridge const void *)(object)) > 0) {
|
||||
[instances addObject:object];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances retained:retain];
|
||||
return references;
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)subclassesOfClassWithName:(NSString *)className {
|
||||
NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
|
||||
return references;
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)objectsWithReferencesToObject:(id)object retained:(BOOL)retain {
|
||||
static Class SwiftObjectClass = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
SwiftObjectClass = NSClassFromString(@"SwiftObject");
|
||||
if (!SwiftObjectClass) {
|
||||
SwiftObjectClass = NSClassFromString(@"Swift._SwiftObject");
|
||||
}
|
||||
});
|
||||
|
||||
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray new];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
|
||||
// Skip known-invalid objects
|
||||
if (!FLEXPointerIsValidObjcObject((__bridge void *)tryObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all the ivars on the object. Start with the class and and travel up the
|
||||
// inheritance chain. Once we find a match, record it and move on to the next object.
|
||||
// There's no reason to find multiple matches within the same object.
|
||||
Class tryClass = actualClass;
|
||||
while (tryClass) {
|
||||
unsigned int ivarCount = 0;
|
||||
Ivar *ivars = class_copyIvarList(tryClass, &ivarCount);
|
||||
|
||||
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
|
||||
Ivar ivar = ivars[ivarIndex];
|
||||
NSString *typeEncoding = @(ivar_getTypeEncoding(ivar) ?: "");
|
||||
|
||||
if (typeEncoding.flex_typeIsObjectOrClass) {
|
||||
ptrdiff_t offset = ivar_getOffset(ivar);
|
||||
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
|
||||
|
||||
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
|
||||
NSString *ivarName = @(ivar_getName(ivar) ?: "???");
|
||||
id ref = [FLEXObjectRef referencing:tryObject ivar:ivarName retained:retain];
|
||||
[instances addObject:ref];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(ivars);
|
||||
tryClass = class_getSuperclass(tryClass);
|
||||
}
|
||||
}];
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -196,7 +196,8 @@ BOOL FLEXConstructorsShouldRun() {
|
||||
? [UIColor colorWithPatternImage:indentationPatternImage]
|
||||
: [UIColor colorWithPatternImage:darkModePatternImage]);
|
||||
}];
|
||||
} });
|
||||
}
|
||||
});
|
||||
|
||||
return patternColor;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXObjcInternal.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
#import "FLEXMethod.h"
|
||||
|
||||
NSString * const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain";
|
||||
|
||||
@@ -332,15 +333,14 @@ NSString * const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:({
|
||||
Method method;
|
||||
if (object_isClass(object)) {
|
||||
method = class_getClassMethod(object, selector);
|
||||
} else {
|
||||
method = class_getInstanceMethod(object_getClass(object), selector);
|
||||
}
|
||||
method_getTypeEncoding(method);
|
||||
})];
|
||||
// It is important to use object_getClass and not -class here, as
|
||||
// object_getClass will return a different result for class objects
|
||||
Class cls = object_getClass(object);
|
||||
NSMethodSignature *methodSignature = [FLEXMethod selector:selector class:cls].signature;
|
||||
if (!methodSignature) {
|
||||
// Unsupported type encoding
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Probably an unsupported type encoding, like bitfields.
|
||||
// In the future, we could calculate the return length
|
||||
@@ -362,7 +362,7 @@ NSString * const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain
|
||||
[invocation retainArguments];
|
||||
|
||||
// Always self and _cmd
|
||||
NSUInteger numberOfArguments = [methodSignature numberOfArguments];
|
||||
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
|
||||
for (NSUInteger argumentIndex = kFLEXNumberOfImplicitArgs; argumentIndex < numberOfArguments; argumentIndex++) {
|
||||
NSUInteger argumentsArrayIndex = argumentIndex - kFLEXNumberOfImplicitArgs;
|
||||
id argumentObject = arguments.count > argumentsArrayIndex ? arguments[argumentsArrayIndex] : nil;
|
||||
|
||||
@@ -106,17 +106,18 @@ BOOL FLEXPointerIsReadable(const void *inPtr) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Read the memory
|
||||
vm_offset_t readMem = 0;
|
||||
mach_msg_type_number_t size = 0;
|
||||
#if __arm64e__
|
||||
address = (vm_address_t)ptrauth_strip(inPtr, ptrauth_key_function_pointer);
|
||||
#else
|
||||
address = (vm_address_t)inPtr;
|
||||
#endif
|
||||
error = vm_read(mach_task_self(), address, sizeof(uintptr_t), &readMem, &size);
|
||||
|
||||
// Read the memory
|
||||
vm_size_t size = 0;
|
||||
char buf[sizeof(uintptr_t)];
|
||||
error = vm_read_overwrite(mach_task_self(), address, sizeof(uintptr_t), (vm_address_t)buf, &size);
|
||||
if (error != KERN_SUCCESS) {
|
||||
// vm_read returned an error
|
||||
// vm_read_overwrite returned an error
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -162,22 +163,33 @@ BOOL FLEXPointerIsValidObjcObject(const void *ptr) {
|
||||
// We check if the returned class is readable because object_getClass
|
||||
// can return a garbage value when given a non-nil pointer to a non-object
|
||||
Class cls = object_getClass((__bridge id)ptr);
|
||||
if (cls && FLEXPointerIsReadable((__bridge void *)cls)) {
|
||||
// Just because this pointer is readable doesn't mean whatever is at
|
||||
// it's ISA offset is readable. We need to do the same checks on it's ISA.
|
||||
// Even this isn't perfect, because once we call object_isClass, we're
|
||||
// going to dereference a member of the metaclass, which may or may not
|
||||
// be readable itself. For the time being there is no way to access it
|
||||
// to check here, and I have yet to hard-code a solution.
|
||||
Class metaclass = object_getClass(cls);
|
||||
if (metaclass && FLEXPointerIsReadable((__bridge void *)metaclass)) {
|
||||
if (object_isClass(cls)) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
if (!cls || !FLEXPointerIsReadable((__bridge void *)cls)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Just because this pointer is readable doesn't mean whatever is at
|
||||
// it's ISA offset is readable. We need to do the same checks on it's ISA.
|
||||
// Even this isn't perfect, because once we call object_isClass, we're
|
||||
// going to dereference a member of the metaclass, which may or may not
|
||||
// be readable itself. For the time being there is no way to access it
|
||||
// to check here, and I have yet to hard-code a solution.
|
||||
Class metaclass = object_getClass(cls);
|
||||
if (!metaclass || !FLEXPointerIsReadable((__bridge void *)metaclass)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Does the class pointer we got appear as a class to the runtime?
|
||||
if (!object_isClass(cls)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Is the allocation size at least as large as the expected instance size?
|
||||
ssize_t instanceSize = class_getInstanceSize(cls);
|
||||
if (malloc_size(ptr) < instanceSize) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return NO;
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ typedef NS_ENUM(NSUInteger, FLEXPropertyAttribute) {
|
||||
FLEXPropertyAttributeReadOnly = 'R',
|
||||
FLEXPropertyAttributeRetain = '&',
|
||||
FLEXPropertyAttributeWeak = 'W'
|
||||
};
|
||||
}; //NS_SWIFT_NAME(FLEX.PropertyAttribute);
|
||||
|
||||
typedef NS_ENUM(char, FLEXTypeEncoding) {
|
||||
FLEXTypeEncodingNull = '\0',
|
||||
@@ -76,4 +76,4 @@ typedef NS_ENUM(char, FLEXTypeEncoding) {
|
||||
FLEXTypeEncodingBitField = 'b',
|
||||
FLEXTypeEncodingPointer = '^',
|
||||
FLEXTypeEncodingConst = 'r'
|
||||
};
|
||||
}; //NS_SWIFT_NAME(FLEX.TypeEncoding);
|
||||
|
||||
@@ -18,7 +18,7 @@ extern CFSetRef FLEXKnownUnsafeClasses;
|
||||
static Class cNSObject = nil, cNSProxy = nil;
|
||||
|
||||
__attribute__((constructor))
|
||||
static void FLEXInitKnownRootClasses() {
|
||||
static void FLEXInitKnownRootClasses(void) {
|
||||
cNSObject = [NSObject class];
|
||||
cNSProxy = [NSProxy class];
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ BOOL FLEXGetSizeAndAlignment(const char *type, NSUInteger *sizep, NSUInteger *al
|
||||
self.scan.scanLocation = scanLocation;
|
||||
|
||||
// The return / cleanup code for when the scanned type is already clean
|
||||
NSString * (^typeIsClean)() = ^NSString * {
|
||||
NSString * (^typeIsClean)(void) = ^NSString * {
|
||||
NSString *clean = [self.scan.string
|
||||
substringWithRange:NSMakeRange(scanLocation, self.scan.scanLocation - scanLocation)
|
||||
];
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#import "FLEXRuntimeConstants.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXIvar : NSObject
|
||||
|
||||
+ (instancetype)ivar:(Ivar)ivar;
|
||||
@@ -31,17 +33,19 @@
|
||||
@property (nonatomic, readonly) NSString *details;
|
||||
/// The full path of the image that contains this ivar definition,
|
||||
/// or \c nil if this ivar was probably defined at runtime.
|
||||
@property (nonatomic, readonly) NSString *imagePath;
|
||||
@property (nonatomic, readonly, nullable) NSString *imagePath;
|
||||
|
||||
/// For internal use
|
||||
@property (nonatomic) id tag;
|
||||
|
||||
- (id)getValue:(id)target;
|
||||
- (void)setValue:(id)value onObject:(id)target;
|
||||
- (nullable id)getValue:(id)target;
|
||||
- (void)setValue:(nullable id)value onObject:(id)target;
|
||||
|
||||
/// Calls into -getValue: and passes that value into
|
||||
/// -[FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:type:]
|
||||
/// and returns the result
|
||||
- (id)getPotentiallyUnboxedValue:(id)target;
|
||||
- (nullable id)getPotentiallyUnboxedValue:(id)target;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -24,14 +24,6 @@
|
||||
|
||||
#pragma mark Initializers
|
||||
|
||||
- (id)init {
|
||||
[NSException
|
||||
raise:NSInternalInconsistencyException
|
||||
format:@"Class instance should not be created with -init"
|
||||
];
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (instancetype)ivar:(Ivar)ivar {
|
||||
return [[self alloc] initWithIvar:ivar];
|
||||
}
|
||||
|
||||
@@ -8,25 +8,17 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@class FLEXMethod, FLEXProperty, FLEXIvar, FLEXProtocol;
|
||||
#import <objc/runtime.h>
|
||||
@class FLEXMethod, FLEXProperty, FLEXIvar, FLEXProtocol;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark FLEXMirror
|
||||
@interface FLEXMirror : NSObject
|
||||
#pragma mark FLEXMirror Protocol
|
||||
NS_SWIFT_NAME(FLEXMirrorProtocol)
|
||||
@protocol FLEXMirror <NSObject>
|
||||
|
||||
/// Reflects an instance of an object or \c Class.
|
||||
/// @discussion \c FLEXMirror will immediately gather all useful information. Consider using the
|
||||
/// \c NSObject categories provided if your code will only use a few pieces of information,
|
||||
/// or if your code needs to run faster.
|
||||
///
|
||||
/// If you reflect an instance of a class then \c methods and \c properties will be populated
|
||||
/// with instance methods and properties. If you reflect a class itself, then \c methods
|
||||
/// and \c properties will be populated with class methods and properties as you'd expect.
|
||||
///
|
||||
/// @param objectOrClass An instance of an objct or a \c Class object.
|
||||
/// @return An instance of \c FLEXMirror.
|
||||
+ (instancetype)reflect:(id)objectOrClass;
|
||||
/// Swift initializer
|
||||
- (instancetype)initWithSubject:(id)objectOrClass NS_SWIFT_NAME(init(reflecting:));
|
||||
|
||||
/// The underlying object or \c Class used to create this \c FLEXMirror instance.
|
||||
@property (nonatomic, readonly) id value;
|
||||
@@ -41,7 +33,36 @@
|
||||
@property (nonatomic, readonly) NSArray<FLEXProtocol *> *protocols;
|
||||
|
||||
/// @return A reflection of \c value.superClass.
|
||||
@property (nonatomic, readonly) FLEXMirror *superMirror;
|
||||
@property (nonatomic, readonly, nullable) id<FLEXMirror> superMirror NS_SWIFT_NAME(superMirror);
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark FLEXMirror Class
|
||||
@interface FLEXMirror : NSObject <FLEXMirror>
|
||||
|
||||
/// Reflects an instance of an object or \c Class.
|
||||
/// @discussion \c FLEXMirror will immediately gather all useful information. Consider using the
|
||||
/// \c NSObject categories provided if your code will only use a few pieces of information,
|
||||
/// or if your code needs to run faster.
|
||||
///
|
||||
/// If you reflect an instance of a class then \c methods and \c properties will be populated
|
||||
/// with instance methods and properties. If you reflect a class itself, then \c methods
|
||||
/// and \c properties will be populated with class methods and properties as you'd expect.
|
||||
///
|
||||
/// @param objectOrClass An instance of an objct or a \c Class object.
|
||||
/// @return An instance of \c FLEXMirror.
|
||||
+ (instancetype)reflect:(id)objectOrClass;
|
||||
|
||||
@property (nonatomic, readonly) id value;
|
||||
@property (nonatomic, readonly) BOOL isClass;
|
||||
@property (nonatomic, readonly) NSString *className;
|
||||
|
||||
@property (nonatomic, readonly) NSArray<FLEXProperty *> *properties;
|
||||
@property (nonatomic, readonly) NSArray<FLEXIvar *> *ivars;
|
||||
@property (nonatomic, readonly) NSArray<FLEXMethod *> *methods;
|
||||
@property (nonatomic, readonly) NSArray<FLEXProtocol *> *protocols;
|
||||
|
||||
@property (nonatomic, readonly, nullable) FLEXMirror *superMirror NS_SWIFT_NAME(superMirror);
|
||||
|
||||
@end
|
||||
|
||||
@@ -49,12 +70,14 @@
|
||||
@interface FLEXMirror (ExtendedMirror)
|
||||
|
||||
/// @return The method with the given name, or \c nil if one does not exist.
|
||||
- (FLEXMethod *)methodNamed:(NSString *)name;
|
||||
- (nullable FLEXMethod *)methodNamed:(nullable NSString *)name;
|
||||
/// @return The property with the given name, or \c nil if one does not exist.
|
||||
- (FLEXProperty *)propertyNamed:(NSString *)name;
|
||||
- (nullable FLEXProperty *)propertyNamed:(nullable NSString *)name;
|
||||
/// @return The instance variable with the given name, or \c nil if one does not exist.
|
||||
- (FLEXIvar *)ivarNamed:(NSString *)name;
|
||||
- (nullable FLEXIvar *)ivarNamed:(nullable NSString *)name;
|
||||
/// @return The protocol with the given name, or \c nil if one does not exist.
|
||||
- (FLEXProtocol *)protocolNamed:(NSString *)name;
|
||||
- (nullable FLEXProtocol *)protocolNamed:(nullable NSString *)name;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -29,15 +29,15 @@
|
||||
|
||||
#pragma mark Initialization
|
||||
+ (instancetype)reflect:(id)objectOrClass {
|
||||
return [[self alloc] initWithValue:objectOrClass];
|
||||
return [[self alloc] initWithSubject:objectOrClass];
|
||||
}
|
||||
|
||||
- (id)initWithValue:(id)value {
|
||||
NSParameterAssert(value);
|
||||
- (id)initWithSubject:(id)objectOrClass {
|
||||
NSParameterAssert(objectOrClass);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_value = value;
|
||||
_value = objectOrClass;
|
||||
[self examine];
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
// Return the given selector if the class responds to it
|
||||
Class cls = _cls;
|
||||
SEL (^selectorIfValid)() = ^SEL(SEL sel) {
|
||||
SEL (^selectorIfValid)(SEL) = ^SEL(SEL sel) {
|
||||
if (!sel || !cls) return nil;
|
||||
return [cls instancesRespondToSelector:sel] ? sel : nil;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#import "FLEXRuntimeConstants.h"
|
||||
@class FLEXProperty, FLEXMethodDescription;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark FLEXProtocol
|
||||
@interface FLEXProtocol : NSObject
|
||||
|
||||
@@ -23,22 +25,22 @@
|
||||
/// The name of the protocol.
|
||||
@property (nonatomic, readonly) NSString *name;
|
||||
/// The required methods of the protocol, if any. This includes property getters and setters.
|
||||
@property (nonatomic, readonly) NSArray<FLEXMethodDescription *> *requiredMethods;
|
||||
@property (nonatomic, readonly) NSArray<FLEXMethodDescription *> *requiredMethods;
|
||||
/// The optional methods of the protocol, if any. This includes property getters and setters.
|
||||
@property (nonatomic, readonly) NSArray<FLEXMethodDescription *> *optionalMethods;
|
||||
@property (nonatomic, readonly) NSArray<FLEXMethodDescription *> *optionalMethods;
|
||||
/// All protocols that this protocol conforms to, if any.
|
||||
@property (nonatomic, readonly) NSArray<FLEXProtocol *> *protocols;
|
||||
@property (nonatomic, readonly) NSArray<FLEXProtocol *> *protocols;
|
||||
/// The full path of the image that contains this protocol definition,
|
||||
/// or \c nil if this protocol was probably defined at runtime.
|
||||
@property (nonatomic, readonly) NSString *imagePath;
|
||||
@property (nonatomic, readonly, nullable) NSString *imagePath;
|
||||
|
||||
/// The properties in the protocol, if any. \c nil on iOS 10+
|
||||
@property (nonatomic, readonly) NSArray<FLEXProperty *> *properties API_DEPRECATED("Use the more specific accessors below", ios(2.0, 10.0));
|
||||
@property (nonatomic, readonly, nullable) NSArray<FLEXProperty *> *properties API_DEPRECATED("Use the more specific accessors below", ios(2.0, 10.0));
|
||||
|
||||
/// The required properties in the protocol, if any.
|
||||
@property (nonatomic, readonly) NSArray<FLEXProperty *> *requiredProperties API_AVAILABLE(ios(10.0));
|
||||
@property (nonatomic, readonly) NSArray<FLEXProperty *> *requiredProperties API_AVAILABLE(ios(10.0));
|
||||
/// The optional properties in the protocol, if any.
|
||||
@property (nonatomic, readonly) NSArray<FLEXProperty *> *optionalProperties API_AVAILABLE(ios(10.0));
|
||||
@property (nonatomic, readonly) NSArray<FLEXProperty *> *optionalProperties API_AVAILABLE(ios(10.0));
|
||||
|
||||
/// For internal use
|
||||
@property (nonatomic) id tag;
|
||||
@@ -67,3 +69,5 @@
|
||||
/// \c YES if this is an instance method, \c NO if it is a class method, or \c nil if unspecified
|
||||
@property (nonatomic, readonly) NSNumber *instance;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -15,14 +15,6 @@
|
||||
|
||||
@implementation FLEXProtocol
|
||||
|
||||
- (id)init {
|
||||
[NSException
|
||||
raise:NSInternalInconsistencyException
|
||||
format:@"Class instance should not be created with -init"
|
||||
];
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark Initializers
|
||||
|
||||
+ (NSArray *)allProtocols {
|
||||
@@ -160,6 +152,9 @@
|
||||
return [FLEXProperty property:objcproperties[i]];
|
||||
}];
|
||||
|
||||
_requiredProperties = @[];
|
||||
_optionalProperties = @[];
|
||||
|
||||
free(objcproperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
- (void)initSubviews {
|
||||
self.userInteractionEnabled = YES;
|
||||
UIImageView * (^newSubviewImageView)() = ^UIImageView *(UIImage *image) {
|
||||
UIImageView * (^newSubviewImageView)(UIImage *) = ^UIImageView *(UIImage *image) {
|
||||
UIImageView *iv = [UIImageView new];
|
||||
iv.image = image;
|
||||
// iv.userInteractionEnabled = YES;
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
|
||||
/// Recursively hides all views that may be obscuring the given view and collects them
|
||||
/// in the given array. You should unhide them all when you are done.
|
||||
+ (void)hideViewsCoveringView:(UIView *)view doWhileHidden:(void(^)())block {
|
||||
+ (void)hideViewsCoveringView:(UIView *)view doWhileHidden:(void(^)(void))block {
|
||||
NSMutableArray *viewsToUnhide = [NSMutableArray new];
|
||||
if ([self _hideViewsCoveringView:view root:view.window hiddenViews:viewsToUnhide]) {
|
||||
block();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MiscNetworkRequests : NSObject <NSURLConnectionDataDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
|
||||
@interface MiscNetworkRequests : NSObject <NSURLConnectionDataDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate, NSURLSessionWebSocketDelegate>
|
||||
|
||||
+ (void)sendExampleRequests;
|
||||
|
||||
|
||||
@@ -11,14 +11,21 @@
|
||||
@implementation MiscNetworkRequests
|
||||
|
||||
+ (void)sendExampleRequests {
|
||||
[[self new] sendExampleNetworkRequests];
|
||||
MiscNetworkRequests *misc = [self new];
|
||||
|
||||
NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
|
||||
config.timeoutIntervalForRequest = 10.0;
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:misc delegateQueue:nil];
|
||||
|
||||
[misc sendExampleNetworkRequests:session];
|
||||
[misc sendExampleWebsocketTraffic:session];
|
||||
}
|
||||
|
||||
- (NSMutableURLRequest *)request:(NSString *)url {
|
||||
return [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
|
||||
}
|
||||
|
||||
- (void)sendExampleNetworkRequests {
|
||||
- (void)sendExampleNetworkRequests:(NSURLSession *)sessionWithDelegate {
|
||||
NSString *kFlipboardIcon = @"https://cdn.flipboard.com/serviceIcons/v2/social-icon-flipboard-96.png";
|
||||
NSString *kRandomAnimal = @"https://lorempixel.com/248/250/animals/";
|
||||
NSString *kSnowLeopard = @"https://lorempixel.com/248/250/animals/4/";
|
||||
@@ -35,13 +42,10 @@
|
||||
|
||||
// With delegate //
|
||||
|
||||
NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
|
||||
config.timeoutIntervalForRequest = 10.0;
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
|
||||
// NSURLSessionDataTask
|
||||
[pendingTasks addObject:[session dataTaskWithURL:[NSURL URLWithString:kFlipboardIcon]]];
|
||||
[pendingTasks addObject:[sessionWithDelegate dataTaskWithURL:[NSURL URLWithString:kFlipboardIcon]]];
|
||||
// NSURLSessionDownloadTask
|
||||
[pendingTasks addObject:[session downloadTaskWithURL:[NSURL URLWithString:kRandomAnimal]]];
|
||||
[pendingTasks addObject:[sessionWithDelegate downloadTaskWithURL:[NSURL URLWithString:kRandomAnimal]]];
|
||||
|
||||
// Without delegate //
|
||||
|
||||
@@ -71,7 +75,7 @@
|
||||
NSMutableURLRequest *upload = [self request:kImgurUpload];
|
||||
upload.HTTPMethod = @"POST";
|
||||
[upload setValue:@"Client-ID 0e8a1cb2eb594ef" forHTTPHeaderField:@"Authorization"];
|
||||
[pendingTasks addObject:[session
|
||||
[pendingTasks addObject:[sessionWithDelegate
|
||||
uploadTaskWithRequest:upload
|
||||
fromFile:[NSBundle.mainBundle URLForResource:@"image" withExtension:@"jpg"]
|
||||
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
@@ -148,6 +152,27 @@
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
- (void)sendExampleWebsocketTraffic:(NSURLSession *)sessionWithDelegate {
|
||||
NSString *APIKey = @"oCdCMcMPQpbvNjUIzqtvF1d2X2okWpDQj4AwARJuAgtjhzKxVEjQU6IdCjwm";
|
||||
NSString *wsurl = [NSString stringWithFormat:@"wss://demo.piesocket.com/v3/channel_1?api_key=%@¬ify_self", APIKey];
|
||||
NSURLSessionWebSocketTask *task = [sessionWithDelegate webSocketTaskWithURL:[NSURL URLWithString:wsurl]];
|
||||
[task resume];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)task didOpenWithProtocol:(NSString *)protocol {
|
||||
[task receiveMessageWithCompletionHandler:^(NSURLSessionWebSocketMessage *message, NSError *error) {
|
||||
if (!error) {
|
||||
NSLog(@"Received WS message: %@", message.string);
|
||||
id message = [[NSURLSessionWebSocketMessage alloc] initWithString:@"Hello"];
|
||||
[task sendMessage:message completionHandler:^(NSError *error) {
|
||||
if (error) {
|
||||
NSLog(@"Error sending WS message: %@", error.localizedDescription);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
|
||||
NSLog(@"URLSession didBecomeInvalidWithError: %@", error.localizedDescription);
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "FLEX"
|
||||
spec.version = "4.5.0"
|
||||
spec.version = "4.6.1"
|
||||
spec.summary = "A set of in-app debugging and exploration tools for iOS"
|
||||
spec.description = <<-DESC
|
||||
- Inspect and modify views in the hierarchy.
|
||||
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
|
||||
- Dynamically view and modify `NSUserDefaults` values.
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/Flipboard/FLEX"
|
||||
spec.homepage = "https://github.com/FLEXTool/FLEX"
|
||||
spec.screenshots = [ "http://engineering.flipboard.com/assets/flex/basic-view-exploration.gif",
|
||||
"http://engineering.flipboard.com/assets/flex/advanced-view-editing.gif",
|
||||
"http://engineering.flipboard.com/assets/flex/heap-browser.gif",
|
||||
@@ -30,12 +30,12 @@ Pod::Spec.new do |spec|
|
||||
spec.author = { "Tanner Bennett" => "tannerbennett@me.com" }
|
||||
spec.social_media_url = "https://twitter.com/NSExceptional"
|
||||
spec.platform = :ios, "9.0"
|
||||
spec.source = { :git => "https://github.com/Flipboard/FLEX.git", :tag => "#{spec.version}" }
|
||||
spec.source = { :git => "https://github.com/FLEXTool/FLEX.git", :tag => "#{spec.version}" }
|
||||
spec.source_files = "Classes/**/*.{h,c,m,mm}"
|
||||
spec.frameworks = [ "Foundation", "UIKit", "CoreGraphics", "ImageIO", "QuartzCore", "WebKit", "Security", "SceneKit" ]
|
||||
spec.libraries = [ "z", "sqlite3" ]
|
||||
spec.requires_arc = true
|
||||
spec.compiler_flags = "-Wno-unsupported-availability-guard -Wno-strict-prototypes"
|
||||
spec.compiler_flags = "-Wno-unsupported-availability-guard"
|
||||
spec.public_header_files = [ "Classes/*.h", "Classes/Manager/*.h", "Classes/Toolbar/*.h",
|
||||
"Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.h",
|
||||
"Classes/Core/**/*.h", "Classes/Utility/Runtime/Objc/**/*.h",
|
||||
|
||||
@@ -95,8 +95,6 @@
|
||||
3A4C953B1B5B21410088C3F2 /* FLEXNetworkSettingsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94BA1B5B21410088C3F2 /* FLEXNetworkSettingsController.m */; };
|
||||
3A4C953C1B5B21410088C3F2 /* FLEXNetworkTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94BB1B5B21410088C3F2 /* FLEXNetworkTransaction.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
3A4C953D1B5B21410088C3F2 /* FLEXNetworkTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94BC1B5B21410088C3F2 /* FLEXNetworkTransaction.m */; };
|
||||
3A4C953E1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94BD1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
3A4C953F1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94BE1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.m */; };
|
||||
3A4C95401B5B21410088C3F2 /* FLEXNetworkTransactionCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94BF1B5B21410088C3F2 /* FLEXNetworkTransactionCell.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
3A4C95411B5B21410088C3F2 /* FLEXNetworkTransactionCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94C01B5B21410088C3F2 /* FLEXNetworkTransactionCell.m */; };
|
||||
3A4C95421B5B21410088C3F2 /* FLEXNetworkObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94C21B5B21410088C3F2 /* FLEXNetworkObserver.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
@@ -200,6 +198,10 @@
|
||||
C3531BA623E88A2100A184AD /* FLEXNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = C3531BA423E88A2100A184AD /* FLEXNavigationController.m */; };
|
||||
C3531BAA23E88FAC00A184AD /* FLEXTabList.h in Headers */ = {isa = PBXBuildFile; fileRef = C3531BA823E88FAC00A184AD /* FLEXTabList.h */; };
|
||||
C3531BAB23E88FAC00A184AD /* FLEXTabList.m in Sources */ = {isa = PBXBuildFile; fileRef = C3531BA923E88FAC00A184AD /* FLEXTabList.m */; };
|
||||
C35DAD822709140700AA95E6 /* FLEXHTTPTransactionDetailController.h in Headers */ = {isa = PBXBuildFile; fileRef = C35DAD7E2709140700AA95E6 /* FLEXHTTPTransactionDetailController.h */; };
|
||||
C35DAD832709140700AA95E6 /* FLEXHTTPTransactionDetailController.m in Sources */ = {isa = PBXBuildFile; fileRef = C35DAD7F2709140700AA95E6 /* FLEXHTTPTransactionDetailController.m */; };
|
||||
C35DAD8E2709143000AA95E6 /* FLEXMITMDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = C35DAD8C2709143000AA95E6 /* FLEXMITMDataSource.h */; };
|
||||
C35DAD8F2709143000AA95E6 /* FLEXMITMDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = C35DAD8D2709143000AA95E6 /* FLEXMITMDataSource.m */; };
|
||||
C362AE8123C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h in Headers */ = {isa = PBXBuildFile; fileRef = C362AE7F23C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
C362AE8223C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m in Sources */ = {isa = PBXBuildFile; fileRef = C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */; };
|
||||
C3694DBA23EA1096006625D7 /* FLEXTabsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C3694DB823EA1096006625D7 /* FLEXTabsViewController.h */; };
|
||||
@@ -317,6 +319,8 @@
|
||||
C3BFD071233C23ED0015FB82 /* NSArray+FLEX.m in Sources */ = {isa = PBXBuildFile; fileRef = C3BFD06F233C23ED0015FB82 /* NSArray+FLEX.m */; };
|
||||
C3DB9F642107FC9600B46809 /* FLEXObjectRef.h in Headers */ = {isa = PBXBuildFile; fileRef = C3DB9F622107FC9600B46809 /* FLEXObjectRef.h */; };
|
||||
C3DB9F652107FC9600B46809 /* FLEXObjectRef.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DB9F632107FC9600B46809 /* FLEXObjectRef.m */; };
|
||||
C3DBFD0C26CE2FAF00E0466A /* OSCache.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DBFD0A26CE2FAF00E0466A /* OSCache.m */; };
|
||||
C3DBFD0D26CE2FAF00E0466A /* OSCache.h in Headers */ = {isa = PBXBuildFile; fileRef = C3DBFD0B26CE2FAF00E0466A /* OSCache.h */; };
|
||||
C3DC287C223ED5F200F48AA6 /* FLEXOSLogController.m in Sources */ = {isa = PBXBuildFile; fileRef = C34EE30721CB23CC00BD3A7C /* FLEXOSLogController.m */; };
|
||||
C3DFCD942416BC6500BB7084 /* FLEXFilteringTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C3DFCD922416BC6500BB7084 /* FLEXFilteringTableViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3DFCD952416BC6500BB7084 /* FLEXFilteringTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DFCD932416BC6500BB7084 /* FLEXFilteringTableViewController.m */; };
|
||||
@@ -462,8 +466,6 @@
|
||||
3A4C94BA1B5B21410088C3F2 /* FLEXNetworkSettingsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkSettingsController.m; sourceTree = "<group>"; };
|
||||
3A4C94BB1B5B21410088C3F2 /* FLEXNetworkTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransaction.h; sourceTree = "<group>"; };
|
||||
3A4C94BC1B5B21410088C3F2 /* FLEXNetworkTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransaction.m; sourceTree = "<group>"; };
|
||||
3A4C94BD1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransactionDetailController.h; sourceTree = "<group>"; };
|
||||
3A4C94BE1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransactionDetailController.m; sourceTree = "<group>"; };
|
||||
3A4C94BF1B5B21410088C3F2 /* FLEXNetworkTransactionCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransactionCell.h; sourceTree = "<group>"; };
|
||||
3A4C94C01B5B21410088C3F2 /* FLEXNetworkTransactionCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransactionCell.m; sourceTree = "<group>"; };
|
||||
3A4C94C21B5B21410088C3F2 /* FLEXNetworkObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkObserver.h; sourceTree = "<group>"; };
|
||||
@@ -570,6 +572,10 @@
|
||||
C3531BA423E88A2100A184AD /* FLEXNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNavigationController.m; sourceTree = "<group>"; };
|
||||
C3531BA823E88FAC00A184AD /* FLEXTabList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXTabList.h; sourceTree = "<group>"; };
|
||||
C3531BA923E88FAC00A184AD /* FLEXTabList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXTabList.m; sourceTree = "<group>"; };
|
||||
C35DAD7E2709140700AA95E6 /* FLEXHTTPTransactionDetailController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXHTTPTransactionDetailController.h; sourceTree = "<group>"; };
|
||||
C35DAD7F2709140700AA95E6 /* FLEXHTTPTransactionDetailController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXHTTPTransactionDetailController.m; sourceTree = "<group>"; };
|
||||
C35DAD8C2709143000AA95E6 /* FLEXMITMDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXMITMDataSource.h; sourceTree = "<group>"; };
|
||||
C35DAD8D2709143000AA95E6 /* FLEXMITMDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXMITMDataSource.m; sourceTree = "<group>"; };
|
||||
C362AE7F23C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMapTable+FLEX_Subscripting.h"; sourceTree = "<group>"; };
|
||||
C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMapTable+FLEX_Subscripting.m"; sourceTree = "<group>"; };
|
||||
C3694DB823EA1096006625D7 /* FLEXTabsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXTabsViewController.h; sourceTree = "<group>"; };
|
||||
@@ -684,6 +690,8 @@
|
||||
C3BFD06F233C23ED0015FB82 /* NSArray+FLEX.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSArray+FLEX.m"; sourceTree = "<group>"; };
|
||||
C3DB9F622107FC9600B46809 /* FLEXObjectRef.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXObjectRef.h; sourceTree = "<group>"; };
|
||||
C3DB9F632107FC9600B46809 /* FLEXObjectRef.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXObjectRef.m; sourceTree = "<group>"; };
|
||||
C3DBFD0A26CE2FAF00E0466A /* OSCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSCache.m; sourceTree = "<group>"; };
|
||||
C3DBFD0B26CE2FAF00E0466A /* OSCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCache.h; sourceTree = "<group>"; };
|
||||
C3DFCD922416BC6500BB7084 /* FLEXFilteringTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXFilteringTableViewController.h; sourceTree = "<group>"; };
|
||||
C3DFCD932416BC6500BB7084 /* FLEXFilteringTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXFilteringTableViewController.m; sourceTree = "<group>"; };
|
||||
C3DFCD962416E7DD00BB7084 /* FLEXMutableListSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXMutableListSection.h; sourceTree = "<group>"; };
|
||||
@@ -974,12 +982,15 @@
|
||||
3A4C94BA1B5B21410088C3F2 /* FLEXNetworkSettingsController.m */,
|
||||
3A4C94BB1B5B21410088C3F2 /* FLEXNetworkTransaction.h */,
|
||||
3A4C94BC1B5B21410088C3F2 /* FLEXNetworkTransaction.m */,
|
||||
3A4C94BD1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.h */,
|
||||
3A4C94BE1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.m */,
|
||||
C35DAD7E2709140700AA95E6 /* FLEXHTTPTransactionDetailController.h */,
|
||||
C35DAD7F2709140700AA95E6 /* FLEXHTTPTransactionDetailController.m */,
|
||||
C35DAD8C2709143000AA95E6 /* FLEXMITMDataSource.h */,
|
||||
C35DAD8D2709143000AA95E6 /* FLEXMITMDataSource.m */,
|
||||
3A4C94BF1B5B21410088C3F2 /* FLEXNetworkTransactionCell.h */,
|
||||
3A4C94C01B5B21410088C3F2 /* FLEXNetworkTransactionCell.m */,
|
||||
2EF6B04C1D494BE50006BDA5 /* FLEXNetworkCurlLogger.h */,
|
||||
2EF6B04B1D494BE50006BDA5 /* FLEXNetworkCurlLogger.m */,
|
||||
C3DBFD0926CE2FAF00E0466A /* OSCache */,
|
||||
3A4C94C11B5B21410088C3F2 /* PonyDebugger */,
|
||||
);
|
||||
path = Network;
|
||||
@@ -1414,6 +1425,15 @@
|
||||
path = Shortcuts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3DBFD0926CE2FAF00E0466A /* OSCache */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3DBFD0A26CE2FAF00E0466A /* OSCache.m */,
|
||||
C3DBFD0B26CE2FAF00E0466A /* OSCache.h */,
|
||||
);
|
||||
path = OSCache;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3E5D9FF2317007F00E655DB /* Sections */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1481,7 +1501,6 @@
|
||||
94A515141C4CA1C00063292F /* FLEXManager.h in Headers */,
|
||||
C37A0C93218BAC9600848CA7 /* FLEXObjcInternal.h in Headers */,
|
||||
C3A9422C23C3DA39006871A3 /* FHSView.h in Headers */,
|
||||
3A4C953E1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.h in Headers */,
|
||||
3A4C95301B5B21410088C3F2 /* FLEXSystemLogMessage.h in Headers */,
|
||||
C398625523AD6C67007E6793 /* FLEXObjcRuntimeViewController.h in Headers */,
|
||||
C3E5DA02231700EE00E655DB /* FLEXObjectInfoSection.h in Headers */,
|
||||
@@ -1518,6 +1537,7 @@
|
||||
C312A13023ECB5D300E38049 /* FLEXBookmarkManager.h in Headers */,
|
||||
3A4C95341B5B21410088C3F2 /* FLEXSystemLogViewController.h in Headers */,
|
||||
C34C9BDD23A7F2740031CA3E /* FLEXRuntime+UIKitHelpers.h in Headers */,
|
||||
C35DAD822709140700AA95E6 /* FLEXHTTPTransactionDetailController.h in Headers */,
|
||||
C398624F23AD6C67007E6793 /* FLEXRuntimeKeyPathTokenizer.h in Headers */,
|
||||
C362AE8123C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h in Headers */,
|
||||
3A4C95091B5B21410088C3F2 /* FLEXFieldEditorView.h in Headers */,
|
||||
@@ -1544,6 +1564,7 @@
|
||||
3A4C94FF1B5B21410088C3F2 /* FLEXArgumentInputSwitchView.h in Headers */,
|
||||
C398625423AD6C67007E6793 /* FLEXRuntimeBrowserToolbar.h in Headers */,
|
||||
3A4C94E71B5B21410088C3F2 /* FLEXHierarchyTableViewCell.h in Headers */,
|
||||
C3DBFD0D26CE2FAF00E0466A /* OSCache.h in Headers */,
|
||||
224D49AA1C673AB5000EAB86 /* FLEXSQLiteDatabaseManager.h in Headers */,
|
||||
C386D6A9241995A800699085 /* FLEXTypeEncodingParser.h in Headers */,
|
||||
C30D2962261FAE9E00D89649 /* FLEXNSDataShortcuts.h in Headers */,
|
||||
@@ -1597,6 +1618,7 @@
|
||||
C33C825E2316DC8600DD2451 /* FLEXObjectExplorer.h in Headers */,
|
||||
C34D4EB423A2AF2A00C1F903 /* FLEXColorPreviewSection.h in Headers */,
|
||||
C383C3BE23B6B398007A321B /* UITextField+Range.h in Headers */,
|
||||
C35DAD8E2709143000AA95E6 /* FLEXMITMDataSource.h in Headers */,
|
||||
224D49A81C673AB5000EAB86 /* FLEXRealmDatabaseManager.h in Headers */,
|
||||
C313853F23F5C1A10046E63C /* FLEXViewControllersViewController.h in Headers */,
|
||||
C386D6ED24199EC600699085 /* FLEX-Runtime.h in Headers */,
|
||||
@@ -1753,6 +1775,7 @@
|
||||
C3DFCDB92418336D00BB7084 /* NSUserDefaults+FLEX.m in Sources */,
|
||||
C3878DBE23A74A8F0038FDBE /* FLEXNetworkRecorder.m in Sources */,
|
||||
C3878DBC23A749F70038FDBE /* FLEXFieldEditorViewController.m in Sources */,
|
||||
C35DAD832709140700AA95E6 /* FLEXHTTPTransactionDetailController.m in Sources */,
|
||||
942DCD871BAE0CA300DB5DC2 /* FLEXKeyboardShortcutManager.m in Sources */,
|
||||
C30D2961261FAE9E00D89649 /* FLEXNSDataShortcuts.m in Sources */,
|
||||
C3F977862311B38F0032776D /* NSString+ObjcRuntime.m in Sources */,
|
||||
@@ -1845,6 +1868,7 @@
|
||||
3A4C94FC1B5B21410088C3F2 /* FLEXArgumentInputStringView.m in Sources */,
|
||||
3A4C94F81B5B21410088C3F2 /* FLEXArgumentInputNotSupportedView.m in Sources */,
|
||||
3A4C95351B5B21410088C3F2 /* FLEXSystemLogViewController.m in Sources */,
|
||||
C3DBFD0C26CE2FAF00E0466A /* OSCache.m in Sources */,
|
||||
C383C3C623B6BB81007A321B /* FLEXCodeFontCell.m in Sources */,
|
||||
3A4C95271B5B21410088C3F2 /* FLEXGlobalsViewController.m in Sources */,
|
||||
C313854823F5F7D50046E63C /* flex_fishhook.c in Sources */,
|
||||
@@ -1879,12 +1903,12 @@
|
||||
3A4C94FA1B5B21410088C3F2 /* FLEXArgumentInputNumberView.m in Sources */,
|
||||
C3474C4123DA496400466532 /* FLEXKeyValueTableViewCell.m in Sources */,
|
||||
779B1ED71C0C4D7C001F5E49 /* FLEXTableContentViewController.m in Sources */,
|
||||
C35DAD8F2709143000AA95E6 /* FLEXMITMDataSource.m in Sources */,
|
||||
C36FBFCB230F3B98008D95D5 /* FLEXMirror.m in Sources */,
|
||||
C36FBFD5230F3B98008D95D5 /* FLEXMethodBase.m in Sources */,
|
||||
3A4C95001B5B21410088C3F2 /* FLEXArgumentInputSwitchView.m in Sources */,
|
||||
C386D6F02419A33F00699085 /* FLEXRuntimeConstants.m in Sources */,
|
||||
C3F31D442267D883003C991A /* FLEXTableView.m in Sources */,
|
||||
3A4C953F1B5B21410088C3F2 /* FLEXNetworkTransactionDetailController.m in Sources */,
|
||||
224D49AB1C673AB5000EAB86 /* FLEXSQLiteDatabaseManager.m in Sources */,
|
||||
C312A13C23ECE79000E38049 /* FLEXWindowManagerController.m in Sources */,
|
||||
C30D2969261FAEEE00D89649 /* Cocoa+FLEXShortcuts.m in Sources */,
|
||||
@@ -2084,7 +2108,6 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
@@ -2093,11 +2116,13 @@
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
INFOPLIST_FILE = Classes/Info.plist;
|
||||
INSTALL_PATH = "@rpath";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.flipboard.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
@@ -2119,7 +2144,6 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
@@ -2128,11 +2152,13 @@
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
INFOPLIST_FILE = Classes/Info.plist;
|
||||
INSTALL_PATH = "@rpath";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.flipboard.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <objc/runtime.h>
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "FLEXObjcInternal.h"
|
||||
#import "FLEXHeapEnumerator.h"
|
||||
#import "FLEXObjectRef.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXProperty.h"
|
||||
@@ -25,6 +28,12 @@
|
||||
@end
|
||||
@implementation Subclass @end
|
||||
|
||||
@interface NeverCreated : Subclass {
|
||||
NSInteger a, b, c;
|
||||
}
|
||||
@end
|
||||
@implementation NeverCreated @end
|
||||
|
||||
@interface FLEXTests : XCTestCase
|
||||
@property (nonatomic, setter=setMyFoo:) id foo;
|
||||
@end
|
||||
@@ -140,4 +149,22 @@
|
||||
XCTAssertEqualObjects(@"FLEXNewRootClass", className);
|
||||
}
|
||||
|
||||
- (void)testInvalidObjectFinding {
|
||||
// Create something that looks like an objc object
|
||||
uintptr_t *pointer = (uintptr_t *)calloc(1, sizeof(uintptr_t));
|
||||
object_setClass((__bridge id)(void *)pointer, [NeverCreated class]);
|
||||
|
||||
// Find that one object and assert that it is the object we just created
|
||||
NSArray<FLEXObjectRef *> *refs = [FLEXHeapEnumerator
|
||||
instancesOfClassWithName:@"NeverCreated" retained:NO
|
||||
];
|
||||
XCTAssertEqual(refs.count, 1);
|
||||
XCTAssertEqual((__bridge void *)refs.firstObject.object, pointer);
|
||||
|
||||
// Find that the object isn't really an object
|
||||
XCTAssertFalse(FLEXPointerIsValidObjcObject(pointer));
|
||||
|
||||
free(pointer);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user