UIStringDrawing methods don't seem to be thread safe in iOS 6
Asked Answered
H

4

22

Using any UIStringDrawing methods on two threads simultaneously causes a crash. My understanding was that all UIStringDrawing methods were thread safe from iOS 4.0.

This code (that does nothing of any use) demonstrates the problem:

dispatch_queue_t queue = dispatch_queue_create("com.queue", NULL);

for (int i = 0; i < 10000; i++) {

    dispatch_async(queue, ^{

        NSString *string = @"My string";
        CGSize size = [string sizeWithFont:[UIFont boldSystemFontOfSize:13]];
    });
}

for (int i = 0; i < 10000; i++) {

    NSString *string = @"My string";
    CGSize size = [string sizeWithFont:[UIFont boldSystemFontOfSize:13]];
}

dispatch_release(queue);

The app crashes after a few iterations of the loops with the following backtrace:

* thread #1: tid = 0x2403, 0x00ad40c8, stop reason = EXC_BAD_ACCESS (code=2, address=0xad40c8)
  frame #0: 0x00ad40c8
  frame #1: 0x36bc4252 WebCore`WebCore::Font::Font(WebCore::FontPlatformData const&, WTF::PassRefPtr<WebCore::FontSelector>) + 90
  frame #2: 0x36bc41f2 WebCore`WebCore::Font::Font(WebCore::FontPlatformData const&, WTF::PassRefPtr<WebCore::FontSelector>) + 10
  frame #3: 0x38f0368e WebKit`rendererForFont(__GSFont*) + 246
  frame #4: 0x38f03230 WebKit`-[NSString(WebStringDrawing) _web_sizeWithFont:forWidth:ellipsis:letterSpacing:resultRange:] + 200
  frame #5: 0x38f03162 WebKit`-[NSString(WebStringDrawing) _web_sizeWithFont:forWidth:ellipsis:letterSpacing:] + 66
  frame #6: 0x38f04532 WebKit`-[NSString(WebStringDrawing) _web_sizeWithFont:] + 58
  frame #7: 0x361dc5d2 UIKit`-[NSString(UIStringDrawing) sizeWithFont:] + 46
  frame #8: 0x00060ca8 myApp`-[TAViewController drawingTest] + 216 at TAViewController.m:157
  frame #9: 0x38da1e66 Foundation`__NSFireDelayedPerform + 450
  frame #10: 0x3aa47856 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 14
  frame #11: 0x3aa47502 CoreFoundation`__CFRunLoopDoTimer + 274
  frame #12: 0x3aa46176 CoreFoundation`__CFRunLoopRun + 1230
  frame #13: 0x3a9b923c CoreFoundation`CFRunLoopRunSpecific + 356
  frame #14: 0x3a9b90c8 CoreFoundation`CFRunLoopRunInMode + 104
  frame #15: 0x3a8a433a GraphicsServices`GSEventRunModal + 74
  frame #16: 0x3622c288 UIKit`UIApplicationMain + 1120
  frame #17: 0x0005f08c myApp`main + 96 at main.m:16

  thread #5: tid = 0x2a03, 0x00ad40c8, stop reason = EXC_BAD_ACCESS (code=2, address=0xad40c8)
    frame #0: 0x00ad40c8
    frame #1: 0x36bc4252 WebCore`WebCore::Font::Font(WebCore::FontPlatformData const&, WTF::PassRefPtr<WebCore::FontSelector>) + 90
    frame #2: 0x36bc41f2 WebCore`WebCore::Font::Font(WebCore::FontPlatformData const&, WTF::PassRefPtr<WebCore::FontSelector>) + 10
    frame #3: 0x38f0368e WebKit`rendererForFont(__GSFont*) + 246
    frame #4: 0x38f03230 WebKit`-[NSString(WebStringDrawing) _web_sizeWithFont:forWidth:ellipsis:letterSpacing:resultRange:] + 200
    frame #5: 0x38f03162 WebKit`-[NSString(WebStringDrawing) _web_sizeWithFont:forWidth:ellipsis:letterSpacing:] + 66
    frame #6: 0x38f04532 WebKit`-[NSString(WebStringDrawing) _web_sizeWithFont:] + 58
    frame #7: 0x361dc5d2 UIKit`-[NSString(UIStringDrawing) sizeWithFont:] + 46
    frame #8: 0x00060d5c myApp`__31-[TAViewController drawingTest]_block_invoke_0 + 116 at TAViewController.m:150
    frame #9: 0x339f0792 libdispatch.dylib`_dispatch_call_block_and_release + 10
    frame #10: 0x339f3b3a libdispatch.dylib`_dispatch_queue_drain + 142
    frame #11: 0x339f167c libdispatch.dylib`_dispatch_queue_invoke + 44
    frame #12: 0x339f4612 libdispatch.dylib`_dispatch_root_queue_drain + 210
    frame #13: 0x339f47d8 libdispatch.dylib`_dispatch_worker_thread2 + 92
    frame #14: 0x37f957f0 libsystem_c.dylib`_pthread_wqthread + 360
    frame #15: 0x37f95684 libsystem_c.dylib`start_wqthread + 8

