美文网首页
NSCountedSet - 由取图片主色引发的思考

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

作者: 斯瑞德 | 来源:发表于2020-06-06 11:40 被阅读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的实现推测:

    • 相同类的实例对象会随机生成1~N中不同的hash结果;
    • 获取 hash 值后调用 isEqual 方法判断是否和已有的 key 相同:
      - 如果相同,则计数+1;
      - 如果都不相同,则为该 hash 值增加一个新的 key;

    4.结论

    本文中的例子:

    原因:

    • 会出现卡顿,如上文分析, NSCountedSet 会对数组的每个元素进行比较,耗时主要就在这里,这里会有一次对数组遍历的时间。

    解决办法:

    • 一种有效做法是使用字符串,或者位移,将rgba进行合并,然后再作为元素插入 NSCountedSet 。

    NSCountedSet 使用的注意事项:

    • 1.除非有特定用途,一般不要使用对象作为元素;
    • 2.尽量避免使用 数组、字典 作为元素,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倍。

    相关文章

      网友评论

          本文标题:NSCountedSet - 由取图片主色引发的思考

          本文链接:https://www.haomeiwen.com/subject/pbxxtktx.html