iOS -[UIImage initWithCGImage:scale:orientation:] 在后台线程上崩溃

iOS -[UIImage initWithCGImage:scale:orientation:] crashes on a background thread

我正在使用 -[UIImage initWithCGImage:scale:orientation:] 在后台队列上创建一个 UIImage,它因此崩溃了:

崩溃 1

Crashed: com.apple.root.background-qos
0  libsystem_kernel.dylib         0x183778140 __pthread_kill + 8
1  libsystem_pthread.dylib        0x183840ef8 pthread_kill + 112
2  libsystem_c.dylib              0x1836e9dac abort + 140
3  libsystem_malloc.dylib         0x1837acd34 free_list_checksum_botch + 438
4  libsystem_malloc.dylib         0x1837aced8 free_tiny_botch + 84
5  CoreFoundation                 0x183c08038 __CFBasicHashRehash + 2448
6  CoreFoundation                 0x183c09034 __CFBasicHashAddValue + 100
7  CoreFoundation                 0x183ab49c4 CFDictionarySetValue + 248
8  UIKit                          0x18926fd30 _UITraitCollectionCacheForBuiltinStorage + 152
9  UIKit                          0x189272090 +[UITraitCollection traitCollectionWithDisplayScale:] + 52
10 UIKit                          0x1888d5070 -[UIImage initWithCGImage:scale:orientation:] + 236
11 UIKit                          0x1888d4f74 +[UIImage imageWithCGImage:scale:orientation:] + 72
12 MyApp                          0x10001a168 -[UIImage(Additions) foo_scaledImageData] (Foo.m:1527)
13 MyApp                          0x100017304 -[Foo bar] (Foo.m:731)
14 MyApp                          0x100017094 __95-[Foo bar]_block_invoke_3 (Foo.m:691)
15 libdispatch.dylib              0x183629630 _dispatch_call_block_and_release + 24
16 libdispatch.dylib              0x1836295f0 _dispatch_client_callout + 16
17 libdispatch.dylib              0x183637a88 _dispatch_root_queue_drain + 2140
18 libdispatch.dylib              0x183637224 _dispatch_worker_thread3 + 112
19 libsystem_pthread.dylib        0x18383d470 _pthread_wqthread + 1092
20 libsystem_pthread.dylib        0x18383d020 start_wqthread + 4

我也遇到了以下两个线程的崩溃:

崩溃 2,线程 1:

Crashed: com.apple.root.background-qos
0  libsystem_kernel.dylib         0x237d3c5c __pthread_kill + 8
1  libsystem_pthread.dylib        0x23879b47 pthread_kill + 62
2  libsystem_c.dylib              0x237680c5 abort + 108
3  libsystem_malloc.dylib         0x238040e9 free_list_checksum_botch + 362
4  libsystem_malloc.dylib         0x23804105 free_list_checksum_botch + 28
5  libsystem_malloc.dylib         0x237fbbff tiny_malloc_from_free_list + 202
6  libsystem_malloc.dylib         0x237fa987 szone_malloc_should_clear + 218
7  libsystem_malloc.dylib         0x237fa879 malloc_zone_malloc + 88
8  CoreFoundation                 0x23a4e291 _CFRuntimeCreateInstance + 236
9  CoreGraphics                   0x24e792a9 CGTypeCreateInstance + 20
10 CoreGraphics                   0x24db7d23 CGColorTransformCreate + 246
11 ImageIO                        0x2533c64b IIO_ConvertCGColorToColorComponents + 26
12 ImageIO                        0x252f5775 CGImagePixelDataProviderCreate + 276
13 ImageIO                        0x25319af1 CGImagePixelDataProviderCreateConforming + 1684
14 ImageIO                        0x252f418d CGImageDestinationAddImage + 2852
15 UIKit                          0x283fe4f5 _UIImageJPEGRepresentation + 620
16 MyApp                          0xf6785 -[UIImage(Additions) foo_scaledImageData] (Foo.m:1538)
17 MyApp                          0xf3ecd -[Foo bar] (Foo.m:731)
18 MyApp                          0xf3d29 __95-[Foo bar]_block_invoke_3 (Foo.m:691)
19 libdispatch.dylib              0x236d7cbf _dispatch_call_block_and_release + 10
20 libdispatch.dylib              0x236e36a1 _dispatch_root_queue_drain + 1572
21 libdispatch.dylib              0x236e307b _dispatch_worker_thread3 + 94
22 libsystem_pthread.dylib        0x23876e0d _pthread_wqthread + 1024
23 libsystem_pthread.dylib        0x238769fc start_wqthread + 8

崩溃 2,线程 2:

