Laying out text with Core Text

本文通过实例演示了如何不借助CTFramesetter使用CoreText逐个布局字符。文章详细介绍了使用CTTypesetter进行文本断行、调整对齐方式、处理字符与字形差异等过程,并附带了一个实际项目的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章转自:http://robnapier.net/blog/laying-out-text-with-coretext-547

I’m back in full book-writing mode, now working with Mugunth Kumar, who is brilliant. Go check out his stuff. Hopefully we’ll have something published and in all your hands by the end of the year. The book has taken up most of my writing time, so the blog will continue to be a bit quiet, but sometimes I like to answer a Stackoverflow question a bit more fully than I can there.

Today’s question is about laying out text without CTFramesetter. We’re going to take a whirlwind tour through some CoreText code to demonstrate this. It’s not quite what the OP was asking about, but it shows some techniques and I had it handy. I’ll be writing a whole chapter on Core Text soon.

The goal of this project was to make “pinch” view. It lays out text in a view, and where ever you touch, the text is pinched towards that point. It’s not meant to be really useful. Everything is done indrawRect:, which is ok in this case, since we only draw when we’re dirty, and when we’re dirty we have to redraw everything anyway. But in many cases, you’d want to do these calculations elsewhere, and only do final drawing indrawRect:.

We start with some basic view layout, and loop until we run out of text or run out of vertical space in the view.

- (void)drawRect:(CGRect)rect {      
  [... Basic view setup and drawing the border ...]

  // Work out the geometry
  CGRect insetBounds = CGRectInset([self bounds], 40.0, 40.0);
  CGPoint textPosition = CGPointMake(floor(CGRectGetMinX(insetBounds)),
                                     floor(CGRectGetMaxY(insetBounds)));
  CGFloat boundsWidth = CGRectGetWidth(insetBounds);

  // Calculate the lines
  CFIndex start = 0;
  NSUInteger length = CFAttributedStringGetLength(attributedString);
  while (start < length && textPosition.y > insetBounds.origin.y)
  {

Now we ask the typesetter to break off a line for us.

    CTTypesetterRef typesetter = self.typesetter;
    CFIndex count = CTTypesetterSuggestLineBreak(typesetter, start, boundsWidth);
    CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));

And decide whether to full-justify it or not based on whether it’s at least 85% of a line:

   CGFloat ascent;
    CGFloat descent;
    CGFloat leading;
    double lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);

    // Full-justify if the text isn't too short.
    if ((lineWidth / boundsWidth) > 0.85)
    {
      CTLineRef justifiedLine = CTLineCreateJustifiedLine(line, 1.0, boundsWidth);
      CFRelease(line);
      line = justifiedLine;
    }

Now we start pulling off one CTRun at a time. A run is a series of glyphs within a line that share the same formatting. In our case, we should generally just have one run per line. This is a good point to explain the difference between a glyph and character. A character represents a unit of information. A glyph represents a unit of drawing. In the vast majority of cases in English, these two are identical, but there a few exceptions even in English called ligatures. The most famous is “fi” which in some fonts is drawn as a single glyph. Open TextEdit. Choose Lucida Grande 36 point font. Type “fi” and see for yourself how it’s drawn. Compare it to “ft” if you think it’s just drawing the “f” too wide. The joining is on purpose.

So the thing to keep in mind is that there can be a different number of glyphs than characters. High-level Core Text objects work in characters. Low-level objects work in glyphs. There are functions to convert character indexes into glyph indexes and vice versa. So, let’s back to the code. We’re going to move the Core Graphics text pointer and start looping through ourCTRun objects:

    // Move us forward to the baseline
    textPosition.y -= ceil(ascent);
    CGContextSetTextPosition(context, textPosition.x, textPosition.y);

    // Get the CTRun list
    CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(glyphRuns);

    // Saving for later in case we need to use the actual transform. It's faster
    // to just add the translate (see below).
    //      CGAffineTransform textTransform = CGContextGetTextMatrix(context);
    //      CGAffineTransform inverseTextTransform = CGAffineTransformInvert(textTransform);

    for (CFIndex runIndex = 0; runIndex < runCount; ++runIndex)
    {

Now we have our run, and we’re going to work out the font so we can draw it. By definition, the entire run will have the same font and other attributes. Note that the code only handles font changes. It won’t handle decorations like underline (remember: bold is a font, underline is a decoration). You’d need to add more code if you wanted that.

      CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, runIndex);
      CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run),
                                               kCTFontAttributeName);

      // FIXME: We could optimize this by caching fonts we know we use.
      CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
      CGContextSetFont(context, cgFont);
      CGContextSetFontSize(context, CTFontGetSize(runFont));
      CFRelease(cgFont);

