使用CGImageDestinationFinalize创建一个大型GIF - 内存不足

时间:2015-04-18 21:24:15

标签: ios objective-c core-graphics gif cgimage

我在创建包含大量帧的GIF时尝试解决性能问题。例如,一些GIF可以包含> 1200帧。使用我当前的代码,我的内存不足。我试图弄清楚如何解决这个问题;这可以分批完成吗?我的第一个想法是,是否有可能将图像附加在一起,但我认为没有一种方法可以用ImageIO框架创建GIF。如果有一个复数CGImageDestinationAddImages方法但是没有,那就太好了,所以我迷失了如何尝试解决这个问题。我感谢任何提供的帮助。对于冗长的代码提前抱歉,但我觉得有必要逐步创建GIF。

我可以制作视频文件而不是GIF,只要视频中可以有不同的GIF帧延迟,并且录制并不像每帧中所有动画的总和那样长。

注意:请跳至下方的最新更新标题以跳过背景故事。

更新1 - 6: 使用GCD修复了线程锁定,但仍然存在内存问题。这里不关心100%的CPU利用率,因为我在执行工作时显示UIActivityIndicatorView。使用drawViewHierarchyInRect方法可能比renderInContext方法更有效/更快,但我发现您无法在drawViewHierarchyInRect的后台线程上使用afterScreenUpdates方法} property设置为YES;它锁定了线程。

必须有一些方法可以分批编写GIF。我相信我已经将内存问题缩小到:CGImageDestinationFinalize这种方法对于制作包含大量帧的图像效率非常低,因为所有内容都必须在内存中写出整个图像。我已经证实了这一点,因为我在抓取渲染的containerView图层图像并调用CGImageDestinationAddImage时使用了很少的内存。 我打电话给CGImageDestinationFinalize时,记忆仪会立即响起;有时最高可达2GB,具体取决于帧数。所需的内存量似乎很疯狂,可以制作~20-1000KB的GIF。

更新2:  我发现有一种方法可能会带来一些希望。它是:

CGImageDestinationCopyImageSource(CGImageDestinationRef idst, 
CGImageSourceRef isrc, CFDictionaryRef options,CFErrorRef* err) 

我的新想法是,对于每10帧或其他任意帧数,我会将它们写入目的地,然后在下一个循环中,先前完成的10帧目标将成为我的新源。但是有一个问题;阅读文档,它说明了这一点:

Losslessly copies the contents of the image source, 'isrc', to the * destination, 'idst'. 
The image data will not be modified. No other images should be added to the image destination. 
* CGImageDestinationFinalize() should not be called afterward -
* the result is saved to the destination when this function returns.

这让我觉得我的想法不起作用,但是我试过了。继续更新3.

更新3: 我已经使用下面的更新代码尝试了CGImageDestinationCopyImageSource方法,但是我总是只用一帧取回图像;这是因为上面更新2中所述的文档很有可能。还有一种方法可能尝试:CGImageSourceCreateIncremental但我怀疑这是我需要的。

似乎我需要一些方法来逐步将GIF帧写入/附加到磁盘,这样我就可以清除每个新块的内存。也许CGImageDestinationCreateWithDataConsumer具有适当的回调来逐步保存数据是理想的吗?

更新4: 我开始尝试使用CGImageDestinationCreateWithDataConsumer方法来查看是否可以管理在使用NSFileHandle时输入的字节,但问题是调用CGImageDestinationFinalize会发送所有字节在一次拍摄中与以前相同 - 我耗尽了记忆。我真的需要帮助才能解决这个问题并提供大笔奖金。

更新5: 我发布了大笔奖金。我希望看到一些没有第三方库或框架的出色解决方案,可以将原始的NSData GIF字节相互附加,并以NSFileHandle的形式逐步写入磁盘 - 实际上是手动创建GIF。或者,如果您认为使用ImageIO找到了一个解决方案,就像我尝试过的那样,这也是一件很棒的事情。 Swizzling,subclassing等。

更新6: 我一直在研究GIF是如何在最低级别制作的,我写了一个小小的测试,这与我在赏金方面的帮助有关。我需要抓取渲染的UIImage,从中获取字节,使用LZW压缩它,并附加字节以及其他一些工作,如确定全局颜色表。信息来源:http://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html

