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

lea*_*lin 10 cocoa objective-c nslayoutmanager nsattributedstring nstextstorage

在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显然是要走的路,但问题在于

  • 由于创建了重量级的NSTextStorage对象,因此内存占用空间很大(根据分析器而超过1GB).
  • 创作时间长.所有花费的时间都是在创建上述字符串的过程中,这本身就是一个交易破坏者.(随后测量NSTextStorage对象,其中创建和布置的字形只需要大约0.0002秒).
  • 对于我想要做的事情,7秒仍然太慢.有更快的方法吗?在大约一秒钟内测量一百万个字符串?

    如果你想玩,是github项目.

  • rob*_*off 3

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

    \n\n
      \n
    1. 直接使用核心文本。其他 API 均构建在其之上。

    2. \n
    3. 并行化。所有现代 Mac(甚至所有现代 iOS 设备)都具有多个内核。将字符串数组分成几个子数组。对于每个子数组,将一个块提交到全局 GCD 队列。在块中,创建必要的核心文本或NSLayoutManager对象并测量子数组中的字符串。通过这种方式可以安全地使用这两个 API。(核心文本) ( NSLayoutManager)

    4. \n
    5. 关于\xe2\x80\x9c高内存占用\xe2\x80\x9d:使用本地自动释放池块来减少峰值内存占用。

    6. \n
    7. 关于 \xe2\x80\x9c 所有花费的时间都是在创建上述字符串期间,这本身就是一个破坏者\xe2\x80\x9d:你是说所有时间都花在这些行上:

      \n\n
      double random = (double)arc4random_uniform(1000) / 1000;\nNSString *randomNumber = [NSString stringWithFormat:@"%f", random];\n
      Run Code Online (Sandbox Code Playgroud)\n\n

      格式化浮点数的成本很高。这是您的真实用例吗?如果您只想为 0 \xe2\x89\xa4 n < 1000 格式化 n/1000 形式的随机有理数,有更快的方法。此外,在许多字体中,所有数字都具有相同的宽度,因此可以轻松排版数字列。如果您选择这样的字体,您可以首先避免测量字符串。

    8. \n
    \n\n

    更新

    \n\n

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

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