1

Is it possible to swizzle the addObject: method of NSMutableArray?

Here is what I am trying.

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@implementation NSMutableArray (LoggingAddObject)


+ (void)load {
Method addObject = class_getInstanceMethod(self, @selector(addObject:));
Method logAddObject = class_getInstanceMethod(self, @selector(logAddObject:));
method_exchangeImplementations(addObject, logAddObject);

Method original = class_getInstanceMethod(self, @selector(setObject:atIndexedSubscript:));
Method swizzled = class_getInstanceMethod(self, @selector(swizzled_setObject:atIndexedSubscript:));
method_exchangeImplementations(original, swizzled);
}


- (void)logAddObject:(id)anObject {
[self logAddObject:anObject];
NSLog(@"Added object %@ to array %@", anObject, self);
}

-(void)swizzled_setObject:(id)obj atIndexedSubscript:(NSUInteger)idx
{
NSLog(@"This gets called as expected!!-----");
[self swizzled_setObject:obj atIndexedSubscript:idx];  
}

I am able to swizzle some of the methods like setObject:atIndexedSubscript: but I am worried that I cant do it do the addObject: and others. I think the below can not be swizzled? Can someone explain why ? what I am doing wrong and or a way around this?

/****************   Mutable Array       ****************/

@interface NSMutableArray : NSArray

- (void)addObject:(id)anObject;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeLastObject;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;

@end
glogic
  • 4,042
  • 3
  • 28
  • 44
  • Just try with class `Class class = [self class];`, instead of self. Because self refer to object. Class will contain info about meta class(isa pointer). ex `Method addObject = class_getInstanceMethod(class, @selector(addObject:));` – Mani May 17 '14 at 10:32
  • 5
    `NSMutableArray` is the abstract base class of a class cluster. No objects you receive are ever instances of `NSMutableArray` itself. They are instances of private subclasses. Those subclasses have to provide their own implementation of `-addObject:`. You are not affecting those. The non-primitive methods of `NSMutableArray` would be implemented in terms of the primitive methods, so they may work. Or not, if a given subclass overrides them and doesn't call through to the base class. – Ken Thomases May 17 '14 at 10:38
  • 2
    Is there are concrete purpose about it? or are you just playing around? Maybe there is another solution that doesn't involve swizzling – Andrea May 17 '14 at 10:42
  • @Andrea Well I want to do a nil check before trying to add objects to an array to avoid crashes. Was hoping to add the check in one place before actually adding. I understand that the problem of trying to add nil object exists further up but was just looking for a way to avoid crashes if someone forgot to do nil check before hand – glogic May 17 '14 at 11:01
  • To solve this you need to check to see if the class you're swizzling on actually provides the method you're swizzling and, if not, you need to descend into its parent class(es) until you find it. Then you swizzle at that level. – mah May 17 '14 at 11:09
  • @mah How do I check this within the + (void)load method? – glogic May 17 '14 at 11:32
  • 2
    NS(Mumble)Array and NS(Mumble)Dictionary are especially tricky to extend in any form since there are actually multiple classes for each, depending on which particular constructor was used to create the object. If you read the sections in their docs about subclassing you'll get some feel for the situation. – Hot Licks May 17 '14 at 12:00
  • @HotLicks NSMumbleArray.... thats tricky to use – Bryan Chen May 17 '14 at 12:11
  • @BryanChen -- Yeah, you can never quite tell what you've got. – Hot Licks May 17 '14 at 12:12
  • @glogic why don't you use a proxy object that forwards method to the nsmutablearray? – Andrea May 17 '14 at 12:59

3 Answers3

5

You can try this with NSProxy, but I don't suggest you to use it on production code because:

  • it will break something (some framework may require NSMutableArray to throw exception when add nil into it to prevent more serious error later. i.e. Fail fast)
  • it is slow

If you really want to avoid nil checking, I suggest you to make a subclass of NSMutableArray and use it everywhere in your code. But really? There are so many ObjC code using NSMutableArray, most of them doesn't need this feature. So why you are so special?

#import <objc/runtime.h>

@interface XLCProxy : NSProxy

+ (id)proxyWithObject:(id)obj;

@end

@implementation XLCProxy
{
    id _obj;
}

+ (void)load
{
    Method method = class_getClassMethod([NSMutableArray class], @selector(allocWithZone:));
    IMP originalImp = method_getImplementation(method);

    IMP imp = imp_implementationWithBlock(^id(id me, NSZone * zone) {
        id obj = ((id (*)(id,SEL,NSZone *))originalImp)(me, @selector(allocWithZone:), zone);
        return [XLCProxy proxyWithObject:obj];
    });

    method_setImplementation(method, imp);
}

+ (id)proxyWithObject:(id)obj
{
    XLCProxy *proxy = [self alloc];
    proxy->_obj = obj;
    return proxy;
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation setTarget:_obj];
    [invocation invoke];
    const char *selname = sel_getName([invocation selector]);
    if ([@(selname) hasPrefix:@"init"] && [[invocation methodSignature] methodReturnType][0] == '@') {
        const void * ret;
        [invocation getReturnValue:&ret];
        ret = CFBridgingRetain([XLCProxy proxyWithObject:CFBridgingRelease(ret)]);
        [invocation setReturnValue:&ret];
    }
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [_obj methodSignatureForSelector:sel];
}

- (Class)class
{
    return [_obj class];
}

- (void)addObject:(id)obj
{
    [_obj addObject:obj ?: [NSNull null]];
}

- (BOOL)isEqual:(id)object
{
    return [_obj isEqual:object];
}

- (NSUInteger)hash {
    return [_obj hash];
}

// you can add more methods to "override" methods in `NSMutableArray` 

@end

@interface NSMutableArrayTests : XCTestCase

@end

@implementation NSMutableArrayTests

- (void)testExample
{
    NSMutableArray *array = [NSMutableArray array];
    [array addObject:nil];
    [array addObject:@1];
    [array addObject:nil];
    XCTAssertEqualObjects(array, (@[[NSNull null], @1, [NSNull null]]));
}

@end
Bryan Chen
  • 45,816
  • 18
  • 112
  • 143
  • Yeah you are correct it is such a bad idea, I wasnt thinking when I was attempting this! Its way too dangerous but your sample is great and given me a lot to think on. Voting as correct answer as it is the one that talked me out of doing this :) – glogic May 17 '14 at 16:51
1

You can iterate over all registered classes, check if current class is a subclass of NSMutableArray, if so, swizzle.

I would advice against it, rather act on case-by-case basis to have more predictable behavior - you never know which other system frameworks rely in this particular behavior (e.g. I can see how CoreData might rely on this particular behavior)

Sash Zats
  • 5,376
  • 2
  • 28
  • 42
1

You can swizzle any NSMutableArray method in the following way:

@implementation NSMutableArray (Swizzled)

+ (void)load
{
    Method orig = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), NSSelectorFromString(@"addObject:"));
    Method override = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(addObject_override:));

    method_exchangeImplementations(orig, override);
}

- (void)addObject_override:(id)anObject
{
   [self addObject_override:anObject];

   NSLog(@"addObject called!");
}
Tarek
  • 2,372
  • 2
  • 23
  • 35