最新更新:

我花了整整一周的时间从各个角度研究这个问题,看看究竟是为了根据限制(例如最多256色)构建质量合格的GIF。我相信并假设ImageIO正在做的是在引擎盖下创建一个位图上下文,所有图像帧合并为一个,并在此位图上执行颜色量化以生成要在GIF中使用的单个全局颜色表。在由ImageIO制作的一些成功的GIF上使用十六进制编辑器确认它们具有全局颜色表,并且从不拥有​​本地颜色表,除非您自己为每个帧设置它。在这个巨大的位图上执行颜色量化以构建调色板(再次假设,但强烈相信)。

我有这个奇怪而疯狂的想法:我的应用程序中的帧图像每帧只能有一种颜色,甚至更好,我知道我的应用程序使用的是哪些小颜色。第一个/背景框架是一个框架,其中包含我无法控制的颜色(用户提供的内容,如照片),所以我想的是我将对此视图进行快照,然后快照另一个具有已知颜色的视图应用程序处理并使其成为单个位图上下文,我可以将其传递给正常的ImaegIO GIF制作例程。优势是什么?好吧,通过将两个图像合并为一个图像,可以将其从约1200帧降低到一帧。然后,ImageIO将在更小的位图上执行其操作,并使用一帧写出单个GIF。

现在我该怎么做才能构建实际的1200帧GIF?我认为我可以采用单帧GIF并很好地提取颜色表字节,因为它们位于两个GIF协议块之间。我仍然需要手动构建GIF,但现在我不必计算调色板。我将窃取调色板ImageIO认为最好并将其用于我的字节缓冲区。我仍然需要一个LZW压缩器实现和赏金的帮助,但这应该比颜色量化更容易,这可能会非常缓慢。 LZW也可能很慢,所以我不确定它是否值得;不知道LZW将如何按顺序执行~1200帧。

你有什么想法?

@property (nonatomic, strong) NSFileHandle *outputHandle;    

- (void)makeGIF
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^
    {
        NSString *filePath = @"/Users/Test/Desktop/Test.gif";

        [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];

        self.outputHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];

        NSMutableData *openingData = [[NSMutableData alloc]init];

        // GIF89a header

        const uint8_t gif89aHeader [] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 };

        [openingData appendBytes:gif89aHeader length:sizeof(gif89aHeader)];


        const uint8_t screenDescriptor [] = { 0x0A, 0x00, 0x0A, 0x00, 0x91, 0x00, 0x00 };

        [openingData appendBytes:screenDescriptor length:sizeof(screenDescriptor)];


        // Global color table

        const uint8_t globalColorTable [] = { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00 };

        [openingData appendBytes:globalColorTable length:sizeof(globalColorTable)];


        // 'Netscape 2.0' - Loop forever

        const uint8_t applicationExtension [] = { 0x21, 0xFF, 0x0B, 0x4E, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2E, 0x30, 0x03, 0x01, 0x00, 0x00, 0x00 };

        [openingData appendBytes:applicationExtension length:sizeof(applicationExtension)];

        [self.outputHandle writeData:openingData];

        for (NSUInteger i = 0; i < 1200; i++)
        {
            const uint8_t graphicsControl [] = { 0x21, 0xF9, 0x04, 0x04, 0x32, 0x00, 0x00, 0x00 };

            NSMutableData *imageData = [[NSMutableData alloc]init];

            [imageData appendBytes:graphicsControl length:sizeof(graphicsControl)];


            const uint8_t imageDescriptor [] = { 0x2C, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x00 };

            [imageData appendBytes:imageDescriptor length:sizeof(imageDescriptor)];


            const uint8_t image [] = { 0x02, 0x16, 0x8C, 0x2D, 0x99, 0x87, 0x2A, 0x1C, 0xDC, 0x33, 0xA0, 0x02, 0x75, 0xEC, 0x95, 0xFA, 0xA8, 0xDE, 0x60, 0x8C, 0x04, 0x91, 0x4C, 0x01, 0x00 };

            [imageData appendBytes:image length:sizeof(image)];


            [self.outputHandle writeData:imageData];
        }


        NSMutableData *closingData = [[NSMutableData alloc]init];

        const uint8_t appSignature [] = { 0x21, 0xFE, 0x02, 0x48, 0x69, 0x00 };

        [closingData appendBytes:appSignature length:sizeof(appSignature)];


        const uint8_t trailer [] = { 0x3B };

        [closingData appendBytes:trailer length:sizeof(trailer)];


        [self.outputHandle writeData:closingData];

        [self.outputHandle closeFile];

        self.outputHandle = nil;

        dispatch_async(dispatch_get_main_queue(),^
        {
           // Get back to main thread and do something with the GIF
        });
    });
}

