Zombie IOSurface Objects with SwiftUI and ImageIO

I’m trying to use the new ImageIO API for animating some gifs in a SwiftUI app. I created a wrapper in Objective-C to handle the ImageIO API:


//  ImageFrameScheduler.h

@import CoreGraphics;
@import Foundation;

NS_ASSUME_NONNULL_BEGIN

@interface ImageFrameScheduler : NSObject

@property (readonly) NSURL *imageURL;

- (instancetype)initWithURL:(NSURL *)imageURL NS_DESIGNATED_INITIALIZER;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

- (BOOL)startWithFrameHandler:(void (^)(NSInteger, CGImageRef))handler
                        error:(NSError * _Nullable *)error;

- (void)stop;

@end

NS_ASSUME_NONNULL_END


//  ImageFrameScheduler.m

#import "ImageFrameScheduler.h"

@import ImageIO;

@interface ImageFrameScheduler()

@property (readwrite) NSURL *imageURL;
@property (getter=isStopping) BOOL stopping;

@end

@implementation ImageFrameScheduler

- (instancetype)initWithURL:(NSURL *)imageURL
{
    self = [super init];
 
    if (self) {
        _imageURL = imageURL;
        _stopping = NO;
    }
 
    return self;
}

- (BOOL)startWithFrameHandler:(void (^)(NSInteger, CGImageRef _Nonnull))handler
                        error:(NSError * _Nullable *)error
{
    __weak typeof(self) welf = self;
 
    CFURLRef urlRef = (__bridge CFURLRef)self.imageURL;
 
    OSStatus status = CGAnimateImageAtURLWithBlock(urlRef,
                                                   nil,
                                                   ^(size_t index,
                                                     CGImageRef _Nonnull image,
                                                     bool* _Nonnull stop) {
        if (welf == nil || welf.stopping) {
            *stop = true;
            return;
        }
     
        handler(index, image);
    });
 
    if (status != noErr) {
        if (error != NULL) {
            *error = [NSError errorWithDomain:NSOSStatusErrorDomain
                                         code:status
                                     userInfo:nil];
        }
    }
 
    return status == noErr;
}

- (void)stop
{
    self.stopping = YES;
}

@end


When I finish loading gifs from the network, I use this in a SwiftUI Image, called from my view body’s onAppear property:


    private func startAnimating() {
        imageFrameScheduler = ImageFrameScheduler(url: cachedImageURL)
       
        do {
            try imageFrameScheduler?.start { (_, image) in
                self.image = Image(decorative: image, scale: 1)
            }
        }
        catch {
            print("Error animating image: \(error)")
        }
    }


These gifs are in a List, and if I scroll quickly, I eventually get a crash on an IOSurface zombie object:


*** -[IOSurface retain]: message sent to deallocated instance 0x60200004de50


Is there something I’m missing in the memory management of the SwiftUI Image struct or the ImageIO framework?

Accepted Reply

I think I figured it out. In the callback for the animation frame, I’m setting an image property to an Image struct with the CGImage I get from ImageIO. As you scroll, if a row comes back into view that had a cached CGImage from this animation block, and the animation has since been stopped, then accessing it crashed. So, setting the image property back to nil when I stop the animation fixed the issue.

Replies

Also, if it helps, I’m getting some output in the Xcode console while this happens:


0000000A:  0100  4          4          128
00000016:  0101  4          4          128
00000022:  0102  3          6          110
0000002E:  011A  5          8          116
0000003A:  011B  5          8          124
00000046:  0128  3          2            2
00000052:  0131  2         12          132
0000005E:  0132  2         20          144
000000A6:  0100  4          4          256
000000B2:  0101  4          4          256
000000BE:  0102  3          6          266
000000CA:  0103  3          2            6
000000D6:  0106  3          2            6
000000E2:  0115  3          2            3
000000EE:  0201  4          4          272
000000FA:  0202  4          4         3957
0000000A:  0100  4          4          128
00000016:  0101  4          4          128
00000022:  0102  3          6          110
0000002E:  011A  5          8          116
0000003A:  011B  5          8          124
00000046:  0128  3          2            2
00000052:  0131  2         12          132
0000005E:  0132  2         20          144
000000A6:  0100  4          4          256
000000B2:  0101  4          4          256
000000BE:  0102  3          6          266
000000CA:  0103  3          2            6
000000D6:  0106  3          2            6
000000E2:  0115  3          2            3
000000EE:  0201  4          4          272
000000FA:  0202  4          4         3957

I think I figured it out. In the callback for the animation frame, I’m setting an image property to an Image struct with the CGImage I get from ImageIO. As you scroll, if a row comes back into view that had a cached CGImage from this animation block, and the animation has since been stopped, then accessing it crashed. So, setting the image property back to nil when I stop the animation fixed the issue.