14

I display different types of contents in a tableview and calculate the height of each cell using different custom methods, in heightForRowAtIndexPath.

One of these custom methods implies converting some html in an NSMutableAttributedString, and then calculating the height of this NSMutableAttributedString.
For html conversion I use the new initWithData: method.

All works perfectly except when I rotate the screen => I've got an exc_bad_access every time.

Using Instruments / Zombies, I've been able lo locate the error, and in fact it's this initWithData:.

(When I remove this method and create a "simple" NSMutableAttributedString with initWithString, I can change orientation as many time as I want, no crash anymore).

Any idea why?

(By the way, my project use ARC)


Instrument / Zombie screenshot : enter image description here


Custom method called in heightForRowAtIndexPath :

< UtilitiesForFrontEndUI heightForFacebookAttributedText: >

+(CGFloat)heightForFacebookAttributedText:(NSString *)attributedText withWidth:(CGFloat)width
{
    NSAttributedString *formatedText = [self formatRawFacebookContentForFrontEndRichTextContents:attributedText];
    CGRect rect= [formatedText boundingRectWithSize:CGSizeMake(width, 1000) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil];
    return ceilf(rect.size.height);
}

Custom method using the initWithData for html to NSMutableAttributedString conversion :

< UtilitiesForFrontEndUI formatRawFacebookContentForFrontEndRichTextContents: >

+(NSAttributedString *)formatRawFacebookContentForFrontEndRichTextContents:(NSString *)stringToFormat
{
    // THIS GENERATE EXC_BAD_ACCESS ON DEVICE ROTATION (WORKS IF NO ROTATION)
    NSData *dataContent = [stringToFormat dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableAttributedString *richTxtContent = [[NSMutableAttributedString alloc] initWithData:dataContent options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,NSCharacterEncodingDocumentAttribute: [NSNumber numberWithInt:NSUTF8StringEncoding]} documentAttributes:nil error:nil];

    NSRange myRange;
    myRange.location = 0;
    myRange.length = richTxtContent.length;

    [richTxtContent addAttributes:[self commonAttributesForFrontEndRichText] range:myRange];

    return richTxtContent;
}

If I replace initWithData by a simple initWithString no more exc_bad_access

+(NSAttributedString *)formatRawFacebookContentForFrontEndRichTextContents:(NSString *)stringToFormat
{   
    // THIS WORKS (NO MORE ROTATION CRASH)
    NSMutableAttributedString *richTxtContent = [[NSMutableAttributedString alloc]initWithString:stringToFormat];

    NSRange myRange;
    myRange.location = 0;
    myRange.length = richTxtContent.length;

    [richTxtContent addAttributes:[self commonAttributesForFrontEndRichText] range:myRange];

    return richTxtContent;
}
brainjam
  • 18,863
  • 8
  • 57
  • 82
macbeb
  • 227
  • 2
  • 11

1 Answers1

16

I have a similar situation happening in my app.

[NSMutableAttributedString initWithData:] can take a very long time to return, especially for large inputs. My guess is, while this call is executing, the UIKit rotation handling code needs to run, but, since your main thread is stuck on the initWithData: call, things go a little out of whack.

Try moving the parsing call away from the main thread, so that it doesn't block it:

+(NSAttributedString *)formatRawFacebookContentForFrontEndRichTextContents:(NSString *)stringToFormat completion:(void (^)(NSAttributedString *))completion
   {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                NSData *dataContent = [stringToFormat dataUsingEncoding:NSUTF8StringEncoding];
                NSMutableAttributedString *richTxtContent = [[NSMutableAttributedString alloc] initWithData:dataContent options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,NSCharacterEncodingDocumentAttribute: [NSNumber numberWithInt:NSUTF8StringEncoding]} documentAttributes:nil error:nil];

                NSRange myRange;
                myRange.location = 0;
                myRange.length = richTxtContent.length;

                [richTxtContent addAttributes:[self commonAttributesForFrontEndRichText] range:myRange];

                 dispatch_async(dispatch_get_main_queue(), ^{
                      if (completion)
                          completion(richTxtContent);
                 })
            });
    }

It's also possible that, while your rotation is happening, some object related to your method is being deallocated, causing the EXC_BAD_ACCESS. You'll have to do some debugging on the - (void)dealloc and rotation methods to see what is going on.

Another piece of relevant documentation is the following:

Multicore considerations: Since OS X v10.4, NSAttributedString has used WebKit for all import (but not for export) of HTML documents. Because WebKit document loading is not thread safe, this has not been safe to use on background threads. For applications linked on OS X v10.5 and later, if NSAttributedString imports HTML documents on any but the main thread, the use of WebKit is transferred to the main thread via performSelectorOnMainThread:withObject:waitUntilDone:. This makes the operation thread safe, but it requires that the main thread be executing the run loop in one of the common modes. This behavior can be overridden by setting the value of the standard user default NSRunWebKitOnAppKitThread to either YES (to obtain the new behavior regardless of linkage) or NO (to obtain the old behavior regardless of linkage).

Source

byJeevan
  • 3,728
  • 3
  • 37
  • 60
Arie Litovsky
  • 4,893
  • 2
  • 35
  • 40
  • Thanks, you're probably right (so i accepted the answer). I went the TTTAttributedLabel way as I also wanted to have clickable "links" inside these attributedString. It's working perfectly (after some hard times learning how to use it correctly) = very fast + flexible and easy to integrate/manage + delegate methods – macbeb Jan 27 '14 at 14:27
  • 2
    I would add that Apple specifically says don't use the UIKit version on a background thread. I fixed this by using performSelectorOnMainThread:object:waitUntilDone: and it works like a charm. – Jesse Naugher Feb 06 '14 at 00:13
  • 1
    @Jesse Naugher I realize this is somewhat old, but how did you use `performSelectorOnMainThread:object:waitUntilDone:` in conjunction with this answer that you commented on? Running into a similar issue and looking for a fix. – Evan R Jun 07 '17 at 15:32
  • The fix around using dispatch_async that @arie mentions in the answer would probably be the preferred syntax for dealing with this (more modern syntax using GCD). But I believe I just had a helper method that i just called with the ```-performSelectorOnMainThread``` and it did the actual attributed string processing. – Jesse Naugher Jun 09 '17 at 17:17
  • Regarding this answer—instead of creating a new background queue each time this method gets hit, I think it would be better to create a single serial queue on a background thread to handle all this. Otherwise, you run the risk of creating a multitude of threads at once, as this method gets called in `-tableView:cellForRowAtIndexPath`. In the case of the OP, they were also calling this method in `-tableView:heightForRowAtIndexPath:`. – Evan R Jun 30 '17 at 15:19
  • [Documentation says "The HTML importer should not be called from a background thread"](https://developer.apple.com/documentation/foundation/nsattributedstring/1524613-init) – shim Oct 18 '20 at 02:30