Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash in (NSRange)_effectiveRangeOfOutermostTextBlocksInRange:(NSRange)range #606

Closed
brovador opened this issue Sep 19, 2013 · 24 comments
Closed
Assignees
Labels
Milestone

Comments

@brovador
Copy link

Versions tested: pod 'DTCoreText', '~>1.6.6' and pod 'DTCoreText', :head

We are rendering text in columns inside a collection view. When we build the cell with the column layout (sometimes, with more frequency on simulator), we receive a crash in DTCoreTextLayoutFrame in the method:

-(NSRange)_effectiveRangeOfOutermostTextBlocksInRange:(NSRange)range

Stacktrace:

captura de pantalla 2013-09-18 a la s 15 29 36

captura de pantalla 2013-09-18 a la s 15 29 59

In Xcode5, the console shows the following stacktrace:

2013-09-19 10:53:20.970 App[3116:2a8b] +[__NSCFString objectForKey:]: unrecognized selector sent to class 0x3bd933c
2013-09-19 10:53:20.973 App[3116:2a8b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[__NSCFString objectForKey:]: unrecognized selector sent to class 0x3bd933c'
*** First throw call stack:
(
    0   CoreFoundation                      0x03a945e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x02f508b6 objc_exception_throw + 44
    2   CoreFoundation                      0x03b317a3 +[NSObject(NSObject) doesNotRecognizeSelector:] + 275
    3   CoreFoundation                      0x03a8490b ___forwarding___ + 1019
    4   CoreFoundation                      0x03a844ee _CF_forwarding_prep_0 + 14
    5   Foundation                          0x00cb5119 -[NSConcreteMutableAttributedString attribute:atIndex:effectiveRange:] + 82
    6   App                                 0x000ffc34 -[DTCoreTextLayoutFrame _effectiveRangeOfOutermostTextBlocksInRange:] + 276
    7   App                                 0x001013b7 -[DTCoreTextLayoutFrame _enumerateTextBlocksInRange:usingBlock:] + 103
    8   App                                 0x00101864 -[DTCoreTextLayoutFrame _drawTextBlocksInContext:inRange:] + 228
    9   App                                 0x00103043 -[DTCoreTextLayoutFrame drawInContext:options:] + 2019
    10  App                                 0x000e5dc1 -[DTAttributedTextContentView drawLayer:inContext:] + 785
    11  QuartzCore                          0x01834209 -[CALayer drawInContext:] + 123
    12  QuartzCore                          0x0184ca98 _ZL18tiled_layer_renderP16_CAImageProviderjjjjPv + 1660
    13  QuartzCore                          0x0172d1c3 _ZL21CAImageProviderThreadPjb + 908
    14  QuartzCore                          0x0172ce32 _ZL31CAImageProviderBackgroundThreadPv + 19
    15  libdispatch.dylib                   0x034ad4b0 _dispatch_client_callout + 14
    16  libdispatch.dylib                   0x0349bef1 _dispatch_root_queue_drain + 287
    17  libdispatch.dylib                   0x0349c13d _dispatch_worker_thread2 + 39
    18  libsystem_c.dylib                   0x037f5e72 _pthread_wqthread + 441
    19  libsystem_c.dylib                   0x037dddaa start_wqthread + 30
)
libc++abi.dylib: terminating with uncaught exception of type NSException
@odrobnik
Copy link
Collaborator

@brovador Please add a category -[NSString objectForKey:] and set a breakpoint on that to determine what string this is that this is tried to be called on.

@brovador
Copy link
Author

@Cocoanetics Added the category, printing on the debugger the instance that is receiving the selector we have this:

(lldb) po self
CTFont 
CTFontDescriptor {type = mutable dict, count = 1,
entries =>
    1 : {contents = "NSFontNameAttribute"} = {contents = "Palatino-Roman"}
}

And printing the value of key received in the selector:

(lldb) po key
DTTextBlocks
(lldb) po [key class]
__NSCFConstantString

The screenshot:

captura de pantalla 2013-09-20 a la s 13 21 52

I hope it helps

@odrobnik
Copy link
Collaborator

@brovador No idea. Does that mean that you found a case where self is a string and this string is set as text block?!

@brovador
Copy link
Author

I've added a breakpoint in my category in the method - (id)objectForKey:(id)key, if I ask for self or key in the lldb when it's stopped at this breakpoint the info shown is that.

@brovador
Copy link
Author

The stacktrace:

captura de pantalla 2013-09-20 a la s 13 40 03

Breakpoint screenshot:

captura de pantalla 2013-09-20 a la s 13 38 22

@odrobnik
Copy link
Collaborator

@brovador please inspect the attributed string what class the object has behind the DTTextBlock key.

@brovador
Copy link
Author

After several tries, not always enter in the category, here is other break (BAD_ACCESS in this case) of the same method:

The stacktrace:
captura de pantalla 2013-09-20 a la s 15 02 37

captura de pantalla 2013-09-20 a la s 15 04 07

(lldb) print index
(NSUInteger) $3 = 6372
(lldb) print &effectiveRangeOfBlocksArray
(NSRange *) $4 = location=6363, length=57366424
(lldb) print DTTextBlocksAttribute
(NSString *const) $5 = 0x00508020 @"DTTextBlocks"
(lldb) print _attributedStringFragment
(NSAttributedString *) $6 = 0x0b5d6ec0 @"La se­ño­ra ace­le­ró el paso con sus tres hi­jos, Daoud, Hiba y Ha­san Taha, a tra­vés de la ave­ni­da. No en vano, esta vía se ganó una tris­te fama du­ran­te me­ses, al ser ele­gi­da por un te­mi­ble fran­co­ti­ra­dor que -di­cen- aba­tió a de­ce­nas de per­so­nas.
«Es el pri­mer año que pue­den ve­nir al co­le­gio por­que en el año 2012 era im­po­si­ble cru­zar esa ca­lle. Dis­pa­ra­ban con­ti­nua­men­te. Aho­ra es es­po­rá­di­co y po­de­mos co­rrer el ries­go. Ha­san es­ta­ba hoy fe­liz, como si fue­ra su cum­plea­ños», ex­pli­ca Umm Daoud, la ma­dre de los tres ni­ños, nada más lle­gar al co­le­gio.
Con más de dos de­ce­nas de co­le­gios arra­sa­dos por los bom­bar­deos, la ONG lo­cal Nas­ha­ta ha con­se­gui­do ha­bi­li­tar tres au­las en un edi­fi­cio a me­dio cons­truir y otras tan­tas en una vi­vien­da par­ti­cu­lar aban­do­na­da. El sá­ba­do fue el pri­mer día del se­gun­do año lec­ti­vo que apa­dri­nan en la cas­ti­ga­da ciu­dad si­ria de Deir Ez­zor.
«Usa­mos el só­tano para los más pe­que­ños y p"

@odrobnik
Copy link
Collaborator

@brovador That's not what I asked. Please get the attributes at the specified index and check if the blocks attribute has the correct class.

@brovador
Copy link
Author

Ok, sorry, checking it in both threads:

In thread 23:

(lldb) print index
(NSUInteger) $22 = 6372
(lldb) po [_attributedStringFragment attributesAtIndex:index effectiveRange:&effectiveRange]
{
    CTForegroundColor = " [ (kCGColorSpaceDeviceRGB)] ( 0.223529 0.223529 0.223529 1 )";
    NSFont = "CTFont \nCTFontDescriptor {type = mutable dict, count = 1,\nentries =>\n\t1 : {contents = \"NSFontNameAttribute\"} = {contents = \"Palatino-Roman\"}\n}\n>";
    NSParagraphStyle = "CTParagraphStyle:\nbase writing direction = -1, alignment = 3, line break mode = 0, default tab interval = 36\nfirst line head indent = 0, head indent = 0, tail indent = 0\nline height multiple = 0, maximum line height = 0, minimum line height = 0\nline spacing adjustment = 0, paragraph spacing = 18, paragraph spacing before = 0\n";
}
(lldb) po [_attributedStringFragment attribute:DTTextBlocksAttribute atIndex:index effectiveRange:&effectiveRange]
 nil

In thread 25:

(lldb) print index
(NSUInteger) $25 = 6371
(lldb) po [_attributedStringFragment attributesAtIndex:index effectiveRange:&effectiveRange]
{
    CTForegroundColor = " [ (kCGColorSpaceDeviceRGB)] ( 0.223529 0.223529 0.223529 1 )";
    NSFont = "CTFont \nCTFontDescriptor {type = mutable dict, count = 1,\nentries =>\n\t1 : {contents = \"NSFontNameAttribute\"} = {contents = \"Palatino-Roman\"}\n}\n>";
    NSParagraphStyle = "CTParagraphStyle:\nbase writing direction = -1, alignment = 3, line break mode = 0, default tab interval = 36\nfirst line head indent = 0, head indent = 0, tail indent = 0\nline height multiple = 0, maximum line height = 0, minimum line height = 0\nline spacing adjustment = 0, paragraph spacing = 18, paragraph spacing before = 0\n";
}
(lldb) po [_attributedStringFragment attribute:DTTextBlocksAttribute atIndex:index effectiveRange:&effectiveRange]
 nil

@odrobnik
Copy link
Collaborator

We are not getting anywhere like this. Can you please Skype me at TheDrops?

@brovador
Copy link
Author

Ok perfect!

@brovador
Copy link
Author

Hi Oliver and thanks a lot for the support, really.

I've created a small project that reproduces the issue:

https://github.com/brovador/DTCoreTextLayoutExceptionExample

In the example, I reproduced the same situation that in my project, creating a column based text. It seems that the issue is related to when is creating the TextContentViews for every column. It's not easy to reproduce, in our real project, we have a collection view of articles and the crash comes randomly when we are creating the cell for the article with the columns.

To recreate the same situation in a easy way, I've created a timer inside the ViewController with a button to regenerate the content (creating all the text columns etc). But it's only to force the error, because it's not even necessary the error can be thrown during application launch too.

I think the issue comes by drawing the columns, it's like two columns are being drawn at the same time and it causes the crash. If I inspect both threads, they have the same attributedstring and each one corresponds to one of the columns. The code in the example that I'm using to draw text layouted in columns:

DTHTMLAttributedStringBuilder *stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
DTCoreTextLayouter *layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:[stringBuilder generatedAttributedString]];
    
