Core Text Performance
Asked Answered
I

5

18

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?

Ibbie answered 19/5, 2011 at 17:21 Comment(0)
I
5

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.

Ibbie answered 3/6, 2011 at 7:0 Comment(3)
can you elaborate more on how you delete/hide any attributes?Emigrate
Get the range of the visible part of the text view, loop through all the attributes in the attributed string and then delete ones not in that range.Ibbie
I can confirm this: I was using a single custom attribute with different values depending on the location in the string. Removing it produced a 5X speed boost in CTFramesetterCreateFrame(). The other 2 attributes I used which had the same value for the entire string had no performance impact.Distinguished
T
9

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.

Tailorbird answered 19/5, 2011 at 17:55 Comment(2)
That definitely sounds like a more efficient way of doing it. Are there any examples of this on the internet because all the ones I can find use CTFrameSetter.Ibbie
I haven't seen a lot of good examples of Core Text out there. I've gone ahead and written up some of the work I've done here: robnapier.net/blog/laying-out-text-with-coretext-547. It's not exactly the same thing, but it may give you some pointers in the right direction.Tailorbird
I
5

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.

Ibbie answered 3/6, 2011 at 7:0 Comment(3)
can you elaborate more on how you delete/hide any attributes?Emigrate
Get the range of the visible part of the text view, loop through all the attributes in the attributed string and then delete ones not in that range.Ibbie
I can confirm this: I was using a single custom attribute with different values depending on the location in the string. Removing it produced a 5X speed boost in CTFramesetterCreateFrame(). The other 2 attributes I used which had the same value for the entire string had no performance impact.Distinguished
N
1

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.

Neighborly answered 6/10, 2011 at 22:27 Comment(2)
That sounds good but an added problem with that is if the text runs onto a new line in one paragraph you'd have to shift every paragraph after that so they don't overlap.Ibbie
You're correct, so there's a performance hit each time a new line is added, but only then. And, fortunately, you can get away with slightly worse performance in this case as the user's eyes have to track back to the beginning of the line.Neighborly
A
0

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.

Additament answered 9/1, 2013 at 21:3 Comment(1)
That project looks very good, and I've taken a good look at it. Thanks very much!Ibbie
S
0

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;
}
Sunflower answered 24/1, 2022 at 17:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.