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

时间:2015-05-29 19:40:53

标签: objective-c cocoa nsattributedstring nslayoutmanager 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显然是要走的路,但问题在于

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

    如果您想玩游戏,Here是github项目。

  • 1 个答案:

    答案 0 :(得分:3)

    以下是我未尝试的一些想法。

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

    2. 并行化。所有现代Mac(甚至所有现代iOS设备)都有多个内核。将字符串数组分成几个子数组。对于每个子数组,请向global GCD queue提交一个块。在块中,创建必要的Core Text或NSLayoutManager对象并测量子数组中的字符串。这两种API都可以通过这种方式安全使用。 (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];
      

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

    5. 更新

      这是我使用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;
          });
      }