0

I am subclassing a UILabel in which instead of using drawRect I am rendering the text to UIImage in the background.. here's the code:

- (UIImage *)imageForText
{
    UIGraphicsBeginImageContextWithOptions(self.frameSize, NO, 0);

            CGContextRef ctx = UIGraphicsGetCurrentContext();
            CGContextSaveGState(ctx);

            CGAffineTransform transform = [self _transformForCoreText];
            CGContextConcatCTM(ctx, transform);

            if (nil == self.textFrame) {
                CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)attributedStringWithLinks;
                CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);

                CGMutablePathRef path = CGPathCreateMutable();
                // We must tranform the path rectangle in order to draw the text correctly for bottom/middle
                // vertical alignment modes.
                CGPathAddRect(path, &transform, rect);
                if (nil != self.shadowColor) {
                    CGContextSetShadowWithColor(ctx, self.shadowOffset, self.shadowBlur, self.shadowColor.CGColor);
                }
                self.textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
                CGPathRelease(path);
                CFRelease(framesetter);
            }

            // Draw the tapped link's highlight.
            if ((nil != self.touchedLink || nil != self.actionSheetLink) && nil != self.highlightedLinkBackgroundColor) {
                [self.highlightedLinkBackgroundColor setFill];

                NSRange linkRange = nil != self.touchedLink ? self.touchedLink.range : self.actionSheetLink.range;

                CFArrayRef lines = CTFrameGetLines(self.textFrame);
                CFIndex count = CFArrayGetCount(lines);
                CGPoint lineOrigins[count];
                CTFrameGetLineOrigins(self.textFrame, CFRangeMake(0, 0), lineOrigins);

                for (CFIndex i = 0; i < count; i++) {
                    CTLineRef line = CFArrayGetValueAtIndex(lines, i);

                    CFRange stringRange = CTLineGetStringRange(line);
                    NSRange lineRange = NSMakeRange(stringRange.location, stringRange.length);
                    NSRange intersectedRange = NSIntersectionRange(lineRange, linkRange);
                    if (intersectedRange.length == 0) {
                        continue;
                    }

                    CGRect highlightRect = [self _rectForRange:linkRange inLine:line lineOrigin:lineOrigins[i]];

                    if (!CGRectIsEmpty(highlightRect)) {
                        CGFloat pi = (CGFloat)M_PI;

                        CGFloat radius = 5.0f;
                        CGContextMoveToPoint(ctx, highlightRect.origin.x, highlightRect.origin.y + radius);
                        CGContextAddLineToPoint(ctx, highlightRect.origin.x, highlightRect.origin.y + highlightRect.size.height - radius);
                        CGContextAddArc(ctx, highlightRect.origin.x + radius, highlightRect.origin.y + highlightRect.size.height - radius,
                                        radius, pi, pi / 2.0f, 1.0f);
                        CGContextAddLineToPoint(ctx, highlightRect.origin.x + highlightRect.size.width - radius,
                                                highlightRect.origin.y + highlightRect.size.height);
                        CGContextAddArc(ctx, highlightRect.origin.x + highlightRect.size.width - radius,
                                        highlightRect.origin.y + highlightRect.size.height - radius, radius, pi / 2, 0.0f, 1.0f);
                        CGContextAddLineToPoint(ctx, highlightRect.origin.x + highlightRect.size.width, highlightRect.origin.y + radius);
                        CGContextAddArc(ctx, highlightRect.origin.x + highlightRect.size.width - radius, highlightRect.origin.y + radius,
                                        radius, 0.0f, -pi / 2.0f, 1.0f);
                        CGContextAddLineToPoint(ctx, highlightRect.origin.x + radius, highlightRect.origin.y);
                        CGContextAddArc(ctx, highlightRect.origin.x + radius, highlightRect.origin.y + radius, radius, 
                                        -pi / 2, pi, 1);
                        CGContextFillPath(ctx);
                    }
                }


            CTFrameDraw(self.textFrame, ctx);
            CGContextRestoreGState(ctx);
            self.renderedImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
                return self.renderedImage;
}

