【发布时间】:2015-06-25 17:03:50
【问题描述】:
我正在尝试解决创建具有大量帧的 GIF 时的性能问题。例如,一些 GIF 可能包含 > 1200 帧。使用我当前的代码,我的内存不足。我试图弄清楚如何解决这个问题;这可以分批完成吗?我的第一个想法是是否可以将图像附加在一起,但我不认为有一种方法可以解决这个问题,或者ImageIO 框架是如何创建 GIF 的。如果有复数 CGImageDestinationAddImages 方法但没有,那就太好了,所以我不知道如何尝试解决这个问题。我感谢提供的任何帮助。很抱歉代码冗长,但我觉得有必要展示一步一步创建 GIF。
我可以制作视频文件而不是 GIF,只要视频中可能存在不同的 GIF 帧延迟并且录制所需的时间不会与每帧中所有动画的总和一样长。
p>注意:跳至下方的最新更新标题可跳过背景故事。
更新 1 - 6:
使用GCD 修复了线程锁,但内存问题仍然存在。 100% 的 CPU 利用率在这里不是问题,因为我在执行工作时显示UIActivityIndicatorView。使用drawViewHierarchyInRect 方法可能比renderInContext 方法更有效/更快,但是我发现您不能在afterScreenUpdates 属性设置为YES 的后台线程上使用drawViewHierarchyInRect 方法;它锁定了线程。
必须有某种方法可以批量写入 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、子类化等。
更新 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];
}
【问题讨论】:
-
关键字:@autoreleasepool - stackoverflow.com/search?q=[objective-c]%40autoreleasepool
-
是的,但删除它也无济于事。
-
找到插入池的正确位置 - 您也可以嵌套它们。
-
您尝试过以下方法吗?对于每一帧:将帧图像写入单个图像文件,从该文件创建一个
CGImageSource,将该图像从该源添加到目标,释放源。完成目标后,删除临时图像文件。这个想法是图像由文件支持,因此它们不必一直在内存中。可以根据需要清除和重新加载它们。 -
我认为您在 ImageIO 上不会有太多的运气。你试过Giraffe吗?它是 ANGif library 的 Objective-C 包装器,它以增量方式写入输出文件(根据我的检查)。
标签: ios objective-c core-graphics gif cgimage