My understanding was that UIStringDrawing methods were thread safe from iOS 4. I expect that these loops should complete with no errors.

The crash occurs when running on an iPhone running iOS 6 (tested on iPhone 5) but does NOT occur when running on an iPhone running iOS 5 (tested on iPhone 4) or the simulator (tested with iOS 6).

I have implemented what I thought was a fix by serialising any draw calls using CGD:

- (void)serialiseDrawing:(void (^)())block {

    dispatch_sync(self.serialDrawingQueue, block);
}


- (dispatch_queue_t)serialDrawingQueue {

    if (_serialDrawingQueue == NULL) _serialDrawingQueue = dispatch_queue_create("com.myApp.SerialDrawQueue", NULL);

    return _serialDrawingQueue;
}

...and wrapping every draw call like this:

__block CGSize labelSize = CGSizeZero;

[[TAUtils sharedUtils] serialiseDrawing:^{
    labelSize = [label.text sizeWithFont:label.font];
}];

This seems to have improved things a bit (all my UIStringDrawing calls happen on one thread). But it will still crash at times with a backtrace like this:

Exception Type:  EXC_CRASH (SIGSEGV)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Crashed Thread:  0

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libsystem_kernel.dylib          0x3a28ee80 semaphore_wait_trap + 8
1   libdispatch.dylib               0x32851e90 _dispatch_thread_semaphore_wait + 8
2   libdispatch.dylib               0x32850680 _dispatch_barrier_sync_f_slow + 100
3   myApp                           0x000c4330 -[TAUtils serialiseDrawing:] (TAUtils.m:305)
4   myApp                           0x000edfd4 -[TAOmniBar updateLabel] (TAOmniBar.m:394)
5   myApp                           0x000ee8d6 -[TAOmniBar handleNotification:] (TAOmniBar.m:461)
6   CoreFoundation                  0x39820346 _CFXNotificationPost + 1418
7   Foundation                      0x37b5838a -[NSNotificationCenter postNotificationName:object:userInfo:] + 66
8   Foundation                      0x37b5be9a -[NSNotificationCenter postNotificationName:object:] + 26
9   myApp                           0x000f369a -[TAMyViewController update] (TAMyViewController.m:1308)
10  GLKit                           0x328383ce -[GLKViewController _updateAndDraw] + 270
11  QuartzCore                      0x39ffd77c CA::Display::DisplayLink::dispatch(unsigned long long, unsigned long long) + 156
12  QuartzCore                      0x39ffd6d4 CA::Display::IOMFBDisplayLink::callback(__IOMobileFramebuffer*, unsigned long long, unsigned long long, unsigned long long, void*) + 60
13  IOMobileFramebuffer             0x31221fd4 IOMobileFramebufferVsyncNotifyFunc + 152
14  IOKit                           0x39f7c5aa IODispatchCalloutFromCFMessage + 190
15  CoreFoundation                  0x39899888 __CFMachPortPerform + 116
16  CoreFoundation                  0x398a43e4 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 32
17  CoreFoundation                  0x398a4386 __CFRunLoopDoSource1 + 134
18  CoreFoundation                  0x398a320a __CFRunLoopRun + 1378
19  CoreFoundation                  0x39816238 CFRunLoopRunSpecific + 352
20  CoreFoundation                  0x398160c4 CFRunLoopRunInMode + 100
21  GraphicsServices                0x39701336 GSEventRunModal + 70
22  UIKit                           0x35089284 UIApplicationMain + 1116
23  myApp                           0x000b806e main (main.m:16)
24  myApp                           0x000b8024 start + 36

