Core Text Performance
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 开发者_StackOverflow社区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?
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.
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.
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.
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.
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;
}
精彩评论