- (UIImage *)getImage
{
    // Read question's 'Update 1' to see why I'm not using the
    // drawViewHierarchyInRect method
    UIGraphicsBeginImageContextWithOptions(self.containerView.bounds.size, NO, 1.0);
    [self.containerView.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *snapShot = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // Shaves exported gif size considerably
    NSData *data = UIImageJPEGRepresentation(snapShot, 1.0);

    return [UIImage imageWithData:data];
}

2 个答案:

答案 0 :(得分:10)

如果将kCGImagePropertyGIFHasGlobalColorMap设置为NO,则不会发生内存不足。

答案 1 :(得分:6)

您可以使用AVFoundation为图像编写视频。我上传了一个完整的工作测试项目to this github repository。在模拟器中运行测试项目时,它将打印到调试控制台的文件路径。在视频播放器中打开该路径以检查输出。

我将在这个答案中介绍代码的重要部分。

首先创建AVAssetWriter。我会给它AVFileTypeAppleM4V文件类型,以便视频在iOS设备上运行。

AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:self.url fileType:AVFileTypeAppleM4V error:&error];

使用视频参数设置输出设置字典:

- (NSDictionary *)videoOutputSettings {
    return @{
             AVVideoCodecKey: AVVideoCodecH264,
             AVVideoWidthKey: @((size_t)size.width),
             AVVideoHeightKey: @((size_t)size.height),
             AVVideoCompressionPropertiesKey: @{
                     AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline31,
                     AVVideoAverageBitRateKey: @(1200000) }};
}

您可以调整比特率来控制视频文件的大小。我在这里非常保守地选择了编解码器配置文件(it supports some pretty old devices)。您可能想要选择以后的个人资料。

然后创建一个媒体类型为AVAssetWriterInput的{​​{1}}和您的输出设置。

AVMediaTypeVideo

设置像素缓冲区属性字典:

NSDictionary *outputSettings = [self videoOutputSettings];
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];

您不必在此指定像素缓冲区尺寸; AVFoundation将从输入的输出设置中获取它们。我在这里使用的属性(我相信)是使用Core Graphics绘图的最佳选择。

接下来,使用像素缓冲设置为输入创建- (NSDictionary *)pixelBufferAttributes { return @{ fromCF kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA), fromCF kCVPixelBufferCGBitmapContextCompatibilityKey: @YES }; }

AVAssetWriterInputPixelBufferAdaptor

将输入添加到编写器并告诉编写者继续:

AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
    assetWriterInputPixelBufferAdaptorWithAssetWriterInput:input
    sourcePixelBufferAttributes:[self pixelBufferAttributes]];

接下来我们将告诉输入如何获取视频帧。是的,在我们告诉作者开始写作之后我们可以做到这一点:

[writer addInput:input];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];