Thread 7 name:  Dispatch queue: com.myApp.SerialDrawQueue
Thread 7:
0   WebCore                         0x35a21410 WebCore::FontFallbackList::invalidate(WTF::PassRefPtr<WebCore::FontSelector>) + 156
1   WebCore                         0x35a2124e WebCore::Font::Font(WebCore::FontPlatformData const&, WTF::PassRefPtr<WebCore::FontSelector>) + 86
2   WebCore                         0x35a211ee WebCore::Font::Font(WebCore::FontPlatformData const&, WTF::PassRefPtr<WebCore::FontSelector>) + 6
3   WebKit                          0x37d6068a rendererForFont(__GSFont*) + 242
4   WebKit                          0x37d61796 -[NSString(WebStringDrawing) __web_drawAtPoint:forWidth:withFont:ellipsis:letterSpacing:includeEmoji:measureOnly:renderedStringOut:drawUnderline:] + 198
5   WebKit                          0x37d616bc -[NSString(WebStringDrawing) __web_drawAtPoint:forWidth:withFont:ellipsis:letterSpacing:includeEmoji:measureOnly:renderedStringOut:] + 84
6   WebKit                          0x37d6165e -[NSString(WebStringDrawing) __web_drawAtPoint:forWidth:withFont:ellipsis:letterSpacing:includeEmoji:measureOnly:] + 82
7   WebKit                          0x37d61602 -[NSString(WebStringDrawing) _web_drawAtPoint:forWidth:withFont:ellipsis:letterSpacing:includeEmoji:] + 78
8   UIKit                           0x35041960 -[NSString(UIStringDrawing) drawAtPoint:forWidth:withFont:lineBreakMode:letterSpacing:includeEmoji:] + 172
9   UIKit                           0x3507de1e -[NSString(UIStringDrawing) drawAtPoint:forWidth:withFont:fontSize:lineBreakMode:baselineAdjustment:includeEmoji:] + 358
10  UIKit                           0x3507dca4 -[NSString(UIStringDrawing) drawAtPoint:forWidth:withFont:fontSize:lineBreakMode:baselineAdjustment:] + 68
11  myApp                           0x000d3300 -[TALabelManager textureCGImageForString:] (TALabelManager.m:859)
12  myApp                           0x000d350a __39-[TALabelManager textureDataForString:]_block_invoke_0 (TALabelManager.m:875)
13  libdispatch.dylib               0x3284d5d8 _dispatch_client_callout + 20
14  libdispatch.dylib               0x3285080a _dispatch_barrier_sync_f_invoke + 22
15  myApp                           0x000c4330 -[TAUtils serialiseDrawing:] (TAUtils.m:305)
16  myApp                           0x000d3420 -[TALabelManager textureDataForString:] (TALabelManager.m:873)
17  myApp                           0x000d0dde __block_global_0 (TALabelManager.m:516)
18  libdispatch.dylib               0x3284d790 _dispatch_call_block_and_release + 8
19  libdispatch.dylib               0x32850b36 _dispatch_queue_drain + 138
20  libdispatch.dylib               0x3284e678 _dispatch_queue_invoke + 40
21  libdispatch.dylib               0x32851610 _dispatch_root_queue_drain + 208
22  libdispatch.dylib               0x328517d4 _dispatch_worker_thread2 + 88
23  libsystem_c.dylib               0x36df27ee _pthread_wqthread + 358
24  libsystem_c.dylib               0x36df2680 start_wqthread + 4

I apologise for the long question but this is a serious problem for me and would really appreciate any help.

