Ben, I did a deep dive into impl with a debugger device and iPhone 4, and it looks like the root of the problem is actually implemented in the implementation of CFMutableAttributedString. It seems that what happens is that any object passed to the mutable attribute string using the CFAttributedStringSetAttribute () or CFAttributedStringSetAttributes () methods will leak (because the ref will increase, but not decrease). You saw it with the name kCTFontAttributeName, but I tested it, and the same problem is found with the value kCTForegroundColorAttributeName or kCTParagraphStyleAttributeName. For example, I studied the memory used for a paragraph style object created with CTParagraphStyleCreate () and passed to attr str like this:
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphSettings, 1); CFRange textRange = CFRangeMake(0, [self length]); CFAttributedStringSetAttribute(mAttributedString, textRange, kCTParagraphStyleAttributeName, paragraphStyle); CFRelease(paragraphStyle);
This paragraphStyle object will be saved internally with attr str, but then when it is time to drop the last ref to attr str via:
CFRelease(attrString);
The above should have cast the final ref down to the paragraphStyle object, but that is not the case. I can only come to one conclusion, this is a mistake in Apple's implementation of the modified attribute string. Please also note that I tried using CFAttributedStringRemoveAttribute () and CFAttributedStringSetAttributes () with a fake value, and clearOtherAttributes is set to TRUE, but nothing works to make the object drop references to property objects that it has.
Update: after some additional testing today, I found that this is the minimum application code needed to reproduce the leak in a very simple way. This avoids turning the text into a context, so this may not be a problem with the context preserving the font ref or something like that. You only need these 2 functions in the application delegation 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 tested this in a simulator (iOS 5 and 6) and on a device with iOS 5.1, and I see a leak in all cases. Can someone with iOS 6 or newer try this and see if there is a leak, the key is that the number of CTFont objects increases with the increase of the leak profile or distribution profile.