核心文本性能

发布于 2024-11-08 04:55:18 字数 666 浏览 0 评论 0原文

当 Core Text 在原始 iPad 上运行时,我发现它存在一些性能问题。

我使用 Core Text 和基于 OmniGroup 的 OUIEditableFrame 的 UITextInput 协议创建了一个可编辑视图。

当视图中有大量文本(例如 180 行)时,键入/输入会大大滞后,并且按一下按键通常需要 1-2 秒。

使用带有模拟器的仪器,我能够缩小问题范围并找出是什么花了这么多时间。事实证明,这是因为我每次击键都会重新绘制框架,所以占用了这么多时间的是调用 CTFramesetterCreateWithAttributedStringCTFramesetterCreateFrame

我必须在每次击键时重新绘制,以便更新文本,这意味着调用 CTFramesetterCreateWithAttributedStringCTFramesetterCreateFrame

还有其他人遇到过这个问题吗?如果有,他们是如何解决的?


编辑:

做了一些进一步的调查,结果发现,如果属性字符串没有属性,那么所有内容都会绘制得更快并且没有任何延迟。更改字体、颜色或段落样式都会减慢速度。知道这是否与此有关吗?

I am seeing some performance issues with Core Text when it is run on the original iPad.

I have created an editable view using Core Text and the UITextInput protocol which is based around OmniGroup's OUIEditableFrame.

When there is a fair amount of text in the view say 180 lines, typing/input lags greatly behind and one tap on a key usually takes 1-2 seconds.

Using instruments with the simulator I was able to narrow down the problem and find out what was taking so much time. Turns out it's because I redraw the frame with every key stroke, what takes up so much time is calling CTFramesetterCreateWithAttributedString and CTFramesetterCreateFrame.

I have to redraw with every key stroke so that the text gets updated, this means calling CTFramesetterCreateWithAttributedString and CTFramesetterCreateFrame.

Has anyone else come upon this problem, and if so, how did they get around it?


EDIT:

Did some further investigating and turns out that if the attributed string has no attributes then everything draws so much faster and without any lag. Changing the font, color or paragraphs style all slow it down. Any idea if this could have something to do with it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

蓦然回首 2024-11-15 04:55:18

您可能不应该使用 CTFramesetter 来创建类似 UITextView 的内容。相反,您可能应该保留一个 CTLine 引用数组。如果您需要断字方面的帮助,那么您可以使用 CTTypeSetter,但您只需将当前插入符号及以下的行交给它(您仍然会创建和销毁排字机,所以看看你对他们的要求有多少)。

保留 CTLines 数组的一个好处是,如果内存不足,您可以丢弃不需要的 CTLines 并在以后重建它们。只需跟踪每行的字符范围即可。