Hyacinthus answered 5/10, 2012 at 10:43 Comment(5)
I didn't read your entire question but you know that all drawing should always be done on the main thread, right? None of it is thread safe.Colorless
From Technical Q&A QA1637: CATiledLayer and UIKit graphics: >Important: Beginning with iOS 4.0, drawing to a graphics context in UIKit is thread-safe. This includes accessing and manipulating the current graphics stack, drawing images and strings, and usage of color and font objects from secondary threads.Hyacinthus
I didn't know that. Nice to learn something newColorless
@DavidRönnqvist Your comment is valid but only drawing to the screen should be done on the main thread. There is no reason why offscreen rendering shouldn't happen on a background thread.Glossographer
The problem is that NSString under the hood often calls Webkit which is NOT thread safe.Topper
H
22

While trying to find a work around I noticed that iOS 6 introduces much more widespread integration of NSAttributedString and Core Text so I tried swapping all UIStringDrawing methods with the equivalent NSStringDrawing methods using NSAttributedString in place of NSString and it seems the crashes have stopped.

For example, I'm now using:

NSAttributedString *attribStr = [[NSAttributedString alloc] initWithString:@"My String"];
CGSize size = [attribStr size];

instead of:

NSString *str = @"My String";
CGSize size = [str sizeWithFont:font];
Hyacinthus answered 10/10, 2012 at 11:57 Comment(3)
I'm using [t drawAtPoint:point withFont:font], how could I use it - or any other method - if I replace NSString with NSAttributedString?Jaguarundi
@Jaguarundi NSAttributedString UIKit Additions provides a drawAtPoint: method. You do not need to specify the font as this will be included in the NSAttributedString with NSFontAttributeName.Hyacinthus
I ran into a similar problem where I got the whole WebCore::FontFallbackList::invalidate(WTF::PassRefPtr<WebCore::FontSelector>) error while using UILabels on a background thread. The error went away when I stopped using -adjustsFontSizeToFitWidth. I ended up having to manually size the font for the label. :(Fusibility
B
4

Adam is correct. UIStringDrawing methods are only safe to use from the main queue on iOS 6. You can either use NSStringDrawing methods or CoreText directly to perform rendering from background queues. This is a known issue, but feel free to file more bugs.

Butterscotch answered 13/10, 2012 at 5:49 Comment(0)
H
3

Adam Swinden's solution worked for me. Here's how I converted NSString's sizeWithFont:constrainedToSize::

What used to be:

NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
CGSize size = [text sizeWithFont:font 
               constrainedToSize:(CGSize){width, CGFLOAT_MAX}];

Can be replaced with:

NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
NSAttributedString *attributedText =
    [[NSAttributedString alloc]
        initWithString:text
        attributes:@
        {
            NSFontAttributeName: font
        }];
CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX}
                                           options:NSStringDrawingUsesLineFragmentOrigin
                                           context:nil];
CGSize size = rect.size;

Please note the documentation mentions:

In iOS 7 and later, this method returns fractional sizes (in the size component of the returned CGRect); to use a returned size to size views, you must use raise its value to the nearest higher integer using the ceil function.

So to pull out the calculated height or width to be used for sizing views, I would use:

CGFloat height = ceilf(size.height);
CGFloat width  = ceilf(size.width);
Heliotherapy answered 14/6, 2013 at 2:8 Comment(1)
This does work in iOS6. I reposted this same answer to another question. I give a more detailed explanation there. Take a look: https://mcmap.net/q/23651/-replacement-for-deprecated-sizewithfont-in-ios-7Heliotherapy
W
1

Based on the Adam Swinden's and Mr T's answers I wrote 2 drop-in methods:

@implementation NSString (Extensions)

- (CGSize)threadSafeSizeWithFont:(UIFont *)font {
    return [self threadSafeSizeWithFont:font constrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
}

- (CGSize)threadSafeSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
    // https://mcmap.net/q/577782/-uistringdrawing-methods-don-39-t-seem-to-be-thread-safe-in-ios-6
    NSAttributedString *attributedText =
    [[NSAttributedString alloc]
     initWithString:self
     attributes:@
     {
     NSFontAttributeName: font
     }];
    CGRect rect = [attributedText boundingRectWithSize:size
                                               options:NSStringDrawingUsesLineFragmentOrigin
                                               context:nil];
    return rect.size;
}

@end
Whitmer answered 7/8, 2013 at 11:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.