Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory usage grows with CTFontCreateWithName and CTFramesetterRef

I'm writing an IOS program which uses custom fonts (CTFontManagerRegisterFontsForURL). I load the font, add it as a string attribute, create a framesetter, then a frame, and draw it to a context. I release everything i use. Instruments doesn't notice a leak but :

The memory used by the applications grows and doesn't shrink when using this function. The retain count of my font is 2 when i leave the function.

Here is the code :

CFMutableAttributedStringRef attributedStringRef = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFAttributedStringBeginEditing(attributedStringRef);
CFAttributedStringReplaceString(attributedStringRef, CFRangeMake(0, 0), (CFStringRef)label.text);

font = CTFontCreateWithName((CFStringRef)label.fontName, label.fontHeight, NULL);

retain count of the font : 1

CFAttributedStringSetAttribute(attributedStringRef, CFRangeMake(0, label.text.length), kCTFontAttributeName, font);
CFAttributedStringEndEditing(attributedStringRef);

retain count of the font : 2

CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, rect);

CFRelease(font);

retain count of the font : 1

CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(attributedStringRef); 

retain count of the font : 3

CFRelease(attributedStringRef);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter,
                                            CFRangeMake(0, 0),
                                            path, NULL);

retain count of the font : 5

CFRelease(frameSetter);

retain count of the font : 4

CTFrameDraw(frame, ctx);
CFRelease(frame);

retain count of the font : 2

CGPathRelease(path);

Is there some sort of cache ? I really need to flush the memory used by this font immediately.

P.S : I used CFGetRetainCount to get the retain count of the font.

Thanks !

like image 632
Ben Avatar asked Dec 13 '11 15:12

Ben


2 Answers

retainCount is useless. Don't call it.

If your app's memory is growing in a repeatable fashion, use Heapshot Analysis to figure out what is consuming memory. Leaks only reports objects that are no longer reachable -- objects whose address does not appear in any active regions of memory -- and, thus, leaks will not find many kinds of memory accretion.

This may be a case of a write-only cache; i.e. something somewhere is proactively caching stuff, but your code is written such that the cached copies are never retrieved. Without additional information -- the results of Heapshot Analysis, for starters -- it is hard to say.


I followed your tutorial, and it confirms that the permanent heap growth is due to the line "CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string); ". OK -- you've confirmed what is leaking and where it is allocated, but not where the extra retain comes from. To that, turn on "Record reference counts" in the Allocations instrument and re-run the test. This will allow you to inspect the backtraces of every retain/release call on the offending object. There will be an extra retain in there; a retain not balanced by a release.

I'm guessing the context is somehow hanging on to it.

(I had already analyzed the memory and saw that it was occupied by this object, that's why i checked retain count.

The absolute retain count of an object is useless. That it is still in memory means that it is over-retained and the retain count, itself, can't really tell you anything more unless you also have the full backtrace of every single retain (and release) call on the object, which Instruments gives you.

like image 147
bbum Avatar answered Sep 22 '22 08:09

bbum


Ben, I did some deep diving into the impl with the debugger with and iPhone 4 device and it looks like the root of the problem is actually in the CFMutableAttributedString implementation. It looks like what is going on is that any object passed into an mutable attributed string using the CFAttributedStringSetAttribute() or CFAttributedStringSetAttributes() methods will leak (because the ref will be incremented but not decremented). You were seeing it with a kCTFontAttributeName, but I tested it and the same problem shows up with a kCTForegroundColorAttributeName or kCTParagraphStyleAttributeName value as well. For example, I examined the memory used for a paragraph style object created via CTParagraphStyleCreate() and passed into the attr str like so:

CTParagraphStyleRef  paragraphStyle = CTParagraphStyleCreate(paragraphSettings, 1);  
CFRange textRange = CFRangeMake(0, [self length]);
CFAttributedStringSetAttribute(mAttributedString, textRange, kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);

This paragraphStyle object would be preserved internally by the attr str, but then when it came time to drop the last ref to the attr str via:

CFRelease(attrString);

The above should have dropped the final ref to the paragraphStyle object, but it does not. I can only come to one conclusion, this is a bug in Apple's implementation of the mutable attributed string. Note also that I tried CFAttributedStringRemoveAttribute() and CFAttributedStringSetAttributes() with a phony value and the clearOtherAttributes set to TRUE but nothing seems to work to force the object to drop the refs to the property objects it holds.

Update: after some additional testing today, I found that this is the minimal app code needed to reproduce the leak in a very simple way. This avoids ever rendering the text into a context, so it cannot be a problem with the context saving the font ref or something. You only need these 2 functions in an app delegate example:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
  // Override point for customization after application launch.
  self.window.backgroundColor = [UIColor whiteColor];
  [self.window makeKeyAndVisible];

  [self.timer invalidate];
  self.timer = [NSTimer timerWithTimeInterval: 0.5
                                       target: self
                                     selector: @selector(timerCallback:)
                                     userInfo: NULL
                                      repeats: TRUE];

  [[NSRunLoop currentRunLoop] addTimer:self.timer forMode: NSDefaultRunLoopMode];

  return YES;
}

// This callback is invoked onver and over on an interval. The goal of this function is to demonstrate
// a memory leak in CoreText. When a font is set with CFAttributedStringSetAttribute() and then
// the mutable string is copied by CTFramesetterCreateWithAttributedString(), the memory associated
// with the font ref is leaked.

- (void) timerCallback:(NSTimer*)timer
{
  CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);

  CFStringRef cfStr = (CFStringRef)@"a";
  CFAttributedStringReplaceString(attrString, CFRangeMake(0, 0), cfStr);

  CFRange range = CFRangeMake(0, 1);

  CTFontRef plainFontRef = CTFontCreateWithName((CFStringRef)@"Helvetica", 12, nil);

  // plainFontRef retain count incremented from 1 to 2

  CFAttributedStringSetAttribute(attrString, range, kCTFontAttributeName, plainFontRef);

  // plainFontRef retain count incremented from 2 to 4. Note that in order to see
  // a leak  this CTFramesetterCreateWithAttributedString() must be invoked. If
  // the creation of a framesetter is commented out, then the font inside the
  // attr string would be dellocated properly. So, this is likely a bug in the
  // implementation of CTFramesetterCreateWithAttributedString() in how it copies
  // properties from the mutable attr string.

  CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);

  // plainFontRef retain count decremented from 4 to 3 (note that it should have been decremented by 2)

  CFRelease(framesetter);

  // retain count is 1 at this point, so attrString is deallocated. Note that this should
  // drop the retain count of the font ref but it does not do that.

  CFRelease(attrString);

  // The retain count here should be 1 and this invocation should drop the last ref.
  // But the retain count for plainFontRef is 3 at this point so the font leaks.

  CFRelease(plainFontRef);

  return;
}

I have tested this in the simulator (iOS 5 and 6) and on a device with iOS 5.1 and I see the leak in all cases. Could someone with iOS 6 or newer try this out and see if the leak appears there also, the key is that the number of CTFont objects keeps increasing either with the leaks profile or the allocations profile.

like image 35
MoDJ Avatar answered Sep 22 '22 08:09

MoDJ