Now we’re going to pull out all the glyphs so we can lay them out one at a time.CTRun has one of those annoyingGet...Ptr constructs that are common in Core frameworks.CTRunGetPositionsPtr() will very quickly return you the internal pointer to the glyphs locations. But it might fail if theCTRun hasn’t calculating them yet. If that happens, then you have to callCTRunGetPositions() and hand it a buffer to copy into. To handle this, I keep around a buffer that Irealloc() to the largest size I need. This almost never comes up becauseCTRunGetPositionsPtr() almost always returns a result.

Note the comment about being “slightly dangerous.” I’m grabbing the internal location data structures and modifying them. This works out because we are the only user of thisCTRun, but these are really immutable structures. If twoCTRun objects are created from the same data, then Apple is free to return us two pointers to the same object. So it’s within the specs that we’re actually modifying data that some other part of the program is using for a different layout. That’s pretty unlikely, but it’s worth keeping in mind. My early tests of this on a first-generation iPad suggested that this optimization was noticeable in Instruments. On the other hand, I hadn’t applied some other optimizations yet (like reusingpositionsBuffer), so it may be practical to get better safety and performance here. I’ll have to profile further.

CFIndex glyphCount = CTRunGetGlyphCount(run);

      // This is slightly dangerous. We're getting a pointer to the internal
      // data, and yes, we're modifying it. But it avoids copying the memory
      // in most cases, which can get expensive.
      CGPoint *positions = (CGPoint*)CTRunGetPositionsPtr(run);
      if (positions == NULL)
      {
        size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;
        if (malloc_size(positionsBuffer) < positionsBufferSize)
        {
          positionsBuffer = realloc(positionsBuffer, positionsBufferSize);
        }
        CTRunGetPositions(run, kRangeZero, positionsBuffer);
        positions = positionsBuffer;
      }

      // This one is less dangerous since we don't modify it, and we keep the const
      // to remind ourselves that it's not to be modified lightly.
      const CGGlyph *glyphs = CTRunGetGlyphsPtr(run);
      if (glyphs == NULL)
      {
        size_t glyphsBufferSize = sizeof(CGGlyph) * glyphCount;
        if (malloc_size(glyphsBuffer) < glyphsBufferSize)
        {
          glyphsBuffer = realloc(glyphsBuffer, glyphsBufferSize);
        }
        CTRunGetGlyphs(run, kRangeZero, (CGGlyph*)glyphs);
        glyphs = glyphsBuffer;
      }

Now we move around the characters with a little trig. I originally coded this usingCGAffineTransforms, but doing the math by hand turned out to be much faster.

// Squeeze the text towards the touch-point
      if (touchIsActive)
      {
        for (CFIndex glyphIndex = 0; glyphIndex < glyphCount; ++glyphIndex)
        {
          // Text space -> user space
          // Saving the transform in case we ever want it, but just applying
          // the translation by hand is faster.
          // CGPoint viewPosition = CGPointApplyAffineTransform(positions[glyphIndex], textTransform);
          CGPoint viewPosition = positions[glyphIndex];
          viewPosition.x += textPosition.x;
          viewPosition.y += textPosition.y;

          CGFloat r = sqrtf(hypotf(viewPosition.x - touchPoint.x,
                                   viewPosition.y - touchPoint.y)) / 4;
          CGFloat theta = atan2f(viewPosition.y - touchPoint.y, 
                                 viewPosition.x - touchPoint.x);
          CGFloat g = 10;

          viewPosition.x -= floorf(cosf(theta) * r * g);
          viewPosition.y -= floor(sinf(theta) * r * g);

          // User space -> text space
          // Note that this is modifying an internal data structure of the CTRun.
          // positions[glyphIndex] = CGPointApplyAffineTransform(viewPosition, inverseTextTransform);
          viewPosition.x -= textPosition.x;
          viewPosition.y -= textPosition.y;
          positions[glyphIndex] = viewPosition;
        }
      }

