NSCountedSet - 由取图片主色引发的思考

2020-06-06  本文已影响0人  斯瑞德

0.背景

在 iOS 中某场景中需要查询主色,取主色时发现当图片比较大时,会比较耗时,经排查发现了 NSCountedSet 用法上的一些问题。以下是取主色的部分代码:

   NSCountedSet *colorSet = [NSCountedSet setWithCapacity:row*column];
   for (int x=0; x<row; x++) {
       for (int y=0; y<column; y++) {
           int red, green, blue, alpha;
           ··· ···
           NSArray *color = @[@(red),@(green),@(blue),@(alpha)];
           [colorSet addObject:color];     
        }
    }

1.分析

首先分析下取主色的方法:遍历矩形的图片像素点,将所有的像素取出来,然后计算其中出现最多的像素。

最少需要一次全遍历,时间复杂度 O(m*n) (m=row, n=column) 。计数的算法,首先想到的是进行哈希,哈希时间复杂度可以近似认为是 0(1)。比如题中使用的 NSCountedSet ,底层实现就是哈希。

进一步分析发现,耗时主要是 [colorSet addObject:color] 这行代码引入的。

2.NSCountedSet 使用测试

接下来分析到了 [colorSet addObject:color] 这个插入方法的实现、复杂度、使用等。将 NSCountedSet 看做是是黑盒的进行测试:

① 将普通对象作为Key

测试的类为Foo,实现如下:

// Foo Interface
@interface Foo : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
// Foo Implementation
@implementation Foo

- (NSUInteger)hash {
    NSUInteger result = [super hash];
    printf("%s - %s - %lu\n", NSStringFromSelector(_cmd).UTF8String, _name.UTF8String, result);
//    NSLog(@"%@ - %lu", NSStringFromSelector(_cmd), _index);
    return result;
}

- (BOOL)isEqual:(id)object {
    printf("*******%s - %s\n", NSStringFromSelector(_cmd).UTF8String, _name.UTF8String);
//    NSLog(@"*******%@ - [1] - %lu - %lu", NSStringFromSelector(_cmd), ((Foo *)object).index, _index);
    if (![object isKindOfClass:[Foo class]]) {
        return NO;
    }
//    NSLog(@"*******%@ - [2]", NSStringFromSelector(_cmd));
    BOOL result = [((Foo *)object).name isEqualToString:_name];
    if (result) {
//        NSLog(@"*******%@ - [3] - %lu √", NSStringFromSelector(_cmd), _index);
    }
    return result;
}

- (NSString *)description {
    return [NSString stringWithFormat:@"%@", _name];
}

@end

测试代码:

        NSUInteger count = 5;
        NSCountedSet *countedSet = [NSCountedSet setWithCapacity:count];
        for (NSUInteger i = 0; i<count; i++) {
            Foo *temp = [[Foo alloc] init];
            temp.name = [NSString stringWithFormat:@"name_%u_name", arc4random()%2];
            temp.age = arc4random()%100;
            [countedSet addObject:temp];
        }
        
        NSLog(@"========================================");
        [countedSet enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
            printf("%s - %lu\n", [obj description].UTF8String, (unsigned long)[countedSet countForObject:obj]);
        }];

测试结果,每次 addObject 都会调用 hash 方法,前几次调用不会调用 isEqual 方法,之后每次都会调用 isEqual 方法。计数是 hash 结果 + isEqual 综合的。

② 将NSArray实例作为Key

测试发现,NSCountedSet 的元素为数组时,会对数组的每个元素分别比较,所有元素都相同,才会计数。

数组的元素为基本数据类型时会直接比较,如果是对象仍会使用 isEqual 方法比较。

③ 将字典实例作为Key

测试发现,NSCountedSet 的元素为字典时,会对字典的每个键值对分别比较,所有元素都相同,才会计数。

3.NSCountedSet 的实现

虽然没有完整的实现,但是在 GNUStep 里还是能看到部分实现的,主要是以下两个文件:

NSCountedSet : https://github.com/gnustep/libs-base/blob/master/Source/NSCountedSet.m
GSCountedSet : https://github.com/gnustep/libs-base/blob/master/Source/GSCountedSet.m

其中计数部分的注释可以佐证,计数会使用 isEqual 进行比较:

/**
 * Returns the number of times that an object that is equal to the
 * specified object (as determined by the [-isEqual:] method) has
 * been added to the set and not removed from it.
 */

关键实现:对新添加的元素进行哈希,取值如果存在则计数加1,否则添加哈希并赋值为1,代码如下:

/**
 * Adds an object to the set.  If the set already contains an object
 * equal to the specified object (as determined by the [-isEqual:]
 * method) then the count for that object is incremented rather
 * than the new object being added.
 */
- (void) addObject: (id)anObject
{
  GSIMapNode node;

  if (anObject == nil)
    {
      [NSException raise: NSInvalidArgumentException
          format: @"Tried to nil value to counted set"];
    }

  _version++;
  node = GSIMapNodeForKey(&map, (GSIMapKey)anObject);
  if (node == 0)
    {
      GSIMapAddPair(&map,(GSIMapKey)anObject,(GSIMapVal)(NSUInteger)1);
    }
  else
    {
      node->value.nsu++;
    }
  _version++;
}

根据以上代码及测试,对于对象作为key的实现推测:

4.结论

本文中的例子:

原因:

解决办法:

NSCountedSet 使用的注意事项:

最终的测试代码:

    NSCountedSet *countedSet = [NSCountedSet new];
    
    NSLog(@"start");
    for (NSInteger i = 0; i<400000; i++) {
        int red   = arc4random()%10+225;
        int green = arc4random()%10+100;
        int blue  = arc4random()%10;
        int alpha = 1.0;
//        NSArray *color = @[@(red),@(green),@(blue),@(alpha)];
        NSString *color = [NSString stringWithFormat:@"%d,%d,%d,%d",red, green, blue, alpha];
//        NSNumber *color = @((red<<16)+(green<<8)+blue);
        [countedSet addObject:color];
    }
    NSLog(@"ending");
    __block NSUInteger count = 0;
    [countedSet enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
        count += [countedSet countForObject:obj];
    }];
    NSLog(@"end");

以 NSString 作为元素耗时 1s 为基准,其他情况耗时如下:

元素 耗时
NSArray 19.0s
NSString 1.0s
NSNumber 400ms

如果将 NSCountedSet 替换为 NSMutableDictionary ,耗时为 1.1s 。可以看到优化后效率提升了约50倍。

上一篇下一篇

猜你喜欢

热点阅读