0.背景
在 iOS 中某场景中需要查询主色,取主色时发现当图片比较大时,会比较耗时,经排查发现了 NSCountedSet 用法上的一些问题。以下是取主色的部分代码:
NSCountedSet *colorSet = [NSCountedSet setWithCapacity:row*column];
for (int x=0; x|
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
测试结果,每次 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倍。