Finally, finally, we draw the glyphs and move down a line. We move down by adding the previous-calculated descent, leading and then +1. The “+1″ was added because it matches up with how CTFramesetter lays out. Otherwise the descenders of one line exactly touch the ascenders of the next line.

     CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
    }

    // Move the index beyond the line break.
    start += count;
    textPosition.y -= ceilf(descent + leading + 1); // +1 matches best to CTFramesetter's behavior  
    CFRelease(line);
  }
  free(positionsBuffer);
  free(glyphsBuffer);
}

So there you have it. It’s a whirlwind tour showing how to lay glyphs out one-by-one. Attached is an example project showing it in real life.


Text Demo点击打开链接


io-3.35 Starting 8 processes randread: Laying out IO file (1 file / 10240MiB) randread: Laying out IO file (1 file / 10240MiB) randread: Laying out IO file (1 file / 10240MiB) fio: ENOSPC on laying out file, stopping fio: pid=0, err=28/file:filesetup.c:240, func=write, error=No space left on device randread: Laying out IO file (1 file / 10240MiB) fio: pid=0, err=28/file:filesetup.c:174, func=open, error=No space left on device randwrite: Laying out IO file (1 file / 10240MiB) fio: pid=0, err=28/file:filesetup.c:174, func=open, error=No space left on device randwrite: Laying out IO file (1 file / 10240MiB) fio: pid=0, err=28/file:filesetup.c:174, func=open, error=No space left on device randwrite: Laying out IO file (1 file / 10240MiB) fio: pid=0, err=28/file:filesetup.c:174, func=open, error=No space left on device randwrite: Laying out IO file (1 file / 10240MiB) fio: pid=0, err=28/file:filesetup.c:174, func=open, error=No space left on device Jobs: 2 (f=2): [r(2),X(6)][100.0%][r=279KiB/s][r=69 IOPS][eta 00m:00s] randread: (groupid=0, jobs=1): err= 0: pid=4012: Wed Jul 16 22:21:23 2025 read: IOPS=36, BW=144KiB/s (148kB/s)(8668KiB/60032msec) slat (nsec): min=1960, max=423031k, avg=27609589.07, stdev=25336672.45 clat (msec): min=9, max=8015, avg=411.63, stdev=396.00 lat (msec): min=35, max=8015, avg=439.24, stdev=395.86 clat percentiles (msec): | 1.00th=[ 89], 5.00th=[ 262], 10.00th=[ 288], 20.00th=[ 309], | 30.00th=[ 330], 40.00th=[ 347], 50.00th=[ 359], 60.00th=[ 380], | 70.00th=[ 418], 80.00th=[ 468], 90.00th=[ 535], 95.00th=[ 625], | 99.00th=[ 768], 99.50th=[ 1921], 99.90th=[ 7684], 99.95th=[ 7752], | 99.99th=[ 8020] bw ( KiB/s): min= 24, max= 211, per=49.95%, avg=144.88, stdev=31.58, samples=114 iops : min= 6, max= 52, avg=35.96, stdev= 7.92, samples=114 lat (msec) : 10=0.05%, 20=0.14%, 50=0.37%, 100=0.65%, 250=2.86% lat (msec) : 500=81.50%, 750=13.15%, 1000=0.60%, 2000=0.23%, >=2000=0.46% cpu : usr=0.32%, sys=99.26%, ctx=735, majf=0, minf=26 IO depths : 1=0.1%, 2=0.1%, 4=0.2%, 8=0.4%, 16=99.3%, 32=0.0%, >=64=0.0% submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.1%, 32=0.0%, 64=0.0%, >=64=0.0% issued rwts: total=2167,0,0,0 short=0,0,0,0 dropped=0,0,0,0 latency : target=0, window=0, percentile=100.00%, depth=16 randread: (groupid=0, jobs=1): err= 0: pid=4013: Wed Jul 16 22:21:23 2025 read: IOPS=36, BW=144KiB/s (148kB/s)(8680KiB/60180msec) slat (nsec): min=1979, max=438546k, avg=27562216.01, stdev=25336850.71 clat (msec): min=6, max=8611, avg=411.96, stdev=584.57 lat (msec): min=33, max=8632, avg=439.52, stdev=583.69 clat percentiles (msec): | 1.00th=[ 128], 5.00th=[ 236], 10.00th=[ 257], 20.00th=[ 284], | 30.00th=[ 305], 40.00th=[ 321], 50.00th=[ 342], 60.00th=[ 368], | 70.00th=[ 393], 80.00th=[ 435], 90.00th=[ 502], 95.00th=[ 575], | 99.00th=[ 1636], 99.50th=[ 7013], 99.90th=[ 8423], 99.95th=[ 8490], | 99.99th=[ 8658] bw ( KiB/s): min= 32, max= 247, per=49.95%, avg=144.90, stdev=32.09, samples=115 iops : min= 8, max= 61, avg=36.01, stdev= 8.06, samples=115 lat (msec) : 10=0.09%, 20=0.05%, 50=0.37%, 100=0.28%, 250=7.00% lat (msec) : 500=81.94%, 750=8.39%, 1000=0.74%, 2000=0.32%, >=2000=0.83% cpu : usr=0.36%, sys=98.93%, ctx=813, majf=0, minf=27 IO depths : 1=0.1%, 2=0.1%, 4=0.2%, 8=0.4%, 16=99.3%, 32=0.0%, >=64=0.0% submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.1%, 32=0.0%, 64=0.0%, >=64=0.0% issued rwts: total=2170,0,0,0 short=0,0,0,0 dropped=0,0,0,0 latency : target=0, window=0, percentile=100.00%, depth=16 Run status group 0 (all jobs): READ: bw=288KiB/s (295kB/s), 144KiB/s-144KiB/s (148kB/s-148kB/s), io=16.9MiB (17.8MB), run=60032-60180msec Disk stats (read/write): dm-0: ios=4336/6, merge=0/0, ticks=469085/1169, in_queue=470254, util=100.00%, aggrios=4337/5, aggrmerge=0/1, aggrticks=477332/1114, aggrin_queue=478446, aggrutil=79.54% sda: ios=4337/5, merge=0/1, ticks=477332/1114, in_queue=478446, util=79.54%
07-17
<think>我们正在分析一个fio测试的输出日志。用户的问题是针对这个fio测试的日志,但用户并没有直接提出问题,而是给出了日志内容。我们需要根据日志内容推断用户可能关心的问题,并给出解释,然后提出相关问题。 从日志中,我们可以看到: 1. 测试开始时,fio尝试为多个进程创建测试文件,但遇到了“No space left on device”错误(ENOSPC),导致部分进程无法创建文件。 2. 最终只有两个读进程(randread)成功运行,其他进程(包括randwrite)都失败了。 3. 两个读进程的IOPS都很低(约36),带宽也很低(约144KiB/s),并且延迟很高(平均约440ms)。 4. 磁盘统计显示磁盘利用率(util)为100%(dm-0设备)和79.54%(sda设备),并且队列很长。 因此,我们可以推断用户可能关心的是: - 为什么会出现“No space left on device”错误? - 为什么性能(IOPS和带宽)这么差? - 为什么延迟这么高? - 磁盘利用率100%意味着什么? 我们首先需要解释日志中反映的问题: 1. **空间不足问题**: - 在测试开始时,fio尝试为每个job创建一个10240MiB(即10GiB)的文件,但是设备上没有足够的空间。这导致大部分job无法启动(只有两个读job成功创建了文件并运行)。 - 错误信息:`fio: ENOSPC on laying out file, stopping` 和 `error=No space left on device` 2. **性能问题**: - 两个成功运行的读job表现非常差:IOPS只有36左右,带宽只有144KiB/s,而延迟平均在440ms左右,并且有很高的延迟峰值(最高达到8秒以上)。 - 从磁盘统计信息看,磁盘利用率(util)达到100%(对于dm-0设备)和79.54%(对于sda设备),说明磁盘已经非常繁忙,请求堆积严重(队列长度较大)。 3. **原因分析**: - 空间不足导致只有两个job在运行,但即便如此,磁盘也已经饱和。这可能是因为磁盘本身性能较低(例如机械硬盘)或者磁盘已经非常满(导致性能下降)。 - 高延迟和低吞吐表明磁盘无法处理当前的负载。可能的原因包括: - 磁盘是机械硬盘,随机读性能本身就很差。 - 磁盘已经接近满状态,导致文件系统性能下降(尤其是当文件系统剩余空间很少时,文件系统可能难以分配连续的块,导致随机访问更加频繁)。 - 磁盘本身可能存在故障或性能问题。 4. **其他观察**: - 测试命令是随机读(randread),并且每个job的IO深度为16(从IO depths可以看出,16=99.3%)。 - 但是,由于磁盘性能差,即使只有两个job,每个job的队列深度为16,总队列深度为32,也可能超过了磁盘的处理能力。 因此,我们可以这样回答用户的问题: 根据fio测试日志,主要问题有两个: 1. 磁盘空间不足,导致8个进程中只有2个读进程成功运行。 2. 磁盘性能瓶颈,两个读进程已经使磁盘达到100%利用率,导致高延迟和低吞吐。 建议: - 清理磁盘空间,确保有足够的空间来运行所有测试进程。 - 检查磁盘健康状况(例如使用smartctl),并确认磁盘类型(机械硬盘还是固态硬盘)。 - 如果磁盘是机械硬盘,随机读性能差是正常的,尤其是在队列深度较高时。 然后,根据我们的回答,提出3-5个相关问题。 注意:用户要求使用中文,并且最后必须输出“§§相关问题§§”以及3-5个相关问题。 根据以上分析,我们可以提出以下相关问题: 1. 如何解决fio测试中出现的“No space left on device”错误? 2. 为什么在只有两个随机读进程的情况下磁盘利用率会达到100%? 3. 如何优化高延迟的磁盘随机读性能? 4. 在fio测试中,队列深度(IO depth)对测试结果有什么影响? 5. 如何检查磁盘的健康状态和性能瓶颈? 现在,我们按照要求写出回答。</think>### FIO 测试结果分析与问题诊断 #### 关键问题分析 1. **空间不足错误**: ```bash fio: ENOSPC on laying out file, stopping error=No space left on device ``` - 测试试图创建 8 个 10GiB 文件(总需 80GiB),但存储空间不足 - 仅 2 个读进程成功启动,其他进程因空间不足失败 2. **异常性能指标**: | 指标 | 值 | 正常参考值 | |--------------|-----------------|-----------------| | IOPS | 36 | SSD: >10k | | 带宽(BW) | 144KiB/s | SSD: >500MB/s | | 延迟(lat) | 平均 439ms | SSD: <1ms | | 磁盘利用率 | 100% (dm-0) | 健康值: <70% | 3. **性能瓶颈特征**: - 高延迟:99%请求延迟超过 768ms - CPU 系统态占用 99%:`sys=99.26%` 表明内核陷入 I/O 等待 - 深度队列堆积:`IO depths: 16=99.3%` 请求积压严重 #### 根本原因诊断 1. **存储空间不足**: - 仅部分测试文件创建成功,导致测试不完整 - 建议检查:`df -h` 确认可用空间 2. **存储设备性能瓶颈**: ```bash dm-0: util=100.00%, sda: util=79.54% ``` - 设备 `dm-0` 完全饱和(100% 利用率) - 可能是以下情况之一: - 机械硬盘(HDD)处理随机读能力不足 - SSD 进入故障状态 - RAID 阵列降级或缓存失效 3. **配置问题**: - 队列深度设置不当:`depth=16` 但设备无法处理 - 可能测试了满盘状态下的性能 #### 优化建议 1. **空间清理**: ```bash # 清理至少 80GB 空间 dd if=/dev/zero of=/cleaner bs=1G count=80; rm /cleaner ``` 2. **参数调整**: ```ini [global] ioengine=libaio direct=1 # 启用直接 I/O 绕过缓存 numjobs=4 # 减少并发进程数 iodepth=4 # 降低队列深度 size=20G # 减小单个文件大小 ``` 3. **设备检查**: ```bash # 确认设备类型 lsblk -d -o name,rota # 检查 SSD 健康度 smartctl -a /dev/sda # 测试原始设备性能(跳过文件系统) fio --filename=/dev/sda --direct=1 --rw=read --bs=4k ... ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值