4

Today I was working on a project in which I wanted to "alias" an alternative method for all instances of NSArray, and didn't think it would be too difficult with some good old-fashioned method swizzling.

I broke out JRSwizzle and…

[NSArray jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(objectAtIndex_accordingToMe:) error:nil];

To be clear, I paired this with the appropriate category on NSArray, an instance method called objectAtIndex_accordingToMe:.

However, I was just getting that same old object, at that same old index. Sigh. Ultimately, I figured out that despite not throwing any errors - I'm not going to achieve these results due to the fact that NSArray is a class cluster

I guess my question is more of an unwillingness to accept that "this" is really the end of the road trying to override NSArray methods. I mean, come on this is NSArray.. people must wanna muck around with it, no? One would think that Apple's foundation classes would be a prime target for swizzlers, everywhere!

So, is there a way to alter, alias, monkey-patch, override, or otherwise have your way with… an NSArray, etc. (without subclassing)?

Community
  • 1
  • 1
Alex Gray
  • 16,007
  • 9
  • 96
  • 118
  • 1
    What are you trying to accomplish? – NSResponder Aug 01 '12 at 01:45
  • 2
    Just a thought, but I've used the objective C runtime to swizzle in code before. At that lower level you can grab the class of a returned array and swizzle it directly rather than trying to use a category. Then the actual implementation class should not matter. – drekka Aug 01 '12 at 01:50
  • @NSResponder I often use a method `objectAtNormalizedIndex`, which acts like a sort of circular queue on NSArray - and I wanted to implement it here, but without a million changes to the existing code. I never bothered with swizzling before, so I thought I'd see if I could make it happen, but I guess maybe there's a reason Apple seems to hate all things swizzle-related (SIMBL, etc). – Alex Gray Aug 01 '12 at 02:01
  • 1
    Swizzling basic system classes will cause issues. Frameworks rely in a certain functionality, swizzling NSArray will break these assumptions and lead to extremely hard to reproduce bugs. Even if you could do this, please don't. – steipete Feb 28 '19 at 08:48

2 Answers2

4

It's not just that it's a class cluster. NSArray is toll-free bridged to CFArray, and you can't swizzle Core Foundation. So this is very unlikely to work in general.

But what are you trying to solve? If you want to add a new method, use a category. They work on class clusters just fine. Modifying the behavior of some built-in on NSArray seems a recipe for disaster (entertaining as it might be as an exercise).

Before going too far, you probably want to at least take a look at CFArray.c and understand how some of the underlying stuff is implemented.


EDIT: While I would never do this in production code, you may get some of what you want by hijacking individual array instances with ISA-swizzling. See ISASwizzle for some example code. The code explanation is in Chapter 20 of iOS:PTL. Search out for "isa swizzle" and you should find more on the net. It's how KVO is implemented. But with NSArray... wow, that's gotta be fragile.

jscs
  • 63,694
  • 13
  • 151
  • 195
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
3

Presumably you have a particular array for which you'd like this behavior. You can get that instance's class object, no matter what it is, and swizzle that quite easily:

[[myArray class] jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(objectAtIndex_accordingToMe:) error:nil];

There's also only a few concrete subclasses of NSArray:

NSArray * trees = [NSArray array];
NSArray * birds = [NSArray arrayWithObjects:@"Albatross", @"Budgerigar", @"Cardinal", nil];
NSMutableArray * dogs = [NSMutableArray arrayWithObjects:@"Airedale", @"Beagle", @"Collie", nil];

NSLog(@"%@ %@ %@", [trees class], [birds class], [dogs class]);

We get __NSArrayI for the first two and __NSArrayM for the third, so potentially (this is very fragile) you could use a runtime function to grab the class object by name:

[objc_getClass("__NSArrayI") jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(objectAtIndex_accordingToMe:) error:nil];
jscs
  • 63,694
  • 13
  • 151
  • 195
  • could you just do `[objc_getClass(NSStringFromClass([myArray class])) jr_swizzleMethod…`? – Alex Gray Aug 01 '12 at 02:10
  • No, because `objc_getClass()` takes a C string, not an `NSString`; it's unnecessary anyways, because `[myArray class]` will give you the same result. – jscs Aug 01 '12 at 02:12
  • How about `[objc_getClass([NSStringFromClass([myArray class])) UTF8String] …`, lol. Not being pedantic.. I just try not to be cavalier / moronic about introducing points of fragility. Oh wait, I forgot these aren't getting triggered when called.. but are pre-"registered". Nevermind.. I see why that wouldn't work.. – Alex Gray Aug 01 '12 at 02:18
  • 1
    Yes, that works, but again, you don't need to do the converting to string and back to a class object. You already have the class object: `[myArray class] == objc_getClass([NSStringFromClass([myArray class])) UTFString])`, so just use the first option. The only reason I included the second is in case you (or a future reader) wanted to try swizzling _all_ the concrete subclasses they could find. – jscs Aug 01 '12 at 02:22
  • OK.. but once `[trees class]` has reported itself to be `__NSArrayI`, is that guaranteed for it's lifetime.. Or might the backend implementation change, without notice? And I guess I had been imagining this would be able to work with all / any instances within a scope (amplifying such type-uncertainty, considerably). – Alex Gray Aug 01 '12 at 02:27
  • It is possible in principle to change an object's class; admittedly occurrences of this are incredibly rare (I seem to recall that `NSMutableString` does an `NSPlaceholderString`/"real" class swap, but I may be wrong). – jscs Aug 01 '12 at 02:39
  • 1
    Perhaps it's worth noting that a class can lie about the type of its instances by overriding `-[NSObject class]`, and the classes created by KVO at runtime do override `-[NSObject class]` to hide their existence. But `object_getClass` (different from `objc_getClass`!) always tells the truth. – rob mayoff Feb 28 '19 at 21:32