├── .gitignore ├── Aspects-LICENSE ├── Aspects.h ├── Aspects.m ├── LICENSE ├── PSTEnableUIKitDebugging.m ├── README.md ├── fishhook-LICENSE ├── fishhook.c └── fishhook.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | # Pods/ 27 | -------------------------------------------------------------------------------- /Aspects-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Peter Steinberger, steipete@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Aspects.h: -------------------------------------------------------------------------------- 1 | // 2 | // Aspects.h 3 | // Aspects - A delightful, simple library for aspect oriented programming. 4 | // 5 | // Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. 6 | // 7 | 8 | #import 9 | 10 | typedef NS_OPTIONS(NSUInteger, AspectOptions) { 11 | AspectPositionAfter = 0, /// Called after the original implementation (default) 12 | AspectPositionInstead = 1, /// Will replace the original implementation. 13 | AspectPositionBefore = 2, /// Called before the original implementation. 14 | 15 | AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution. 16 | }; 17 | 18 | /// Opaque Aspect Token that allows to deregister the hook. 19 | @protocol AspectToken 20 | 21 | /// Deregisters an aspect. 22 | /// @return YES if deregistration is successful, otherwise NO. 23 | - (BOOL)remove; 24 | 25 | @end 26 | 27 | /// The AspectInfo protocol is the first parameter of our block syntax. 28 | @protocol AspectInfo 29 | 30 | /// The instance that is currently hooked. 31 | - (id)instance; 32 | 33 | /// The original invocation of the hooked method. 34 | - (NSInvocation *)originalInvocation; 35 | 36 | /// All method arguments, boxed. This is lazily evaluated. 37 | - (NSArray *)arguments; 38 | 39 | @end 40 | 41 | /** 42 | Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second. 43 | 44 | Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe. 45 | */ 46 | @interface NSObject (Aspects) 47 | 48 | /// Adds a block of code before/instead/after the current `selector` for a specific class. 49 | /// 50 | /// @param block Aspects replicates the type signature of the method being hooked. 51 | /// The first parameter will be `id`, followed by all parameters of the method. 52 | /// These parameters are optional and will be filled to match the block signature. 53 | /// You can even use an empty block, or one that simple gets `id`. 54 | /// 55 | /// @note Hooking static methods is not supported. 56 | /// @return A token which allows to later deregister the aspect. 57 | + (id)aspect_hookSelector:(SEL)selector 58 | withOptions:(AspectOptions)options 59 | usingBlock:(id)block 60 | error:(NSError *__autoreleasing*)error; 61 | 62 | /// Adds a block of code before/instead/after the current `selector` for a specific instance. 63 | - (id)aspect_hookSelector:(SEL)selector 64 | withOptions:(AspectOptions)options 65 | usingBlock:(id)block 66 | error:(NSError *__autoreleasing*)error; 67 | 68 | @end 69 | 70 | 71 | typedef NS_ENUM(NSUInteger, AspectErrorCode) { 72 | AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted. 73 | AspectErrorDoesNotRespondToSelector, /// Selector could not be found. 74 | AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed. 75 | AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed. 76 | AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair. 77 | AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called. 78 | AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large. 79 | 80 | AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated. 81 | }; 82 | 83 | extern NSString *const AspectErrorDomain; 84 | -------------------------------------------------------------------------------- /Aspects.m: -------------------------------------------------------------------------------- 1 | // 2 | // Aspects.m 3 | // Aspects - A delightful, simple library for aspect oriented programming. 4 | // 5 | // Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. 6 | // 7 | 8 | #import "Aspects.h" 9 | #import 10 | #import 11 | #import 12 | 13 | #define AspectLog(...) 14 | //#define AspectLog(...) do { NSLog(__VA_ARGS__); }while(0) 15 | #define AspectLogError(...) do { NSLog(__VA_ARGS__); }while(0) 16 | 17 | // Block internals. 18 | typedef NS_OPTIONS(int, AspectBlockFlags) { 19 | AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), 20 | AspectBlockFlagsHasSignature = (1 << 30) 21 | }; 22 | typedef struct _AspectBlock { 23 | __unused Class isa; 24 | AspectBlockFlags flags; 25 | __unused int reserved; 26 | void (__unused *invoke)(struct _AspectBlock *block, ...); 27 | struct { 28 | unsigned long int reserved; 29 | unsigned long int size; 30 | // requires AspectBlockFlagsHasCopyDisposeHelpers 31 | void (*copy)(void *dst, const void *src); 32 | void (*dispose)(const void *); 33 | // requires AspectBlockFlagsHasSignature 34 | const char *signature; 35 | const char *layout; 36 | } *descriptor; 37 | // imported variables 38 | } *AspectBlockRef; 39 | 40 | @interface AspectInfo : NSObject 41 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation; 42 | @property (nonatomic, unsafe_unretained, readonly) id instance; 43 | @property (nonatomic, strong, readonly) NSArray *arguments; 44 | @property (nonatomic, strong, readonly) NSInvocation *originalInvocation; 45 | @end 46 | 47 | // Tracks a single aspect. 48 | @interface AspectIdentifier : NSObject 49 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError *__autoreleasing*)error; 50 | - (BOOL)invokeWithInfo:(id)info; 51 | @property (nonatomic, assign) SEL selector; 52 | @property (nonatomic, strong) id block; 53 | @property (nonatomic, strong) NSMethodSignature *blockSignature; 54 | @property (nonatomic, weak) id object; 55 | @property (nonatomic, assign) AspectOptions options; 56 | @end 57 | 58 | // Tracks all aspects for an object/class. 59 | @interface AspectsContainer : NSObject 60 | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition; 61 | - (BOOL)removeAspect:(id)aspect; 62 | - (BOOL)hasAspects; 63 | @property (atomic, copy) NSArray *beforeAspects; 64 | @property (atomic, copy) NSArray *insteadAspects; 65 | @property (atomic, copy) NSArray *afterAspects; 66 | @end 67 | 68 | @interface AspectTracker : NSObject 69 | - (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)parent; 70 | @property (nonatomic, strong) Class trackedClass; 71 | @property (nonatomic, strong) NSMutableSet *selectorNames; 72 | @property (nonatomic, weak) AspectTracker *parentEntry; 73 | @end 74 | 75 | @interface NSInvocation (Aspects) 76 | - (NSArray *)aspects_arguments; 77 | @end 78 | 79 | #define AspectPositionFilter 0x07 80 | 81 | #define AspectError(errorCode, errorDescription) do { \ 82 | AspectLogError(@"Aspects: %@", errorDescription); \ 83 | if (error) { *error = [NSError errorWithDomain:AspectErrorDomain code:errorCode userInfo:@{NSLocalizedDescriptionKey: errorDescription}]; }}while(0) 84 | 85 | NSString *const AspectErrorDomain = @"AspectErrorDomain"; 86 | static NSString *const AspectsSubclassSuffix = @"_Aspects_"; 87 | static NSString *const AspectsMessagePrefix = @"aspects_"; 88 | 89 | @implementation NSObject (Aspects) 90 | 91 | /////////////////////////////////////////////////////////////////////////////////////////// 92 | #pragma mark - Public Aspects API 93 | 94 | + (id)aspect_hookSelector:(SEL)selector 95 | withOptions:(AspectOptions)options 96 | usingBlock:(id)block 97 | error:(NSError *__autoreleasing*)error { 98 | return aspect_add((id)self, selector, options, block, error); 99 | } 100 | 101 | /// @return A token which allows to later deregister the aspect. 102 | - (id)aspect_hookSelector:(SEL)selector 103 | withOptions:(AspectOptions)options 104 | usingBlock:(id)block 105 | error:(NSError *__autoreleasing*)error { 106 | return aspect_add(self, selector, options, block, error); 107 | } 108 | 109 | /////////////////////////////////////////////////////////////////////////////////////////// 110 | #pragma mark - Private Helper 111 | 112 | static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError *__autoreleasing*error) { 113 | NSCParameterAssert(self); 114 | NSCParameterAssert(selector); 115 | NSCParameterAssert(block); 116 | 117 | __block AspectIdentifier *identifier = nil; 118 | aspect_performLocked(^{ 119 | if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { 120 | AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); 121 | identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; 122 | if (identifier) { 123 | [aspectContainer addAspect:identifier withOptions:options]; 124 | 125 | // Modify the class to allow message interception. 126 | aspect_prepareClassAndHookSelector(self, selector, error); 127 | } 128 | } 129 | }); 130 | return identifier; 131 | } 132 | 133 | static BOOL aspect_remove(AspectIdentifier *aspect, NSError *__autoreleasing*error) { 134 | NSCAssert([aspect isKindOfClass:AspectIdentifier.class], @"Must have correct type."); 135 | 136 | __block BOOL success = NO; 137 | aspect_performLocked(^{ 138 | id self = aspect.object; // strongify 139 | if (self) { 140 | AspectsContainer *aspectContainer = aspect_getContainerForObject(self, aspect.selector); 141 | success = [aspectContainer removeAspect:aspect]; 142 | 143 | aspect_cleanupHookedClassAndSelector(self, aspect.selector); 144 | // destroy token 145 | aspect.object = nil; 146 | aspect.block = nil; 147 | aspect.selector = NULL; 148 | } else { 149 | NSString *errrorDesc = [NSString stringWithFormat:@"Unable to deregister hook. Object already deallocated: %@", aspect]; 150 | AspectError(AspectErrorRemoveObjectAlreadyDeallocated, errrorDesc); 151 | } 152 | }); 153 | return success; 154 | } 155 | 156 | static void aspect_performLocked(dispatch_block_t block) { 157 | static OSSpinLock aspect_lock = OS_SPINLOCK_INIT; 158 | OSSpinLockLock(&aspect_lock); 159 | block(); 160 | OSSpinLockUnlock(&aspect_lock); 161 | } 162 | 163 | static SEL aspect_aliasForSelector(SEL selector) { 164 | NSCParameterAssert(selector); 165 | return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]); 166 | } 167 | 168 | static NSMethodSignature *aspect_blockMethodSignature(id block, NSError *__autoreleasing*error) { 169 | AspectBlockRef layout = (__bridge void *)block; 170 | if (!(layout->flags & AspectBlockFlagsHasSignature)) { 171 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; 172 | AspectError(AspectErrorMissingBlockSignature, description); 173 | return nil; 174 | } 175 | void *desc = layout->descriptor; 176 | desc += 2 * sizeof(unsigned long int); 177 | if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { 178 | desc += 2 * sizeof(void *); 179 | } 180 | if (!desc) { 181 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; 182 | AspectError(AspectErrorMissingBlockSignature, description); 183 | return nil; 184 | } 185 | const char *signature = (*(const char **)desc); 186 | return [NSMethodSignature signatureWithObjCTypes:signature]; 187 | } 188 | 189 | static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError *__autoreleasing*error) { 190 | NSCParameterAssert(blockSignature); 191 | NSCParameterAssert(object); 192 | NSCParameterAssert(selector); 193 | 194 | BOOL signaturesMatch = YES; 195 | NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector]; 196 | if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) { 197 | signaturesMatch = NO; 198 | } else { 199 | if (blockSignature.numberOfArguments > 1) { 200 | const char *blockType = [blockSignature getArgumentTypeAtIndex:1]; 201 | if (blockType[0] != '@') { 202 | signaturesMatch = NO; 203 | } 204 | } 205 | // Argument 0 is self/block, argument 1 is SEL or id. We start comparing at argument 2. 206 | // The block can have less arguments than the method, that's ok. 207 | if (signaturesMatch) { 208 | for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) { 209 | const char *methodType = [methodSignature getArgumentTypeAtIndex:idx]; 210 | const char *blockType = [blockSignature getArgumentTypeAtIndex:idx]; 211 | // Only compare parameter, not the optional type data. 212 | if (!methodType || !blockType || methodType[0] != blockType[0]) { 213 | signaturesMatch = NO; break; 214 | } 215 | } 216 | } 217 | } 218 | 219 | if (!signaturesMatch) { 220 | NSString *description = [NSString stringWithFormat:@"Blog signature %@ doesn't match %@.", blockSignature, methodSignature]; 221 | AspectError(AspectErrorIncompatibleBlockSignature, description); 222 | return NO; 223 | } 224 | return YES; 225 | } 226 | 227 | /////////////////////////////////////////////////////////////////////////////////////////// 228 | #pragma mark - Class + Selector Preparation 229 | 230 | static BOOL aspect_isMsgForwardIMP(IMP impl) { 231 | return impl == _objc_msgForward 232 | #if !defined(__arm64__) 233 | || impl == (IMP)_objc_msgForward_stret 234 | #endif 235 | ; 236 | } 237 | 238 | static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) { 239 | IMP msgForwardIMP = _objc_msgForward; 240 | #if !defined(__arm64__) 241 | // As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id. 242 | // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html 243 | // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783 244 | // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4) 245 | Method method = class_getInstanceMethod(self.class, selector); 246 | const char *encoding = method_getTypeEncoding(method); 247 | BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B; 248 | if (methodReturnsStructValue) { 249 | @try { 250 | NSUInteger valueSize = 0; 251 | NSGetSizeAndAlignment(encoding, &valueSize, NULL); 252 | 253 | if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) { 254 | methodReturnsStructValue = NO; 255 | } 256 | } @catch (__unused NSException *e) {} 257 | } 258 | if (methodReturnsStructValue) { 259 | msgForwardIMP = (IMP)_objc_msgForward_stret; 260 | } 261 | #endif 262 | return msgForwardIMP; 263 | } 264 | 265 | static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError *__autoreleasing*error) { 266 | NSCParameterAssert(selector); 267 | Class klass = aspect_hookClass(self, error); 268 | Method targetMethod = class_getInstanceMethod(klass, selector); 269 | IMP targetMethodIMP = method_getImplementation(targetMethod); 270 | if (!aspect_isMsgForwardIMP(targetMethodIMP)) { 271 | // Make a method alias for the existing method implementation, it not already copied. 272 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 273 | SEL aliasSelector = aspect_aliasForSelector(selector); 274 | if (![klass instancesRespondToSelector:aliasSelector]) { 275 | BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); 276 | NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 277 | } 278 | 279 | // We use forwardInvocation to hook in. 280 | class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); 281 | AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 282 | } 283 | } 284 | 285 | // Will undo the runtime changes made. 286 | static void aspect_cleanupHookedClassAndSelector(NSObject *self, SEL selector) { 287 | NSCParameterAssert(self); 288 | NSCParameterAssert(selector); 289 | 290 | Class klass = object_getClass(self); 291 | BOOL isMetaClass = class_isMetaClass(klass); 292 | if (isMetaClass) { 293 | klass = (Class)self; 294 | } 295 | 296 | // Check if the method is marked as forwarded and undo that. 297 | Method targetMethod = class_getInstanceMethod(klass, selector); 298 | IMP targetMethodIMP = method_getImplementation(targetMethod); 299 | if (aspect_isMsgForwardIMP(targetMethodIMP)) { 300 | // Restore the original method implementation. 301 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 302 | SEL aliasSelector = aspect_aliasForSelector(selector); 303 | Method originalMethod = class_getInstanceMethod(klass, aliasSelector); 304 | IMP originalIMP = method_getImplementation(originalMethod); 305 | NSCAssert(originalMethod, @"Original implementation for %@ not found %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 306 | 307 | class_replaceMethod(klass, selector, originalIMP, typeEncoding); 308 | AspectLog(@"Aspects: Removed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 309 | } 310 | 311 | // Deregister global tracked selector 312 | aspect_deregisterTrackedSelector(self, selector); 313 | 314 | // Get the aspect container and check if there are any hooks remaining. Clean up if there are not. 315 | AspectsContainer *container = aspect_getContainerForObject(self, selector); 316 | if (!container.hasAspects) { 317 | // Destroy the container 318 | aspect_destroyContainerForObject(self, selector); 319 | 320 | // Figure out how the class was modified to undo the changes. 321 | NSString *className = NSStringFromClass(klass); 322 | if ([className hasSuffix:AspectsSubclassSuffix]) { 323 | Class originalClass = NSClassFromString([className stringByReplacingOccurrencesOfString:AspectsSubclassSuffix withString:@""]); 324 | NSCAssert(originalClass != nil, @"Original class must exist"); 325 | object_setClass(self, originalClass); 326 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(originalClass)); 327 | 328 | // We can only dispose the class pair if we can ensure that no instances exist using our subclass. 329 | // Since we don't globally track this, we can't ensure this - but there's also not much overhead in keeping it around. 330 | //objc_disposeClassPair(object.class); 331 | } else { 332 | // Class is most likely swizzled in place. Undo that. 333 | if (isMetaClass) { 334 | aspect_undoSwizzleClassInPlace((Class)self); 335 | } 336 | } 337 | } 338 | } 339 | 340 | /////////////////////////////////////////////////////////////////////////////////////////// 341 | #pragma mark - Hook Class 342 | 343 | static Class aspect_hookClass(NSObject *self, NSError *__autoreleasing*error) { 344 | NSCParameterAssert(self); 345 | Class statedClass = self.class; 346 | Class baseClass = object_getClass(self); 347 | NSString *className = NSStringFromClass(baseClass); 348 | 349 | // Already subclassed 350 | if ([className hasSuffix:AspectsSubclassSuffix]) { 351 | return baseClass; 352 | 353 | // We swizzle a class object, not a single object. 354 | } else if (class_isMetaClass(baseClass)) { 355 | return aspect_swizzleClassInPlace((Class)self); 356 | // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place. 357 | } else if (statedClass != baseClass) { 358 | return aspect_swizzleClassInPlace(baseClass); 359 | } 360 | 361 | // Default case. Create dynamic subclass. 362 | const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; 363 | Class subclass = objc_getClass(subclassName); 364 | 365 | if (subclass == nil) { 366 | subclass = objc_allocateClassPair(baseClass, subclassName, 0); 367 | if (subclass == nil) { 368 | NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; 369 | AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); 370 | return nil; 371 | } 372 | 373 | aspect_swizzleForwardInvocation(subclass); 374 | aspect_hookedGetClass(subclass, statedClass); 375 | aspect_hookedGetClass(object_getClass(subclass), statedClass); 376 | objc_registerClassPair(subclass); 377 | } 378 | 379 | object_setClass(self, subclass); 380 | return subclass; 381 | } 382 | 383 | static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; 384 | static void aspect_swizzleForwardInvocation(Class klass) { 385 | NSCParameterAssert(klass); 386 | // If there is no method, replace will act like class_addMethod. 387 | IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); 388 | if (originalImplementation) { 389 | class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); 390 | } 391 | AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); 392 | } 393 | 394 | static void aspect_undoSwizzleForwardInvocation(Class klass) { 395 | NSCParameterAssert(klass); 396 | Method originalMethod = class_getInstanceMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName)); 397 | Method objectMethod = class_getInstanceMethod(NSObject.class, @selector(forwardInvocation:)); 398 | // There is no class_removeMethod, so the best we can do is to retore the original implementation, or use a dummy. 399 | IMP originalImplementation = method_getImplementation(originalMethod ?: objectMethod); 400 | class_replaceMethod(klass, @selector(forwardInvocation:), originalImplementation, "v@:@"); 401 | 402 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(klass)); 403 | } 404 | 405 | static void aspect_hookedGetClass(Class class, Class statedClass) { 406 | NSCParameterAssert(class); 407 | NSCParameterAssert(statedClass); 408 | Method method = class_getInstanceMethod(class, @selector(class)); 409 | IMP newIMP = imp_implementationWithBlock(^(id self) { 410 | return statedClass; 411 | }); 412 | class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method)); 413 | } 414 | 415 | /////////////////////////////////////////////////////////////////////////////////////////// 416 | #pragma mark - Swizzle Class In Place 417 | 418 | static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) { 419 | static NSMutableSet *swizzledClasses; 420 | static dispatch_once_t pred; 421 | dispatch_once(&pred, ^{ 422 | swizzledClasses = [NSMutableSet new]; 423 | }); 424 | @synchronized(swizzledClasses) { 425 | block(swizzledClasses); 426 | } 427 | } 428 | 429 | static Class aspect_swizzleClassInPlace(Class klass) { 430 | NSCParameterAssert(klass); 431 | NSString *className = NSStringFromClass(klass); 432 | 433 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 434 | if (![swizzledClasses containsObject:className]) { 435 | aspect_swizzleForwardInvocation(klass); 436 | [swizzledClasses addObject:className]; 437 | } 438 | }); 439 | return klass; 440 | } 441 | 442 | static void aspect_undoSwizzleClassInPlace(Class klass) { 443 | NSCParameterAssert(klass); 444 | NSString *className = NSStringFromClass(klass); 445 | 446 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 447 | if ([swizzledClasses containsObject:className]) { 448 | aspect_undoSwizzleForwardInvocation(klass); 449 | [swizzledClasses removeObject:className]; 450 | } 451 | }); 452 | } 453 | 454 | /////////////////////////////////////////////////////////////////////////////////////////// 455 | #pragma mark - Aspect Invoke Point 456 | 457 | // This is a macro so we get a cleaner stack trace. 458 | #define aspect_invoke(aspects, info) \ 459 | for (AspectIdentifier *aspect in aspects) {\ 460 | [aspect invokeWithInfo:info];\ 461 | if (aspect.options & AspectOptionAutomaticRemoval) { \ 462 | aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \ 463 | } \ 464 | } 465 | 466 | // This is the swizzled forwardInvocation: method. 467 | static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { 468 | NSCParameterAssert(self); 469 | NSCParameterAssert(invocation); 470 | SEL originalSelector = invocation.selector; 471 | SEL aliasSelector = aspect_aliasForSelector(invocation.selector); 472 | invocation.selector = aliasSelector; 473 | AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector); 474 | AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector); 475 | AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation]; 476 | NSArray *aspectsToRemove = nil; 477 | 478 | // Before hooks. 479 | aspect_invoke(classContainer.beforeAspects, info); 480 | aspect_invoke(objectContainer.beforeAspects, info); 481 | 482 | // Instead hooks. 483 | BOOL respondsToAlias = YES; 484 | if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { 485 | aspect_invoke(classContainer.insteadAspects, info); 486 | aspect_invoke(objectContainer.insteadAspects, info); 487 | } else { 488 | Class klass = object_getClass(invocation.target); 489 | do { 490 | if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { 491 | [invocation invoke]; 492 | break; 493 | } 494 | }while (!respondsToAlias && (klass = class_getSuperclass(klass))); 495 | } 496 | 497 | // After hooks. 498 | aspect_invoke(classContainer.afterAspects, info); 499 | aspect_invoke(objectContainer.afterAspects, info); 500 | 501 | // If no hooks are installed, call original implementation (usually to throw an exception) 502 | if (!respondsToAlias) { 503 | invocation.selector = originalSelector; 504 | SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); 505 | if ([self respondsToSelector:originalForwardInvocationSEL]) { 506 | ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); 507 | } else { 508 | [self doesNotRecognizeSelector:invocation.selector]; 509 | } 510 | } 511 | 512 | // Remove any hooks that are queued for deregistration. 513 | [aspectsToRemove makeObjectsPerformSelector:@selector(remove)]; 514 | } 515 | #undef aspect_invoke 516 | 517 | /////////////////////////////////////////////////////////////////////////////////////////// 518 | #pragma mark - Aspect Container Management 519 | 520 | // Loads or creates the aspect container. 521 | static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) { 522 | NSCParameterAssert(self); 523 | SEL aliasSelector = aspect_aliasForSelector(selector); 524 | AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector); 525 | if (!aspectContainer) { 526 | aspectContainer = [AspectsContainer new]; 527 | objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN); 528 | } 529 | return aspectContainer; 530 | } 531 | 532 | static AspectsContainer *aspect_getContainerForClass(Class klass, SEL selector) { 533 | NSCParameterAssert(klass); 534 | AspectsContainer *classContainer = nil; 535 | do { 536 | classContainer = objc_getAssociatedObject(klass, selector); 537 | if (classContainer.hasAspects) break; 538 | }while ((klass = class_getSuperclass(klass))); 539 | 540 | return classContainer; 541 | } 542 | 543 | static void aspect_destroyContainerForObject(id self, SEL selector) { 544 | NSCParameterAssert(self); 545 | SEL aliasSelector = aspect_aliasForSelector(selector); 546 | objc_setAssociatedObject(self, aliasSelector, nil, OBJC_ASSOCIATION_RETAIN); 547 | } 548 | 549 | /////////////////////////////////////////////////////////////////////////////////////////// 550 | #pragma mark - Selector Blacklist Checking 551 | 552 | static NSMutableDictionary *aspect_getSwizzledClassesDict() { 553 | static NSMutableDictionary *swizzledClassesDict; 554 | static dispatch_once_t pred; 555 | dispatch_once(&pred, ^{ 556 | swizzledClassesDict = [NSMutableDictionary new]; 557 | }); 558 | return swizzledClassesDict; 559 | } 560 | 561 | static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError *__autoreleasing*error) { 562 | static NSSet *disallowedSelectorList; 563 | static dispatch_once_t pred; 564 | dispatch_once(&pred, ^{ 565 | disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil]; 566 | }); 567 | 568 | // Check against the blacklist. 569 | NSString *selectorName = NSStringFromSelector(selector); 570 | if ([disallowedSelectorList containsObject:selectorName]) { 571 | NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName]; 572 | AspectError(AspectErrorSelectorBlacklisted, errorDescription); 573 | return NO; 574 | } 575 | 576 | // Additional checks. 577 | AspectOptions position = options&AspectPositionFilter; 578 | if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) { 579 | NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc."; 580 | AspectError(AspectErrorSelectorDeallocPosition, errorDesc); 581 | return NO; 582 | } 583 | 584 | if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) { 585 | NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName]; 586 | AspectError(AspectErrorDoesNotRespondToSelector, errorDesc); 587 | return NO; 588 | } 589 | 590 | // Search for the current class and the class hierarchy IF we are modifying a class object 591 | if (class_isMetaClass(object_getClass(self))) { 592 | Class klass = [self class]; 593 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 594 | Class currentClass = [self class]; 595 | do { 596 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 597 | if ([tracker.selectorNames containsObject:selectorName]) { 598 | 599 | // Find the topmost class for the log. 600 | if (tracker.parentEntry) { 601 | AspectTracker *topmostEntry = tracker.parentEntry; 602 | while (topmostEntry.parentEntry) { 603 | topmostEntry = topmostEntry.parentEntry; 604 | } 605 | NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)]; 606 | AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); 607 | return NO; 608 | } else if (klass == currentClass) { 609 | // Already modified and topmost! 610 | return YES; 611 | } 612 | } 613 | }while ((currentClass = class_getSuperclass(currentClass))); 614 | 615 | // Add the selector as being modified. 616 | currentClass = klass; 617 | AspectTracker *parentTracker = nil; 618 | do { 619 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 620 | if (!tracker) { 621 | tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker]; 622 | swizzledClassesDict[(id)currentClass] = tracker; 623 | } 624 | [tracker.selectorNames addObject:selectorName]; 625 | // All superclasses get marked as having a subclass that is modified. 626 | parentTracker = tracker; 627 | }while ((currentClass = class_getSuperclass(currentClass))); 628 | } 629 | 630 | return YES; 631 | } 632 | 633 | static void aspect_deregisterTrackedSelector(id self, SEL selector) { 634 | if (!class_isMetaClass(object_getClass(self))) return; 635 | 636 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 637 | NSString *selectorName = NSStringFromSelector(selector); 638 | Class currentClass = [self class]; 639 | do { 640 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 641 | if (tracker) { 642 | [tracker.selectorNames removeObject:selectorName]; 643 | if (tracker.selectorNames.count == 0) { 644 | [swizzledClassesDict removeObjectForKey:tracker]; 645 | } 646 | } 647 | }while ((currentClass = class_getSuperclass(currentClass))); 648 | } 649 | 650 | @end 651 | 652 | @implementation AspectTracker 653 | 654 | - (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)parent { 655 | if ((self = [super init])) { 656 | _trackedClass = trackedClass; 657 | _parentEntry = parent; 658 | _selectorNames = [NSMutableSet new]; 659 | } 660 | return self; 661 | } 662 | - (NSString *)description { 663 | return [NSString stringWithFormat:@"<%@: %@, trackedClass: %@, selectorNames:%@, parent:%p>", self.class, self, NSStringFromClass(self.trackedClass), self.selectorNames, self.parentEntry]; 664 | } 665 | 666 | @end 667 | 668 | /////////////////////////////////////////////////////////////////////////////////////////// 669 | #pragma mark - NSInvocation (Aspects) 670 | 671 | @implementation NSInvocation (Aspects) 672 | 673 | // Thanks to the ReactiveCocoa team for providing a generic solution for this. 674 | - (id)aspect_argumentAtIndex:(NSUInteger)index { 675 | const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; 676 | // Skip const type qualifier. 677 | if (argType[0] == _C_CONST) argType++; 678 | 679 | #define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0) 680 | if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { 681 | __autoreleasing id returnObj; 682 | [self getArgument:&returnObj atIndex:(NSInteger)index]; 683 | return returnObj; 684 | } else if (strcmp(argType, @encode(SEL)) == 0) { 685 | SEL selector = 0; 686 | [self getArgument:&selector atIndex:(NSInteger)index]; 687 | return NSStringFromSelector(selector); 688 | } else if (strcmp(argType, @encode(Class)) == 0) { 689 | __autoreleasing Class theClass = Nil; 690 | [self getArgument:&theClass atIndex:(NSInteger)index]; 691 | return theClass; 692 | // Using this list will box the number with the appropriate constructor, instead of the generic NSValue. 693 | } else if (strcmp(argType, @encode(char)) == 0) { 694 | WRAP_AND_RETURN(char); 695 | } else if (strcmp(argType, @encode(int)) == 0) { 696 | WRAP_AND_RETURN(int); 697 | } else if (strcmp(argType, @encode(short)) == 0) { 698 | WRAP_AND_RETURN(short); 699 | } else if (strcmp(argType, @encode(long)) == 0) { 700 | WRAP_AND_RETURN(long); 701 | } else if (strcmp(argType, @encode(long long)) == 0) { 702 | WRAP_AND_RETURN(long long); 703 | } else if (strcmp(argType, @encode(unsigned char)) == 0) { 704 | WRAP_AND_RETURN(unsigned char); 705 | } else if (strcmp(argType, @encode(unsigned int)) == 0) { 706 | WRAP_AND_RETURN(unsigned int); 707 | } else if (strcmp(argType, @encode(unsigned short)) == 0) { 708 | WRAP_AND_RETURN(unsigned short); 709 | } else if (strcmp(argType, @encode(unsigned long)) == 0) { 710 | WRAP_AND_RETURN(unsigned long); 711 | } else if (strcmp(argType, @encode(unsigned long long)) == 0) { 712 | WRAP_AND_RETURN(unsigned long long); 713 | } else if (strcmp(argType, @encode(float)) == 0) { 714 | WRAP_AND_RETURN(float); 715 | } else if (strcmp(argType, @encode(double)) == 0) { 716 | WRAP_AND_RETURN(double); 717 | } else if (strcmp(argType, @encode(BOOL)) == 0) { 718 | WRAP_AND_RETURN(BOOL); 719 | } else if (strcmp(argType, @encode(bool)) == 0) { 720 | WRAP_AND_RETURN(BOOL); 721 | } else if (strcmp(argType, @encode(char *)) == 0) { 722 | WRAP_AND_RETURN(const char *); 723 | } else if (strcmp(argType, @encode(void (^)(void))) == 0) { 724 | __unsafe_unretained id block = nil; 725 | [self getArgument:&block atIndex:(NSInteger)index]; 726 | return [block copy]; 727 | } else { 728 | NSUInteger valueSize = 0; 729 | NSGetSizeAndAlignment(argType, &valueSize, NULL); 730 | 731 | unsigned char valueBytes[valueSize]; 732 | [self getArgument:valueBytes atIndex:(NSInteger)index]; 733 | 734 | return [NSValue valueWithBytes:valueBytes objCType:argType]; 735 | } 736 | return nil; 737 | #undef WRAP_AND_RETURN 738 | } 739 | 740 | - (NSArray *)aspects_arguments { 741 | NSMutableArray *argumentsArray = [NSMutableArray array]; 742 | for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) { 743 | [argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null]; 744 | } 745 | return [argumentsArray copy]; 746 | } 747 | 748 | @end 749 | 750 | /////////////////////////////////////////////////////////////////////////////////////////// 751 | #pragma mark - AspectIdentifier 752 | 753 | @implementation AspectIdentifier 754 | 755 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError *__autoreleasing*)error { 756 | NSCParameterAssert(block); 757 | NSCParameterAssert(selector); 758 | NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc. 759 | if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) { 760 | return nil; 761 | } 762 | 763 | AspectIdentifier *identifier = nil; 764 | if (blockSignature) { 765 | identifier = [AspectIdentifier new]; 766 | identifier.selector = selector; 767 | identifier.block = block; 768 | identifier.blockSignature = blockSignature; 769 | identifier.options = options; 770 | identifier.object = object; // weak 771 | } 772 | return identifier; 773 | } 774 | 775 | - (BOOL)invokeWithInfo:(id)info { 776 | NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature]; 777 | NSInvocation *originalInvocation = info.originalInvocation; 778 | NSUInteger numberOfArguments = self.blockSignature.numberOfArguments; 779 | 780 | // Be extra paranoid. We already check that on hook registration. 781 | if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) { 782 | AspectLogError(@"Block has too many arguments. Not calling %@", info); 783 | return NO; 784 | } 785 | 786 | // The `self` of the block will be the AspectInfo. Optional. 787 | if (numberOfArguments > 1) { 788 | [blockInvocation setArgument:&info atIndex:1]; 789 | } 790 | 791 | void *argBuf = NULL; 792 | for (NSUInteger idx = 2; idx < numberOfArguments; idx++) { 793 | const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx]; 794 | NSUInteger argSize; 795 | NSGetSizeAndAlignment(type, &argSize, NULL); 796 | 797 | if (!(argBuf = reallocf(argBuf, argSize))) { 798 | AspectLogError(@"Failed to allocate memory for block invocation."); 799 | return NO; 800 | } 801 | 802 | [originalInvocation getArgument:argBuf atIndex:idx]; 803 | [blockInvocation setArgument:argBuf atIndex:idx]; 804 | } 805 | 806 | [blockInvocation invokeWithTarget:self.block]; 807 | 808 | if (argBuf != NULL) { 809 | free(argBuf); 810 | } 811 | return YES; 812 | } 813 | 814 | - (NSString *)description { 815 | return [NSString stringWithFormat:@"<%@: %p, SEL:%@ object:%@ options:%tu block:%@ (#%tu args)>", self.class, self, NSStringFromSelector(self.selector), self.object, self.options, self.block, self.blockSignature.numberOfArguments]; 816 | } 817 | 818 | - (BOOL)remove { 819 | return aspect_remove(self, NULL); 820 | } 821 | 822 | @end 823 | 824 | /////////////////////////////////////////////////////////////////////////////////////////// 825 | #pragma mark - AspectsContainer 826 | 827 | @implementation AspectsContainer 828 | 829 | - (BOOL)hasAspects { 830 | return self.beforeAspects.count > 0 || self.insteadAspects.count > 0 || self.afterAspects.count > 0; 831 | } 832 | 833 | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options { 834 | NSParameterAssert(aspect); 835 | NSUInteger position = options&AspectPositionFilter; 836 | switch (position) { 837 | case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break; 838 | case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break; 839 | case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break; 840 | } 841 | } 842 | 843 | - (BOOL)removeAspect:(id)aspect { 844 | for (NSString *aspectArrayName in @[NSStringFromSelector(@selector(beforeAspects)), 845 | NSStringFromSelector(@selector(insteadAspects)), 846 | NSStringFromSelector(@selector(afterAspects))]) { 847 | NSArray *array = [self valueForKey:aspectArrayName]; 848 | NSUInteger index = [array indexOfObjectIdenticalTo:aspect]; 849 | if (array && index != NSNotFound) { 850 | NSMutableArray *newArray = [NSMutableArray arrayWithArray:array]; 851 | [newArray removeObjectAtIndex:index]; 852 | [self setValue:newArray forKey:aspectArrayName]; 853 | return YES; 854 | } 855 | } 856 | return NO; 857 | } 858 | 859 | - (NSString *)description { 860 | return [NSString stringWithFormat:@"<%@: %p, before:%@, instead:%@, after:%@>", self.class, self, self.beforeAspects, self.insteadAspects, self.afterAspects]; 861 | } 862 | 863 | @end 864 | 865 | /////////////////////////////////////////////////////////////////////////////////////////// 866 | #pragma mark - AspectInfo 867 | 868 | @implementation AspectInfo 869 | 870 | @synthesize arguments = _arguments; 871 | 872 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation { 873 | NSCParameterAssert(instance); 874 | NSCParameterAssert(invocation); 875 | if ((self = [super init])) { 876 | _instance = instance; 877 | _originalInvocation = invocation; 878 | } 879 | return self; 880 | } 881 | 882 | - (NSArray *)arguments { 883 | // Lazily evaluate arguments, boxing is expensive. 884 | if (!_arguments) { 885 | _arguments = self.originalInvocation.aspects_arguments; 886 | } 887 | return _arguments; 888 | } 889 | 890 | @end 891 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Peter Steinberger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PSTEnableUIKitDebugging.m: -------------------------------------------------------------------------------- 1 | // 2 | // PSTEnableUIKitDebugging.m 3 | // 4 | // Copyright (c) 2015 Peter Steinberger. Licensed under the MIT license. 5 | // 6 | 7 | #import "Aspects.h" 8 | #import "fishhook.h" 9 | #import 10 | 11 | // UIAnimationDragCoefficient 12 | // UISimulatedApplicationResizeGestureEnabled 13 | // UIExternalTouchSloppinessFactor 14 | // UIEnableParallaxEffects 15 | // UIDeviceUsesLowQualityGraphics 16 | 17 | // UIMotionEffectsEnabled 18 | // UIMotionEffectMotionUpdateFrequency 19 | // UIMotionEffectMotionUpdateSlowFrequency 20 | // UIMotionEffectMinimumBacklightLevel 21 | // UIMotionEffectHysteresisExitThreshold 22 | // UIMotionEffectHysteresisEntranceThreshold 23 | // UIMotionEffectUIUpdateFrequency 24 | // UIMotionEffectUIUpdateSlowFrequency 25 | 26 | // UIDocumentConsoleLogLevel 27 | // UIDocumentFileLogLevel 28 | // GestureFailureMapLogging 29 | // UIPopoverControllerForceAttemptsToAvoidKeyboard 30 | 31 | // _UISiriAnimationSpeed ... 32 | 33 | // UIBackdropViewNoBlur 34 | // UIBackdropViewNoComputedColorSettingsKey 35 | 36 | static NSDictionary *UIKitOverrides() { 37 | return @{@"UIPopoverControllerPaintsTargetRect" : @YES, // only works on iOS 7 38 | @"TouchLogging" : @YES, 39 | @"AnimationLogging" : @YES, 40 | @"UIUseAugmentedPopGesture" : @YES, 41 | @"UIKitDecorateFallbackImagesFromScale" : @YES, 42 | @"UIScreenEdgePanRecognizerShouldLog" : @YES, 43 | @"SystemGestureGateLogging" : @YES, 44 | @"GestureLogging" : @YES, 45 | @"GestureFailureMapLogging" : @YES, 46 | @"UIKeyboardTypingSpeedLogger" : @YES, 47 | @"UICatchCAPackageDecodingExceptions" : @YES 48 | }; 49 | } 50 | 51 | static BOOL (*GetBoolAnswer)(NSString *capability); 52 | static BOOL PSTGetBoolAnswer(NSString *capability) { 53 | return [capability isEqual:@"InternalBuild"] ? YES : GetBoolAnswer(capability); 54 | } 55 | 56 | __attribute__((constructor)) static void PSTEnableUIKitDebugMode() { 57 | // Enable Internal Build mode. 58 | GetBoolAnswer = dlsym(dlopen("/usr/lib/libMobileGestalt.dylib", RTLD_LAZY), "MGGetBoolAnswer"); 59 | rebind_symbols((struct rebinding[1]){{"MGGetBoolAnswer", PSTGetBoolAnswer}}, 1); 60 | 61 | // Install custom overrides. 62 | NSDictionary *overrides = UIKitOverrides(); 63 | 64 | [[NSUserDefaults standardUserDefaults] aspect_hookSelector:@selector(persistentDomainForName:) withOptions:0 usingBlock:^(id info, NSString *domainName) { 65 | if ([domainName hasSuffix:@"com.apple.UIKit"]) { 66 | __autoreleasing NSDictionary *dictionary; 67 | [[info originalInvocation] invoke]; 68 | [[info originalInvocation] getReturnValue:&dictionary]; 69 | NSMutableDictionary *mutable = [NSMutableDictionary dictionaryWithDictionary:dictionary]; 70 | [mutable addEntriesFromDictionary:overrides]; 71 | dictionary = [mutable copy]; 72 | [[info originalInvocation] setReturnValue:&dictionary]; 73 | } 74 | } error:NULL]; 75 | 76 | [NSUserDefaults aspect_hookSelector:@selector(objectForKey:) withOptions:0 usingBlock:^(id info, NSString *key) { 77 | if (overrides[key]) { 78 | __autoreleasing id value = overrides[key]; 79 | [[info originalInvocation] setReturnValue:&value]; 80 | } 81 | } error:NULL]; 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UIKitDebugging 2 | A set of files that enables various debug flags in UIKit 3 | 4 | See http://petersteinberger.com/blog/2015/uikit-debug-mode/ for details. 5 | 6 | MIT licensed. -------------------------------------------------------------------------------- /fishhook-LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /fishhook.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #import "fishhook.h" 25 | 26 | #import 27 | #import 28 | #import 29 | #import 30 | #import 31 | #import 32 | #import 33 | 34 | #ifdef __LP64__ 35 | typedef struct mach_header_64 mach_header_t; 36 | typedef struct segment_command_64 segment_command_t; 37 | typedef struct section_64 section_t; 38 | typedef struct nlist_64 nlist_t; 39 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64 40 | #else 41 | typedef struct mach_header mach_header_t; 42 | typedef struct segment_command segment_command_t; 43 | typedef struct section section_t; 44 | typedef struct nlist nlist_t; 45 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT 46 | #endif 47 | 48 | struct rebindings_entry { 49 | struct rebinding *rebindings; 50 | size_t rebindings_nel; 51 | struct rebindings_entry *next; 52 | }; 53 | 54 | static struct rebindings_entry *_rebindings_head; 55 | 56 | static int prepend_rebindings(struct rebindings_entry **rebindings_head, 57 | struct rebinding rebindings[], 58 | size_t nel) { 59 | struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry)); 60 | if (!new_entry) { 61 | return -1; 62 | } 63 | new_entry->rebindings = malloc(sizeof(struct rebinding) * nel); 64 | if (!new_entry->rebindings) { 65 | free(new_entry); 66 | return -1; 67 | } 68 | memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel); 69 | new_entry->rebindings_nel = nel; 70 | new_entry->next = *rebindings_head; 71 | *rebindings_head = new_entry; 72 | return 0; 73 | } 74 | 75 | static void perform_rebinding_with_section(struct rebindings_entry *rebindings, 76 | section_t *section, 77 | intptr_t slide, 78 | nlist_t *symtab, 79 | char *strtab, 80 | uint32_t *indirect_symtab) { 81 | uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; 82 | void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); 83 | for (uint i = 0; i < section->size / sizeof(void *); i++) { 84 | uint32_t symtab_index = indirect_symbol_indices[i]; 85 | if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || 86 | symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { 87 | continue; 88 | } 89 | uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; 90 | char *symbol_name = strtab + strtab_offset; 91 | struct rebindings_entry *cur = rebindings; 92 | while (cur) { 93 | for (uint j = 0; j < cur->rebindings_nel; j++) { 94 | if (strlen(symbol_name) > 1 && 95 | strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { 96 | indirect_symbol_bindings[i] = cur->rebindings[j].replacement; 97 | goto symbol_loop; 98 | } 99 | } 100 | cur = cur->next; 101 | } 102 | symbol_loop:; 103 | } 104 | } 105 | 106 | static void rebind_symbols_for_image(struct rebindings_entry *rebindings, 107 | const struct mach_header *header, 108 | intptr_t slide) { 109 | Dl_info info; 110 | if (dladdr(header, &info) == 0) { 111 | return; 112 | } 113 | uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); 114 | segment_command_t *cur_seg_cmd; 115 | segment_command_t *linkedit_segment = NULL; 116 | section_t *lazy_symbols = NULL; 117 | section_t *non_lazy_symbols = NULL; 118 | struct symtab_command* symtab_cmd = NULL; 119 | struct dysymtab_command* dysymtab_cmd = NULL; 120 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 121 | cur_seg_cmd = (segment_command_t *)cur; 122 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 123 | if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { 124 | linkedit_segment = cur_seg_cmd; 125 | continue; 126 | } 127 | if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0) { 128 | continue; 129 | } 130 | for (uint j = 0; j < cur_seg_cmd->nsects; j++) { 131 | section_t *sect = 132 | (section_t *)(cur + sizeof(segment_command_t)) + j; 133 | if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) { 134 | lazy_symbols = sect; 135 | } 136 | if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) { 137 | non_lazy_symbols = sect; 138 | } 139 | } 140 | } else if (cur_seg_cmd->cmd == LC_SYMTAB) { 141 | symtab_cmd = (struct symtab_command*)cur_seg_cmd; 142 | } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { 143 | dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; 144 | } 145 | } 146 | if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || 147 | !dysymtab_cmd->nindirectsyms) { 148 | return; 149 | } 150 | // Find base symbol/string table addresses 151 | uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; 152 | nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); 153 | char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); 154 | // Get indirect symbol table (array of uint32_t indices into symbol table) 155 | uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); 156 | if (lazy_symbols) { 157 | perform_rebinding_with_section(rebindings, lazy_symbols, slide, symtab, strtab, indirect_symtab); 158 | } 159 | if (non_lazy_symbols) { 160 | perform_rebinding_with_section(rebindings, non_lazy_symbols, slide, symtab, strtab, indirect_symtab); 161 | } 162 | } 163 | 164 | static void _rebind_symbols_for_image(const struct mach_header *header, 165 | intptr_t slide) { 166 | rebind_symbols_for_image(_rebindings_head, header, slide); 167 | } 168 | 169 | int rebind_symbols_image(void *header, 170 | intptr_t slide, 171 | struct rebinding rebindings[], 172 | size_t rebindings_nel) { 173 | struct rebindings_entry *rebindings_head = NULL; 174 | int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel); 175 | rebind_symbols_for_image(rebindings_head, header, slide); 176 | free(rebindings_head); 177 | return retval; 178 | } 179 | 180 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { 181 | int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel); 182 | if (retval < 0) { 183 | return retval; 184 | } 185 | // If this was the first call, register callback for image additions (which is also invoked for 186 | // existing images, otherwise, just run on existing images 187 | if (!_rebindings_head->next) { 188 | _dyld_register_func_for_add_image(_rebind_symbols_for_image); 189 | } else { 190 | uint32_t c = _dyld_image_count(); 191 | for (uint32_t i = 0; i < c; i++) { 192 | _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); 193 | } 194 | } 195 | return retval; 196 | } -------------------------------------------------------------------------------- /fishhook.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #ifndef fishhook_h 25 | #define fishhook_h 26 | 27 | #include 28 | #include 29 | 30 | #ifdef __cplusplus 31 | extern "C" { 32 | #endif //__cplusplus 33 | 34 | /* 35 | * A structure representing a particular intended rebinding from a symbol 36 | * name to its replacement 37 | */ 38 | struct rebinding { 39 | char *name; 40 | void *replacement; 41 | }; 42 | 43 | /* 44 | * For each rebinding in rebindings, rebinds references to external, indirect 45 | * symbols with the specified name to instead point at replacement for each 46 | * image in the calling process as well as for all future images that are loaded 47 | * by the process. If rebind_functions is called more than once, the symbols to 48 | * rebind are added to the existing list of rebindings, and if a given symbol 49 | * is rebound more than once, the later rebinding will take precedence. 50 | */ 51 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); 52 | 53 | /* 54 | * Rebinds as above, but only in the specified image. The header should point 55 | * to the mach-o header, the slide should be the slide offset. Others as above. 56 | */ 57 | int rebind_symbols_image(void *header, 58 | intptr_t slide, 59 | struct rebinding rebindings[], 60 | size_t rebindings_nel); 61 | 62 | #ifdef __cplusplus 63 | } 64 | #endif //__cplusplus 65 | 66 | #endif //fishhook_h 67 | 68 | --------------------------------------------------------------------------------