You probably should not be using CTFramesetter to create something like UITextView. Instead, you should likely keep an array of CTLine references. If you need help with word breaking, then you can use a CTTypeSetter, but you only need to hand it lines at the current caret and below (you'll still be creating and destroying typesetters a bit, so watch how much you ask of them).

One nice thing about keeping an array of CTLines is that you can throw away the ones you don't need if you're low on memory and reconstruct them later. Just keep track of the character range for each line.

虚拟世界 2024-11-15 04:55:18

自从我最初的问题以来,我做了一些更多的调查,发现绘制的字符串具有的属性越多,花费的时间就越长。

有了这些知识,我决定简单地删除/隐藏用户看不到的任何属性(特别是kCTForegroundColor),这将绘图速度提高了十倍,并使其成为更可用的体验。

Since my original question, I did some more investigating and found out that the more attributes the drawn string has, the longer it takes.

With that knowledge I decided to simply delete/hide any attributes (specifically kCTForegroundColor) the user could not see, this sped up the drawing ten fold and made it a much more usable experience.

梦归所梦 2024-11-15 04:55:18

另一种方法是继续使用 CTFramesetter,但使用较小的 CTFrame。只需将您的 NSAttributedString 转换为子字符串(例如,使用 [NSString paragraphRangeForRange:] 获取段落范围,然后使用 attributeSubstringFromRange: 分解您的属性字符串)。然后为每个段落创建一个 CTFrame。当某些内容发生变化时(例如,用户键入某些内容),您只需更新发生变化的 CTFrame。

这意味着您可以继续利用 CTFramesetter 提供的功能,而不会因每次重新设置所有文本而造成性能损失。

An alternative approach is to continue to use CTFramesetter, but use smaller CTFrames. Just your NSAttributedString into substrings (e.g. using [NSString paragraphRangeForRange:] to get paragraph ranges and then break your attributed string up using attributedSubstringFromRange:). Then create a CTFrame per paragraph. When something changes (e.g. the user types something), you only update the CTFrame(s) that changed.

This means you get to keep taking advantage of what CTFramesetter gives you without the performance penalty of re-setting all the text every time.

故笙诉离歌 2024-11-15 04:55:18

我一直在尝试使用 CTLinesUITableView 在 iOS 上进行语法突出显示。 tableView 的伟大之处在于您可以刷新、删除和插入一行,并且只能重绘该行。

CTFramesetterCreateWithAttributedString 确实很慢。所以你用得越少越好。如果用户输入一些内容,您不需要再次将整个文本拆分成行,您可以只更新当前行,如果它溢出,您可以插入新行。要使其在每种情况下都能正常工作需要做一些工作,但性能可能会令人惊叹。

这就是我所做的:https://github.com/Anviking/Chromatism

I have been experimenting with using CTLines and an UITableView in my attempts to do syntax highlighting on iOS. The great thing about the tableView is that you can refresh, delete, and insert a line, and only redraw that line.

CTFramesetterCreateWithAttributedString is really slow. So the less you use it the better. If the user types something you don't need to split the entire text into lines again, you could just update the current line, and if its overflowing you could insert new one. It's a bit of work getting it to work in every case, but the performance could be amazing.

This what I have done: https://github.com/Anviking/Chromatism.

想念有你 2024-11-15 04:55:18

在没有框架设置器的情况下布局文本时,我需要像 Rob Napier 建议的类似功能。然而,罗布的答案看起来与用框架设置器布置时并不完全相同,所以我稍微修改了它:
https://gist.github.com/jpiringer/75ed6e666832d1f8201a6b2c79610736

#include "CustomLayouter.hpp"

#import <Foundation/Foundation.h>

#include <CoreText/CoreText.h>

#include <malloc/malloc.h>

static const CFRange kRangeZero = {0, 0};

CFIndex layoutTextInRectangle(CGContextRef context, CGRect rect, CFAttributedStringRef attributedString, CFIndex startLocation, float justificationWidth, float baselineGrid, float baselineGridOffsetY) {
    // Calculate the lines
    CFIndex start = startLocation;
    CGFloat boundsWidth = rect.size.width;
    CGPoint textPosition = CGPointMake(rect.origin.x, rect.origin.y+rect.size.height);
    CTTypesetterRef typesetter;
    CGPoint *positionsBuffer = nullptr;
    CGGlyph *glyphsBuffer = nullptr;

    typesetter = CTTypesetterCreateWithAttributedString(attributedString);
    NSUInteger length = CFAttributedStringGetLength(attributedString);
    while (start < length && textPosition.y > rect.origin.y) {
        CFIndex count = CTTypesetterSuggestLineBreak(typesetter, start, boundsWidth);
        CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));
        
        CGFloat ascent;
        CGFloat descent;
        CGFloat leading;
        double lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat lineHeight = ascent+descent+leading;
        
        if (textPosition.y-lineHeight <= rect.origin.y) {
            break;
        }

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

        CGContextSetTextPosition(context, textPosition.x, ceilf(textPosition.y-ascent));
        
        // Get the CTRun list
        CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
        CFIndex runCount = CFArrayGetCount(glyphRuns);
        
        for (CFIndex runIndex = 0; runIndex < runCount; ++runIndex) {
            CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(glyphRuns, runIndex);
            CTFontRef runFont = (CTFontRef)CFDictionaryGetValue(CTRunGetAttributes(run),
                                                     kCTFontAttributeName);
            
            //CGFloat lineHeight = getLineHeight(runFont);
            //NSLog(@"lineHeight: %f == %f", lineHeight, ascent+descent+leading);
            
            // FIXME: We could optimize this by caching fonts we know we use.
            CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, nullptr);
            CGContextSetFont(context, cgFont);
            CGContextSetFontSize(context, CTFontGetSize(runFont));
            CFRelease(cgFont);
            
            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 == nullptr) {
                size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;
                if (malloc_size(positionsBuffer) < positionsBufferSize) {
                    positionsBuffer = (CGPoint *)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 == nullptr) {
                size_t glyphsBufferSize = sizeof(CGGlyph) * glyphCount;
                if (malloc_size(glyphsBuffer) < glyphsBufferSize) {
                    glyphsBuffer = (CGGlyph *)realloc(glyphsBuffer, glyphsBufferSize);
                }
                CTRunGetGlyphs(run, kRangeZero, (CGGlyph*)glyphs);
                glyphs = glyphsBuffer;
            }
            CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
        }
        
        // Move the index beyond the line break.
        start += count;
        textPosition.y -= floorf(descent + leading + ascent);
        CFRelease(line);
    }
    free(positionsBuffer);
    free(glyphsBuffer);
    
    CFRelease(typesetter);
    
    if (start > length) {
        return 0;
    }
    return start;
}