NSInteger textRangeInit;
DTCoreTextLayoutFrame *layoutFrame;
DTAttributedTextView *textView;
    
//Column1
textRangeInit = 0;
    
layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column1.size} range:NSMakeRange(textRangeInit, 0)];
textView = [[DTAttributedTextView alloc] initWithFrame:column1];
[textView setAttributedString:[stringBuilder generatedAttributedString]];
[[textView attributedTextContentView] setLayoutFrame:layoutFrame];
[self.view addSubview:textView];
    
//Column2
textRangeInit = [layoutFrame visibleStringRange].location + [layoutFrame visibleStringRange].length;
    
layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column2.size} range:NSMakeRange(textRangeInit, 0)];
textView = [[DTAttributedTextView alloc] initWithFrame:column2];
[textView setAttributedString:[stringBuilder generatedAttributedString]];
[[textView attributedTextContentView] setLayoutFrame:layoutFrame];
[self.view addSubview:textView];
    
//Column3
textRangeInit = [layoutFrame visibleStringRange].location + [layoutFrame visibleStringRange].length;
    
layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column3.size} range:NSMakeRange(textRangeInit, 0)];
textView = [[DTAttributedTextView alloc] initWithFrame:column3];
[textView setAttributedString:[stringBuilder generatedAttributedString]];
[[textView attributedTextContentView] setLayoutFrame:layoutFrame];
[self.view addSubview:textView];