这个块将完成我们需要用AVFoundation做的所有其他事情。每次输入准备接受更多数据时,输入都会调用它。它可能能够在一次调用中接受多个帧,因此只要它准备就绪,我们就会循环:

    [input requestMediaDataWhenReadyOnQueue:adaptorQueue usingBlock:^{

我正在使用 while (input.readyForMoreMediaData && self.frameGenerator.hasNextFrame) { 来实际绘制帧。我稍后会显示该代码。 self.frameGenerator决定视频何时结束(通过从frameGenerator返回NO)。它还知道每个帧何时出现在屏幕上:

hasNextFrame

要实际绘制帧,我们需要从适配器获取像素缓冲区:

            CMTime time = self.frameGenerator.nextFramePresentationTime;

如果我们无法获得像素缓冲区,我们会发出错误信号并中止所有内容。如果我们确实获得了像素缓冲区,我们需要在其周围包含一个位图上下文,并要求 CVPixelBufferRef buffer = 0; CVPixelBufferPoolRef pool = adaptor.pixelBufferPool; CVReturn code = CVPixelBufferPoolCreatePixelBuffer(0, pool, &buffer); if (code != kCVReturnSuccess) { errorBlock([self errorWithFormat:@"could not create pixel buffer; CoreVideo error code %ld", (long)code]); [input markAsFinished]; [writer cancelWriting]; return; } else { 在上下文中绘制下一帧:

frameGenerator

现在我们可以将缓冲区附加到视频中。适配器执行:

                CVPixelBufferLockBaseAddress(buffer, 0); {
                    CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); {
                        CGContextRef gc = CGBitmapContextCreate(CVPixelBufferGetBaseAddress(buffer), CVPixelBufferGetWidth(buffer), CVPixelBufferGetHeight(buffer), 8, CVPixelBufferGetBytesPerRow(buffer), rgb, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); {
                            [self.frameGenerator drawNextFrameInContext:gc];
                        } CGContextRelease(gc);
                    } CGColorSpaceRelease(rgb);

上面的循环通过适配器推送帧,直到输入表明它已经足够,或者直到 [adaptor appendPixelBuffer:buffer withPresentationTime:time]; } CVPixelBufferUnlockBaseAddress(buffer, 0); } CVPixelBufferRelease(buffer); } 表示它没有帧。如果frameGenerator有更多帧,我们只返回,输入会在准备好更多帧时再次给我们打电话:

frameGenerator

如果 if (self.frameGenerator.hasNextFrame) { return; } 超出了帧数,我们会关闭输入:

frameGenerator

然后我们告诉作者完成。它完成后会调用一个完成处理程序:

        [input markAsFinished];

相比之下,生成帧非常简单。这是发电机采用的协议:

        [writer finishWritingWithCompletionHandler:^{
            if (writer.status == AVAssetWriterStatusFailed) {
                errorBlock(writer.error);
            } else {
                dispatch_async(dispatch_get_main_queue(), doneBlock);
            }
        }];
    }];

对于我的测试,我绘制了一张背景图片,并在视频播放过程中用红色慢慢覆盖它。

@protocol DqdFrameGenerator <NSObject>

@required

// You should return the same size every time I ask for it.
@property (nonatomic, readonly) CGSize frameSize;

// I'll ask for frames in a loop. On each pass through the loop, I'll start by asking if you have any more frames:
@property (nonatomic, readonly) BOOL hasNextFrame;

// If you say NO, I'll stop asking and end the video.

// If you say YES, I'll ask for the presentation time of the next frame:
@property (nonatomic, readonly) CMTime nextFramePresentationTime;

// Then I'll ask you to draw the next frame into a bitmap graphics context:
- (void)drawNextFrameInContext:(CGContextRef)gc;

// Then I'll go back to the top of the loop.

@end

核心图形将原点放在位图上下文的左下角,但是我使用的是@implementation TestFrameGenerator { UIImage *baseImage; CMTime nextTime; } - (instancetype)init { if (self = [super init]) { baseImage = [UIImage imageNamed:@"baseImage.jpg"]; _totalFramesCount = 100; nextTime = CMTimeMake(0, 30); } return self; } - (CGSize)frameSize { return baseImage.size; } - (BOOL)hasNextFrame { return self.framesEmittedCount < self.totalFramesCount; } - (CMTime)nextFramePresentationTime { return nextTime; } ,而UIKit喜欢将原点放在左上角。

UIImage

我调用我的测试程序用来更新进度指示器的回调:

- (void)drawNextFrameInContext:(CGContextRef)gc {
    CGContextTranslateCTM(gc, 0, baseImage.size.height);
    CGContextScaleCTM(gc, 1, -1);
    UIGraphicsPushContext(gc); {
        [baseImage drawAtPoint:CGPointZero];

        [[UIColor redColor] setFill];
        UIRectFill(CGRectMake(0, 0, baseImage.size.width, baseImage.size.height * self.framesEmittedCount / self.totalFramesCount));
    } UIGraphicsPopContext();

    ++_framesEmittedCount;

最后,为了演示可变帧速率,我以每秒30帧的速度发射帧的前半部分,后半部分以每秒15帧的速度发射:

    if (self.frameGeneratedCallback != nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.frameGeneratedCallback();
        });
    }