com.apple.root.background-qos
0  libsystem_malloc.dylib         0x238041d8 free_tiny_botch
1  CoreFoundation                 0x23b6f560 __CFBasicHashRehash + 2968
2  CoreFoundation                 0x23b706d4 __CFBasicHashAddValue + 100
3  CoreFoundation                 0x23a4f933 CFDictionarySetValue + 206
4  UIKit                          0x28a87ffb _UITraitCollectionCacheForBuiltinStorage + 146
5  UIKit                          0x28a8a2a7 +[UITraitCollection traitCollectionWithDisplayScale:] + 50
6  UIKit                          0x2812390f -[UIImage initWithCGImage:scale:orientation:] + 214
7  UIKit                          0x2812382b +[UIImage imageWithCGImage:scale:orientation:] + 62
8  MyApp                          0xf676d -[UIImage(Additions) foo_scaledImageData] (Foo.m:1527)
9  MyApp                          0xf3ecd -[Foo bar] (Foo.m:731)
10 MyApp                          0xf3d29 __95-[Foo bar]_block_invoke_3 (Foo.m:691)
11 libdispatch.dylib              0x236d7cbf _dispatch_call_block_and_release + 10
12 libdispatch.dylib              0x236e36a1 _dispatch_root_queue_drain + 1572
13 libdispatch.dylib              0x236e307b _dispatch_worker_thread3 + 94
14 libsystem_pthread.dylib        0x23876e0d _pthread_wqthread + 1024
15 libsystem_pthread.dylib        0x238769fc start_wqthread + 8

崩溃 2,线程 2 看起来像崩溃 1,线程 1,这让我认为它们是相关的,但崩溃 2,线程 2 是异常值。我有很多其他崩溃看起来像没有线程 2 堆栈的崩溃 2,这让我认为 UIImageJPEGRepresentation 可能也不是线程安全的。

这看起来类似于 AFNetworking issue

I can't name the source, but it's being taken care of. (or at least a radar has been filed by someone inside Apple)

Side note: I've looked at the UIKit disassembly and that API calls up traitCollection and a few other things and really is everything but thread safe; more so it really should only be called on the main thread ideally. We could swizzle and add the lock directly into the implementation, that would at least remove the fact that any non-AFNetworking consumer would still call this without the lock and might trigger the race condition; but it's also very much not pretty.

Using CGImageSource directly is more work but might be the better solution.

正如 AFNetworking 问题提到的和堆栈跟踪显示的那样,这看起来像 -[UIImage initWithCGImage:scale:orientation:] 调用到 UITraitCollection。我在 UITraitCollection.hUITraitCollection documentation 中找不到任何表明 UITraitCollection 是线程安全的内容。

因此,即使 UIImage documentationUIImage 是线程安全的:

Because image objects are immutable, you cannot change their properties after creation. Most image properties are set automatically using metadata in the accompanying image file or image data. The immutable nature of image objects also means that they are safe to use from any thread.

文档必须表明它们在主线程上创建后可以安全使用,或者如果它们只是连续创建则可以安全使用。 (这就是 AFNetworking fixed their issue。)对于我的解决方法,我改用 CoreGraphics:

CGImageRef image = ...
CGSize scaledSize = ...

CGContextRef context = CGBitmapContextCreate(NULL,
                                             scaledSize.width,
                                             scaledSize.height,
                                             CGImageGetBitsPerComponent(image),
                                             CGImageGetBytesPerRow(image),
                                             CGImageGetColorSpace(image),
                                             CGImageGetAlphaInfo(image));

CGImageRef scaledImage;
if (context)
{
    CGContextDrawImage(context,
                       CGRectMake(0,
                                  0,
                                  scaledSize.width,
                                  scaledSize.height),
                       image);
    scaledImage = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
}
else
{
    scaledImage = NULL;
}

然后,为了避免在后台线程上使用 UIImageJPEGRepresentation,我使用 ImageIO 导出 JPEG 数据:

NSData *NSDataFromCGImage(CGImageRef scaledImage) 
{
    NSMutableData *scaledImageMutableData = scaledImage ? [NSMutableData new] : nil;
    CGDataConsumerRef scaledImageDataConsumer = scaledImageMutableData ? CGDataConsumerCreateWithCFData((__bridge CFMutableDataRef) scaledImageMutableData) : NULL;
    // from the iOS 9.3 header:
    // The `options' dictionary is reserved for future use; currently, you
    // should pass NULL for this parameter.
    CGImageDestinationRef scaledImageDestination = scaledImageDataConsumer ? CGImageDestinationCreateWithDataConsumer(scaledImageDataConsumer,
                                                                                                                      kUTTypeJPEG,
                                                                                                                      1,
                                                                                                                      NULL) : NULL;
    BOOL success = (^BOOL ()
                    {
                        if (scaledImageDestination)
                        {
                            NSDictionary *addImageOptions = @{(__bridge NSString *)kCGImageDestinationLossyCompressionQuality : @(0.9),
                                                              };
                            CGImageDestinationAddImage(scaledImageDestination,
                                                       scaledImage,
                                                       (__bridge CFDictionaryRef)addImageOptions);

                            return (BOOL) CGImageDestinationFinalize(scaledImageDestination);
                        }
                        else
                        {
                            return NO;
                        }
                    })();
    if (scaledImageDestination)
    {
        CFRelease(scaledImageDestination);
    }
    CGDataConsumerRelease(scaledImageDataConsumer);

    if (success)
    {
        return scaledImageMutableData;
    }
    else
    {
        return nil;
    }
}