我正在尝试解决创建包含大量帧的GIF时的性能问题。例如,一些GIF可能包含> 1200帧。使用我的当前代码会耗尽内存。我正试图想出如何解决这个问题;是否可以分批处理?我的第一个想法是将图像拼接在一起,但我不认为有一种方法可以实现或者GIF是如何由ImageIO
框架创建的。如果有复数CGImageDestinationAddImages
方法就好了,但是没有,所以我迷失在如何尝试解决这个问题的路上。感谢提供的任何帮助。抱歉事先写了这么长的代码,但我觉得有必要显示逐步创建GIF。
只要视频中的不同GIF帧延迟在视频中也是可能的,并且录制的时间不会超过每帧动画的总和,那么用视频文件代替GIF是可以接受的。
注意:跳转到下面的“最新更新”标题以跳过背景故事。
更新1-6:使用GCD
修复了线程锁定问题,但内存问题仍然存在。100%的CPU利用率不是这里的问题,在执行工作时我显示UIActivityIndicatorView
。使用drawViewHierarchyInRect
方法可能比renderInContext
方法更高效/快速,但是我发现您无法在设置为YES的afterScreenUpdates
属性的后台线程上使用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.
这让我认为我的想法行不通,但我还是试了。继续进行第三次更新。
第三次更新:
我已经尝试了以下更新后的代码中的CGImageDestinationCopyImageSource
方法,但我一直只得到一个帧的图像;这很可能是由于上述第2个更新中的文档所述。也许还有另一种方法可以尝试:使用CGImageSourceCreateIncremental
,但我怀疑那不是我需要的。
看起来我需要一种将GIF帧逐步写入/附加到磁盘的方式,以便我可以将每个新块清除出内存。也许使用适当的回调函数创建CGImageDestinationCreateWithDataConsumer
以逐步保存数据会更理想?
第4次更新:
我开始尝试使用CGImageDestinationCreateWithDataConsumer
方法,看看是否可以使用NSFileHandle
将字节按它们进入的顺序写出,但问题仍然是调用CGImageDestinationFinalize
会一次性发送所有字节,这与之前相同 - 我的内存不足。我真的需要帮助解决这个问题,并将提供大量赏金。
第5次更新:
我发布了大量赏金。我希望看到一些辉煌的解决方案,而无需第三方库或框架来将原始的NSData
GIF字节附加到彼此并使用NSFileHandle
逐步写出它 - 基本上是手动创建GIF。或者,如果您认为可以使用像我尝试过的ImageIO
找到的解决方案那将是令人惊奇的。交换、子类化等。
第6次更新:
我一直在研究如何在最低级别制作GIF,并编写了一个小测试,与赏金的帮助相似。我需要抓取渲染的UIImage,从中获取字节,使用LZW压缩它并附加字节以及其他工作,例如确定全局颜色表。信息来源:http://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html。
最新更新:
我已经花了整个星期从各个角度研究这个问题,以了解基于限制(例如最多256种颜色)构建良好质量的GIF所需的所有步骤。我认为并假设ImageIO
正在在幕后创建一个单个位图上下文,将所有图像帧合并为一个,并对该位图执行颜色量化以生成用于GIF的单个全局颜色表。使用十六进制编辑器查看一些成功从ImageIO
创建的GIF确认它们具有全局颜色表,并且除非您自己为每个帧设置它,否则永远不会有本地颜色表。我相信这个巨大的位图进行颜色量化来构建调色板(再次假设,但强烈相信)。
我有这么一个奇怪而疯狂的想法:我的应用程序中的框架图像每个帧只能相差一个颜色,更好的是,我知道我的应用程序使用了哪些小型颜色集。第一/背景帧是包含用户提供内容(如照片)的颜色,因此我的想法是:我将拍摄此视图的快照,然后再拍摄另一个具有我应用程序处理的已知颜色的视图,并将其作为单个位图上下文,可以传递到 normalImaegIO
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];
}
CGImageSource
,将来自该源的图像添加到目标中,释放该源。在完成目标后,删除临时图像文件。这样做的想法是图像由文件支持,因此它们不必一直存在于内存中。它们可以根据需要清除并重新加载。 - Ken ThomasesCGImageDestinationFinalize
写入磁盘之前,需要将所有图像存储在RAM中。 - klcjr89