The same exception raised in the example:

captura de pantalla 2013-09-21 a la s 11 59 10

I hope it helps

@brovador
Copy link
Author

I've updated the example above, using a button to regenerate the content better than a timer.

@brovador
Copy link
Author

Ok, after several tries, I finally found a workaround and more info about the problem (updated on my test project).

The issue happens when all the textviews created for each column share the same StringBuilder in the layouter. If I change the code to create a new instance of the stringbuilder and the layouter for each column, the issue does not appear.

In my project example I've added a switch to enable and disable de creation of all the elements for each column:

    DTHTMLAttributedStringBuilder *stringBuilder;
    DTCoreTextLayoutFrame *layoutFrame;
    DTAttributedTextView *textView;
    DTCoreTextLayouter *layouter;
    
    stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
    layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:[stringBuilder generatedAttributedString]];
    
    //Column1
    textRangeInit = 0;
    
    layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column1.size} range:NSMakeRange(textRangeInit, 0)];
    textView = [[DTAttributedTextView alloc] initWithFrame:column1];
    [textView setAttributedString:[stringBuilder generatedAttributedString]];
    [[textView attributedTextContentView] setLayoutFrame:layoutFrame];
    [self.vTextContainer addSubview:textView];
    
    //Column2
    
    textRangeInit = [layoutFrame visibleStringRange].location + [layoutFrame visibleStringRange].length;
    if (createAllContent) {
        stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
        layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:[stringBuilder generatedAttributedString]];
    }
    layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column2.size} range:NSMakeRange(textRangeInit, 0)];
    textView = [[DTAttributedTextView alloc] initWithFrame:column2];
    [textView setAttributedString:[stringBuilder generatedAttributedString]];
    [[textView attributedTextContentView] setLayoutFrame:layoutFrame];
    [self.vTextContainer addSubview:textView];
    
    //Column3
    textRangeInit = [layoutFrame visibleStringRange].location + [layoutFrame visibleStringRange].length;
    if (createAllContent) {
        stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
        layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:[stringBuilder generatedAttributedString]];
    }
    layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column3.size} range:NSMakeRange(textRangeInit, 0)];
    textView = [[DTAttributedTextView alloc] initWithFrame:column3];
    [textView setAttributedString:[stringBuilder generatedAttributedString]];
    [[textView attributedTextContentView] setLayoutFrame:layoutFrame];
    [self.vTextContainer addSubview:textView];

