Search code examples
iosobjective-c64-bitnsnumberobjective-c-category

How to create a category with a property for NSNumber objects


For various reasons, I need to associate a default number format (a NSNumberFormatter) with NSNumber objects. I want to support this even for objects created outside my control, hence rather than create a sub-class of NSNumber, I have used a category and the ObjC associated object functionality to achieve this:

@interface NSNumber (defaultNumberFormat)
@property (nonatomic,strong) NSNumberFormatter *defaultNumberFormat;
@end

@implementation NSNumber (defaultNumberFormat)

@dynamic defaultNumberFormat;

- (void)setDefaultNumberFormat:(NSNumberFormatter *)format {
    [self willChangeValueForKey:@"defaultNumberFormat"];
    objc_setAssociatedObject(self, @selector(defaultNumberFormat), format, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self didChangeValueForKey:@"defaultNumberFormat"];
}

- (NSNumberFormatter *)defaultNumberFormat {
    return objc_getAssociatedObject(self, @selector(defaultNumberFormat));
}
@end

This works well under a 32 bit compile, however under iOS7 for 64 bit targets, for some values it crashes with EXC_BAD_ACCESS (code=EXC_I386_GPFLT) at the obj_setAssociatedObject call.

It turns out that the reason is iOS is using tagged pointers for 64 bit targets for selected objects with small values to improve performance (avoids the need for memory allocation and ARC cleanup for the object). This includes some NSString, NSNumber and NSDate values. See more information here and here.

So how do you achieve a category with a property for NSNumber under 64 bit mode?


Solution

  • My first attempt to solve this tried to manage the association manually by exception when a tagged pointer object was detected. This works, but was not a practical solution, since you loose all the benefits of ARC for the associated value (they are not cleaned up when the tagged pointer object is no longer in use).

    Instead, a viable solution makes the property readonly, then uses a separate accessor to transform the tagged pointer object into a normal object before applying the associated object:

    @interface NSNumber (defaultNumberFormat)
    @property (nonatomic,strong,readonly) NSNumberFormatter *defaultNumberFormat;
    - (NSNumber *)applyDefaultNumberFormat:(NSNumberFormatter *)format;
    @end
    
    @implementation NSNumber (defaultNumberFormat)
    
    @dynamic defaultNumberFormat;
    
    - (NSNumber *)applyDefaultNumberFormat:(NSNumberFormatter *)format {
        NSNumber *newNumber = self;
    #ifdef WORKAROUND_IOS_TAGGED_POINTER_ISSUE
        unsigned long ptrValue = (unsigned long)self;
        if (ptrValue & 0x1) {
            // We have a non-aligned pointer - ie a tagged short-cut object stored inside the pointer.  objc_setAssociatedObject() is broken for these object types (it will cause memory access faults).
            // Transform ourselves into a non-tagged object.
            newNumber = [NSDecimalNumber decimalNumberWithDecimal:[self decimalValue]];
            ptrValue = (unsigned long)newNumber;
            if (ptrValue & 0x1 || ![newNumber isKindOfClass:[NSNumber class]]) {
                NSLog(@"Failed to create a non-tagged NSNumber for number: %@ - hence default number format not set", [self description]);
                return self;
            }
        }
    #endif  
        [newNumber willChangeValueForKey:@"defaultNumberFormat"];
        objc_setAssociatedObject(newNumber, @selector(defaultNumberFormat), format, OBJC_ASSOCIATION_COPY_NONATOMIC);
        [newNumber didChangeValueForKey:@"defaultNumberFormat"];
        return newNumber;
    }       
    
    - (NSNumberFormatter *)defaultNumberFormat {
    #ifdef WORKAROUND_IOS_TAGGED_POINTER_ISSUE
        unsigned long ptrValue = (unsigned long)self;
        if (ptrValue & 0x1)
            return nil;
    #endif
        return objc_getAssociatedObject(self, @selector(defaultNumberFormat));
    }
    
    @end