如何自定义NSArray的描述?

How to customise description of NSArray?

我想为 NSArray class 的 description 方法提供我自己的实现,这样我就可以像这样简单地使用它:

NSLog(@"%@", @[]);

我的想法是为 NSArray 提供一个类别并简单地覆盖那里的 description 方法。但是它不起作用,因为 NSArray 是一个 class 集群,而它真正的 class 是 __NSArrayI,所以我的类别实现从未被调用。很遗憾,我无法为 __NSArrayI 提供类别,因为此 class 不可用。

当然,我可以只使用 subclass NSArray 并在我的 subclass 中实现这个方法,但是同样,因为 NSArray 是一个 class cluster 我必须为一堆不同的方法提供一个实现,比如 objectAtIndex: 我不想这样做,因为这对于简单地改变数组打印到控制台的方式来说工作太多了。

伙计们,你们有什么想法吗?谢谢

Do you have any ideas, guys? Thanks

我们有想法。解决方案......你必须决定。

documentation about format specifiers开始,你不能只担心description。具体来说,从该文件...

Objective-C object, printed as the string returned by descriptionWithLocale: if available, or description otherwise. Also works with CFTypeRef objects, returning the result of the CFCopyDescription function.

除非我们想破解链接器 and/or 动态加载器,否则我们对 CFTypeRef 对象无能为力。

然而,我们可以对 descriptiondescriptionWithLocale: 做一些 的事情,虽然这有点粗糙。

您可能还想考虑 debugDescription

这是实现目标的一种方法,尽管我认为它 "educational",您应该根据自己的最佳判断来决定是否要走这条路。

首先,您需要确定替代 description 实施的外观。我们将像这样声明 description 的简单替换(忽略原始实现)。

static NSString * swizzledDescription(id self, SEL _cmd)
{
    NSUInteger count = [self count];
    NSMutableString *result = [NSMutableString stringWithFormat:@"Array instance (%p) of type %@ with %lu elements", (void*)self, NSStringFromClass([self class]), (unsigned long)count];
    int fmtLen = snprintf(0,0,"%lu",count);
    for (NSUInteger i = 0; i < count; ++i) {
        [result appendFormat:@"\n%p: %*lu: %@", (void*)self, fmtLen, i, self[i]];
    }
    return result;
}

还有一个更简单的 descriptionWithLocale: 实现,完全忽略了语言环境。

static NSString * swizzledDescriptionWithLocale(id self, SEL _cmd, id locale) {
    return swizzledDescription(self, _cmd);
}

现在,我们如何让 NSArray 实现使用此代码?一种方法是找到 NSArray 的所有子 classes 并替换它们的方法...

static void swizzleMethod(Class class, SEL selector, IMP newImp) {
    Method method = class_getInstanceMethod(class, selector);
    if (method) {
        IMP origImp = method_getImplementation(method);
        if (origImp != newImp) {
            method_setImplementation(method, newImp);
        }
    }
}

static void swizzleArrayDescriptions() {
    int numClasses = objc_getClassList(NULL, 0);
    if (numClasses <= 0) return;
    Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    Class target = [NSArray class];
    for (int i = 0; i < numClasses; i++) {
        for (Class c = classes[i]; c; c = class_getSuperclass(c)) {
            if (c == target) {
                c = classes[i];
                swizzleMethod(c, @selector(description), (IMP)swizzledDescription);
                swizzleMethod(c, @selector(descriptionWithLocale:), (IMP)swizzledDescriptionWithLocale);
                break;
            }
        }
    }
    free(classes);
}

调用 swizzleArrayDescriptions 的合理位置是在您的应用委托的 +initialize 方法中。

@implementation AppDelegate

+ (void)initialize {
    if (self == [AppDelegate class]) {
        swizzleArrayDescriptions();
    }
}

现在,你应该可以和它一起玩了,看看你们相处得怎么样。

作为一个非常简单的测试...

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    NSArray *array = @[@"One", @"Two", @3, @"4", @"FIVE", @(6.0), @".7.", @8, @9, @10, @"Eleven" ];
    NSLog(@"%@", array);
    NSLog(@"%@", [array mutableCopy]);
}

产生这个输出...

2015-11-08 14:30:45.501 TestApp[72183:25861219] Array instance (0x6000000c5780) of type __NSArrayI with 11 elements
0x6000000c5780:  0: One
0x6000000c5780:  1: Two
0x6000000c5780:  2: 3
0x6000000c5780:  3: 4
0x6000000c5780:  4: FIVE
0x6000000c5780:  5: 6
0x6000000c5780:  6: .7.
0x6000000c5780:  7: 8
0x6000000c5780:  8: 9
0x6000000c5780:  9: 10
0x6000000c5780: 10: Eleven
2015-11-08 14:30:45.501 TestApp[72183:25861219] Array instance (0x600000045580) of type __NSArrayM with 11 elements
0x600000045580:  0: One
0x600000045580:  1: Two
0x600000045580:  2: 3
0x600000045580:  3: 4
0x600000045580:  4: FIVE
0x600000045580:  5: 6
0x600000045580:  6: .7.
0x600000045580:  7: 8
0x600000045580:  8: 9
0x600000045580:  9: 10
0x600000045580: 10: Eleven

当然,你应该比我做更多的测试,因为我所做的只是在教堂之后把它搞砸了,因为它看起来有点有趣(下雨所以野餐被取消了)。

如果需要调用原始实现,则需要创建原始实现的缓存,以 class 为键,并相应地调用它们。但是,对于像这种情况下您想要修改返回的字符串,您可能不需要这样做,并且无论如何都应该是直接的。

此外,请注意关于 swizzling 的一般注意事项,在使用 class 集群时它们会更加严重。

注意,你也可以做类似的事情来在运行时创建自定义子classes。您甚至可以以正常方式将子 class 定义为 NSArray 的直接子 class ,然后在不影响任何 class 的情况下调整它们的类型不是你的……或者一堆不同的东西……记住 ObjectiveC 运行时是你的朋友。