if the variable createAllContent is true the string builder and the layouter will be created for each textview and doing this the issue does not appear in any case.

I think that it's something related with the string generated by the stringbuilder or something with the stringbuilder itself. As workaround I can create the stringbuilder and layouter for each column but it will be very less efficient that having a single instance shared between all the textviews that creates the columns of the text.

@odrobnik
Copy link
Collaborator

I fear that enumerations of NSAttributedString attributes might not be thread-safe. A solution would be to determine the ranges of text blocks once for the entire layout frame.

Best regards
Oliver Drobnik

@Cocoanetics on Twitter and App.net
www.cocoanetics.com

On 21.09.2013, at 15:30, brovador notifications@github.com wrote:

Ok, after several tries, I finally found a workaround and more info about the problem (updated on my test project).

The issue happens when all the textviews created for each column share the same StringBuilder in the layouter. If I change the code to create a new instance of the stringbuilder and the layouter for each column, the issue does not appear.

In my project example I've added a switch to enable and disable de creation of all the elements for each column. Basicly:

DTHTMLAttributedStringBuilder *stringBuilder;
DTCoreTextLayoutFrame *layoutFrame;
DTAttributedTextView *textView;
DTCoreTextLayouter *layouter;

stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:[stringBuilder generatedAttributedString]];

//Column1
textRangeInit = 0;

layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column1.size} range:NSMakeRange(textRangeInit, 0)];
textView = [[DTAttributedTextView alloc] initWithFrame:column1];
[textView setAttributedString:[stringBuilder generatedAttributedString]];
[[textView attributedTextContentView] setLayoutFrame:layoutFrame];
[self.vTextContainer addSubview:textView];

//Column2

textRangeInit = [layoutFrame visibleStringRange].location + [layoutFrame visibleStringRange].length;
if (createAllContent) {
    stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
    layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:[stringBuilder generatedAttributedString]];
}
layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column2.size} range:NSMakeRange(textRangeInit, 0)];
textView = [[DTAttributedTextView alloc] initWithFrame:column2];
[textView setAttributedString:[stringBuilder generatedAttributedString]];
[[textView attributedTextContentView] setLayoutFrame:layoutFrame];
[self.vTextContainer addSubview:textView];

//Column3
textRangeInit = [layoutFrame visibleStringRange].location + [layoutFrame visibleStringRange].length;
if (createAllContent) {
    stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
    layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:[stringBuilder generatedAttributedString]];
}
layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column3.size} range:NSMakeRange(textRangeInit, 0)];
textView = [[DTAttributedTextView alloc] initWithFrame:column3];
[textView setAttributedString:[stringBuilder generatedAttributedString]];
[[textView attributedTextContentView] setLayoutFrame:layoutFrame];
[self.vTextContainer addSubview:textView];

if the variable createAllContent is true the string builder and the layouter will be created for each textview and doing this the issue does not appear in any case.