Now here's a snapshot of what I am getting at on allocations tool on Instruments.

enter image description here

I am not sure how to dig down deeper into this, but it seems that the image is not getting released and is kept at memory and it is increasing as I scroll the UIScrollView I have and call this method more and more. Any ideas? Is something wrong with my code above? I did profile it using leaks and found no sign of leaking...

EDIT:

I added the code on how I am calling imageForText:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
             NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
            [self parseTagsInComment];
            [self.commentsText_ setLinkColor:self.textColor_];
              [weakSelf.commentsText_ imageForText];


            UIImage *renderedImage = [weakSelf.commentsText_ imageForText];
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf.commentTextImageView_ setImage:renderedImage];
                [weakSelf.commentTextImageView_ setAlpha:0.0];
                [UIView animateWithDuration:0.3 animations:^{
                    [weakSelf.commentTextImageView_ setAlpha:1.0];
                }];
            });

             [pool release];
         });

I even tried calling it just by doing:

[self.commentsText_ imageForText];

and yet the memory still spikes up

adit
  • 32,574
  • 72
  • 229
  • 373

2 Answers2

0

We would need to see how you are calling imageForText to tell, but it is suspicious that it looks like you are double retaining the image from UIGraphicsGetImageFromCurrentImageContext(). You set self.renderedImage equal to the UIImage from UIGraphicsGetImageFromCurrentImageContext() and also return the image. I assume the renderedImage is a strong property so it will retain it once. The returned image is likely set in a local variable or otherwise retained elsewhere. A more typical pattern would have been for imageForText not have the side effect of retaining the image and instead call it as follows if you need to keep the image around in a property:

self.renderedImage = [self imageForText];
// Do whatever you want with self.renderedImage ...

Another potential gotcha is that when you say you are "rendering the text to UIImage in the background" hopefully you do not mean in a background thread. The UIKit functions like UIGraphicsBeginImageContextWithOptions() should only be called from the main thread of your application.

torrey.lyons
  • 5,519
  • 1
  • 23
  • 30
  • thanks for the reply.. I have added the code how I am calling the imageForText.. I watched a session on WWDC 2012 where it does the same thing and put the [self imageForText]; into a separate NSOperationQueue, I assume this then will be performed on a background thread? Correct me if Iam wrong – adit Jul 30 '12 at 04:55
  • the WWDC session is on concurrent UI.. if you're interested.. it basically puts the UIGraphicsGetImageFromCurrentImageCtxt to a queue – adit Jul 30 '12 at 06:56
  • I'm curious about the WWDC session since the documentation still says it is main thread only. I'll check it out. A few questions about your code: Why do you call `[weakSelf.commentsText_ imageForText]` twice in a row? Why do you use weakSelf sometimes in the block and sometimes self? How did you generate weakSelf? Also for reference you can but don't need to use `@autoreleasepool` in blocks per http://stackoverflow.com/questions/4141123/do-you-need-to-create-an-nsautoreleasepool-within-a-block-in-gcd. – torrey.lyons Jul 31 '12 at 02:54
  • the double imageForText was just a typo.. in reality there's only one. Yea I should have used weakSelf all along.. will fix that, but don't think it would change a lot – adit Jul 31 '12 at 03:06
0

You should create a bitmap context explicitly in the secondary thread. Using CoreGraphics from a background thread works no problem, but do not invoke UIKit methods from the secondary thread, it will lead to very difficult to debug problems. There are a few exceptions explicitly mentioned in the apple docs (like UIFont) but the general rule is to not use UIKit functions as they do not work in general except in the main thread. Also, do not call access methods like self.frameSize since this makes calls into UIKit.

Because you are using CoreText, you might also be getting bit by this memory leak memory-usage-grows-with-ctfontcreatewithname-and-ctframesetterref.

Community
  • 1
  • 1
MoDJ
  • 4,309
  • 2
  • 30
  • 65