I needed a similar functionality like Rob Napier suggested when laying out text without a framesetter. however Rob's answer didn't exactly look the same as when laid out with a framesetter, so I slightly reworked it:
https://gist.github.com/jpiringer/75ed6e666832d1f8201a6b2c79610736

#include "CustomLayouter.hpp"

#import <Foundation/Foundation.h>

#include <CoreText/CoreText.h>

#include <malloc/malloc.h>

static const CFRange kRangeZero = {0, 0};

CFIndex layoutTextInRectangle(CGContextRef context, CGRect rect, CFAttributedStringRef attributedString, CFIndex startLocation, float justificationWidth, float baselineGrid, float baselineGridOffsetY) {
    // Calculate the lines
    CFIndex start = startLocation;
    CGFloat boundsWidth = rect.size.width;
    CGPoint textPosition = CGPointMake(rect.origin.x, rect.origin.y+rect.size.height);
    CTTypesetterRef typesetter;
    CGPoint *positionsBuffer = nullptr;
    CGGlyph *glyphsBuffer = nullptr;

    typesetter = CTTypesetterCreateWithAttributedString(attributedString);
    NSUInteger length = CFAttributedStringGetLength(attributedString);
    while (start < length && textPosition.y > rect.origin.y) {
        CFIndex count = CTTypesetterSuggestLineBreak(typesetter, start, boundsWidth);
        CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));
        
        CGFloat ascent;
        CGFloat descent;
        CGFloat leading;
        double lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat lineHeight = ascent+descent+leading;
        
        if (textPosition.y-lineHeight <= rect.origin.y) {
            break;
        }

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

        CGContextSetTextPosition(context, textPosition.x, ceilf(textPosition.y-ascent));
        
        // Get the CTRun list
        CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
        CFIndex runCount = CFArrayGetCount(glyphRuns);
        
        for (CFIndex runIndex = 0; runIndex < runCount; ++runIndex) {
            CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(glyphRuns, runIndex);
            CTFontRef runFont = (CTFontRef)CFDictionaryGetValue(CTRunGetAttributes(run),
                                                     kCTFontAttributeName);
            
            //CGFloat lineHeight = getLineHeight(runFont);
            //NSLog(@"lineHeight: %f == %f", lineHeight, ascent+descent+leading);
            
            // FIXME: We could optimize this by caching fonts we know we use.
            CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, nullptr);
            CGContextSetFont(context, cgFont);
            CGContextSetFontSize(context, CTFontGetSize(runFont));
            CFRelease(cgFont);
            
            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 == nullptr) {
                size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;
                if (malloc_size(positionsBuffer) < positionsBufferSize) {
                    positionsBuffer = (CGPoint *)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 == nullptr) {
                size_t glyphsBufferSize = sizeof(CGGlyph) * glyphCount;
                if (malloc_size(glyphsBuffer) < glyphsBufferSize) {
                    glyphsBuffer = (CGGlyph *)realloc(glyphsBuffer, glyphsBufferSize);
                }
                CTRunGetGlyphs(run, kRangeZero, (CGGlyph*)glyphs);
                glyphs = glyphsBuffer;
            }
            CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
        }
        
        // Move the index beyond the line break.
        start += count;
        textPosition.y -= floorf(descent + leading + ascent);
        CFRelease(line);
    }
    free(positionsBuffer);
    free(glyphsBuffer);
    
    CFRelease(typesetter);
    
    if (start > length) {
        return 0;
    }
    return start;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文