I think that it's something related with the string generated by the stringbuilder or something with the stringbuilder itself. As workaround I can't create this stringbuilder and layouter for each column but it will be very less efficient that having a single instance shared between all the textviews that creates the columns of the text.


Reply to this email directly or view it on GitHub.

@brovador
Copy link
Author

Right @Cocoanetics, calculating first the ranges for the layout frame and assigning the substrings to the columns the issue didn't appear:

    stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:[text dataUsingEncoding:NSUTF8StringEncoding] options:nil documentAttributes:NULL];
    attrString = [stringBuilder generatedAttributedString];
    layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:attrString];
    
    NSRange range1, range2, range3;
    
    textRangeInit = 0;
    layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column1.size} range:NSMakeRange(textRangeInit, 0)];
    range1 = [layoutFrame visibleStringRange];
    
    textRangeInit = range1.location + range1.length;
    layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column2.size} range:NSMakeRange(textRangeInit, 0)];
    range2 = [layoutFrame visibleStringRange];
    
    textRangeInit = range2.location + range2.length;
    layoutFrame = [layouter layoutFrameWithRect:(CGRect){.origin = CGPointZero, .size = column3.size} range:NSMakeRange(textRangeInit, 0)];
    range3 = [layoutFrame visibleStringRange];
    
    //Column1
    textView = [[DTAttributedTextView alloc] initWithFrame:column1];
    [textView setAttributedString:[attrString attributedSubstringFromRange:range1]];
    [self.vTextContainer addSubview:textView];
    
    //Column2
    textView = [[DTAttributedTextView alloc] initWithFrame:column2];
    [textView setAttributedString:[attrString attributedSubstringFromRange:range2]];
    [self.vTextContainer addSubview:textView];
    
    //Column3
    textView = [[DTAttributedTextView alloc] initWithFrame:column3];
    [textView setAttributedString:[attrString attributedSubstringFromRange:range3]];
    [self.vTextContainer addSubview:textView];

This it's probably the best solution. The layouter and the attributedstringbuilder not need to be regenerated for every column and the code is now thread-safe. For me it's a good workaround to my problem, thanks a lot.

I've created a new example project using this technique: https://github.com/brovador/DTCoreTextColumnsExample

@odrobnik
Copy link
Collaborator

Closing this as it does not require a change to DTCoreText.

@odrobnik odrobnik reopened this Oct 2, 2013
@odrobnik
Copy link
Collaborator

odrobnik commented Oct 2, 2013

I'm going to implement a serial queue to avoid doing two enumerations on different threads at the same time.

@ghost ghost assigned odrobnik Oct 2, 2013
@odrobnik
Copy link
Collaborator

odrobnik commented Oct 2, 2013

I've implemented additional synchronization to prevent multiple processes from enumerating the text boxes at the same time.

@brovador it would be great if you could try the current develop version and verify that this indeed fixes the issue.

@odrobnik odrobnik closed this as completed Oct 2, 2013
@brovador
Copy link
Author

brovador commented Oct 2, 2013

Ok, I will check it against my test project and verify it. Thanks!

@brovador
Copy link
Author

brovador commented Oct 2, 2013

@Cocoanetics , I've uploaded again the sample project here:

https://github.com/brovador/DTCoreTextExceptionExample

I was trying with this Podfile, but error still happens:

platform :ios, '5.0'
pod 'DTCoreText', :git => 'https://github.com/Cocoanetics/DTCoreText.git', :branch => 'develop'
pod 'DTFoundation', :git => 'https://github.com/Cocoanetics/DTFoundation.git', :branch => 'develop'

You can download and use my sample project to test or debug the issue. I hope it helps

@odrobnik
Copy link
Collaborator

odrobnik commented Oct 3, 2013

@brovador please post a screen shot of the threads call stacks with all threads expanded.

Also, please tell me which Xcode, Sim version you use to reproduce this.

@odrobnik odrobnik reopened this Oct 3, 2013
@odrobnik
Copy link
Collaborator

odrobnik commented Oct 3, 2013

@brovador Turns out that I needed to synch on the _framesetter or globally. Apparently even though the layoutFrame, the view and the attributed string are separate instances, if you enumerate on overlapping regions you get a crash. Synchronizing on the view class or the _framesetter worked. For safety I opted to do it on the content view globally.

please re-test and comment.

@brovador
Copy link
Author

brovador commented Oct 3, 2013

Tested over my sample project and now works perfect, Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants