在 AppKit 中测量文本宽度的性能

Performance of measuring text width in AppKit

AppKit 中有没有一种方法可以非常快速地测量大量 NSString 对象(比如一百万个)的宽度?我尝试了 3 种不同的方法来做到这一点:

  • [NSString sizeWithAttributes:]
  • [NSAttributedString size]
  • NSLayoutManager(获取文本宽度而不是高度)

    以下是一些性能指标

    Count\Mechanism    sizeWithAttributes    NSAttributedString    NSLayoutManager
    1000               0.057                 0.031                 0.007
    10000              0.329                 0.325                 0.064
    100000             3.06                  3.14                  0.689
    1000000            29.5                  31.3                  7.06



    NSLayoutManager 显然是要走的路,但问题是

  • 高内存占用(根据探查器超过 1GB)因为创建重量级 NSTextStorage 对象。
  • 创建时间长。所有花费的时间都是在创建上述字符串的过程中,这本身就是一个破坏因素。(随后测量具有创建和布局的字形的 NSTextStorage 对象只需要大约 0.0002 秒)。
  • 7 秒对于我正在尝试做的事情来说仍然太慢。有没有更快的方法?要在大约一秒钟内测量一百万根弦?

    如果你想玩,Here 是 github 项目。

  • 这里有一些我没有尝试过的想法。

    1. 直接用Core Text。其他 API 都建立在它之上。

    2. 并行化。所有现代 Mac(甚至所有现代 iOS 设备)都有多个内核。将字符串数组分成几个子数组。对于每个子数组,提交一个块到 global GCD queue. In the block, create the necessary Core Text or NSLayoutManager objects and measure the strings in the subarray. Both APIs can be used safely this way. (Core Text) (NSLayoutManager)

    3. 关于“高内存占用”:Use Local Autorelease Pool Blocks to Reduce Peak Memory Footprint.

    4. 关于“所有时间都花在创建上述字符串的过程中,这本身就是一个破坏者”:你是说所有时间都花在这些行上:

      double random = (double)arc4random_uniform(1000) / 1000;
      NSString *randomNumber = [NSString stringWithFormat:@"%f", random];
      

      格式化浮点数是很昂贵的。这是您的真实用例吗?如果您只想为 0 ≤ n < 1000 格式化 n/1000 形式的随机有理数,则有更快的方法。此外,在许多字体中,所有数字都具有相同的宽度,因此很容易排版数字列。如果你选择这样的字体,就可以避免一开始就测量字符串。

    更新

    这是我使用 Core Text 编写的最快的代码。在我的 Core i7 MacBook Pro 上,调度版本的速度几乎是单线程版本的两倍。我的项目分支是 here.

    static CGFloat maxWidthOfStringsUsingCTFramesetter(
            NSArray *strings, NSRange range) {
        NSString *bigString =
            [[strings subarrayWithRange:range] componentsJoinedByString:@"\n"];
        NSAttributedString *richText =
            [[NSAttributedString alloc]
                initWithString:bigString
                attributes:@{ NSFontAttributeName: (__bridge NSFont *)font }];
        CGPathRef path =
            CGPathCreateWithRect(CGRectMake(0, 0, CGFLOAT_MAX, CGFLOAT_MAX), NULL);
        CGFloat width = 0.0;
        CTFramesetterRef setter =
            CTFramesetterCreateWithAttributedString(
                (__bridge CFAttributedStringRef)richText);
        CTFrameRef frame =
            CTFramesetterCreateFrame(
                setter, CFRangeMake(0, bigString.length), path, NULL);
        NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
        for (id item in lines) {
            CTLineRef line = (__bridge CTLineRef)item;
            width = MAX(width, CTLineGetTypographicBounds(line, NULL, NULL, NULL));
        }
        CFRelease(frame);
        CFRelease(setter);
        CFRelease(path);
        return (CGFloat)width;
    }
    
    static void test_CTFramesetter() {
        runTest(__func__, ^{
            return maxWidthOfStringsUsingCTFramesetter(
                testStrings, NSMakeRange(0, testStrings.count));
        });
    }
    
    static void test_CTFramesetter_dispatched() {
        runTest(__func__, ^{
            dispatch_queue_t gatherQueue = dispatch_queue_create(
                "test_CTFramesetter_dispatched result-gathering queue", nil);
            dispatch_queue_t runQueue =
                dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
            dispatch_group_t group = dispatch_group_create();
    
            __block CGFloat gatheredWidth = 0.0;
    
            const size_t Parallelism = 16;
            const size_t totalCount = testStrings.count;
            // Force unsigned long to get 64-bit math to avoid overflow for
            // large totalCounts.
            for (unsigned long i = 0; i < Parallelism; ++i) {
                NSUInteger start = (totalCount * i) / Parallelism;
                NSUInteger end = (totalCount * (i + 1)) / Parallelism;
                NSRange range = NSMakeRange(start, end - start);
                dispatch_group_async(group, runQueue, ^{
                    double width =
                        maxWidthOfStringsUsingCTFramesetter(testStrings, range);
                    dispatch_sync(gatherQueue, ^{
                        gatheredWidth = MAX(gatheredWidth, width);
                    });
                });
            }
    
            dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
            return gatheredWidth;
        });
    }