- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
在创建具有大量帧的 GIF 时,我正在尝试解决性能问题。例如,某些 GIF 可能包含 > 1200 帧。使用我当前的代码,我的内存不足。我正在想办法解决这个问题;这可以分批完成吗?我的第一个想法是是否可以将图像附加在一起,但我认为没有方法可以做到这一点,也不认为 ImageIO
是如何创建 GIF 的。框架。如果有复数形式就好了 CGImageDestinationAddImages
方法,但没有,所以我不知道如何尝试解决这个问题。我感谢提供的任何帮助。对冗长的代码提前表示抱歉,但我觉得有必要展示 GIF 的逐步创建。
我可以制作视频文件而不是 GIF,只要视频中可能存在不同的 GIF 帧延迟,并且录制时间不会与每帧中所有动画的总和一样长。
注意:跳转到下面的最新更新标题以跳过背景故事。
更新 1 - 6:
使用 GCD
修复了线程锁,但内存问题仍然存在。此处不关心 100% CPU 利用率,因为我显示了 UIActivityIndicatorView
在执行工作的同时。使用 drawViewHierarchyInRect
方法可能比 renderInContext
更有效/更快方法,但是我发现您不能使用 drawViewHierarchyInRect
使用 afterScreenUpdates
的后台线程上的方法属性设置为 YES;它锁定了线程。
必须有某种方式可以批量写出 GIF。我相信我已经将内存问题缩小到:CGImageDestinationFinalize
这种方法对于制作具有大量帧的图像似乎非常低效,因为所有内容都必须在内存中才能写出整个图像。我已经确认了这一点,因为我在抓取渲染的 containerView 层图像并调用 CGImageDestinationAddImage
时使用的内存很少。 .
我打电话的那一刻 CGImageDestinationFinalize
内存计立即飙升;有时根据帧数高达 2GB。制作大约 20-1000KB 的 GIF 所需的内存量似乎很疯狂。
更新 2:
我发现有一种方法可能会带来一些希望。这是:
CGImageDestinationCopyImageSource(CGImageDestinationRef idst,
CGImageSourceRef isrc, CFDictionaryRef options,CFErrorRef* err)
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
但我怀疑这就是我所需要的。
CGImageDestinationCreateWithDataConsumer
使用适当的回调来增量保存数据会是理想的吗?
CGImageDestinationCreateWithDataConsumer
方法来查看我是否可以使用
NSFileHandle
管理写入字节,但同样的问题是调用
CGImageDestinationFinalize
一次发送所有字节,这与以前相同 - 我用完了内存。我真的需要帮助来解决这个问题,并会提供一大笔赏金。
NSData
GIF 字节到彼此,并使用
NSFileHandle
增量地将其写入磁盘。 - 本质上是手动创建 GIF。或者,如果您认为可以使用
ImageIO
找到解决方案就像我尝试过的那样,那也太棒了。 Swizzling,子类化等。
ImageIO
正在做的是在引擎盖下创建一个单一的位图上下文,将所有图像帧合并为一个,并且正在对该位图执行颜色量化以生成要在 GIF 中使用的单个全局颜色表。在由
ImageIO
制作的一些成功的 GIF 上使用十六进制编辑器确认他们有一个全局颜色表,除非您自己为每一帧设置它,否则永远不会有本地颜色表。在这个巨大的位图上执行颜色量化以构建调色板(再次假设,但强烈相信)。
ImaegIO
的单个位图上下文GIF制作套路。有什么好处?好吧,通过将两个图像合并为一个图像,它可以从大约 1200 帧减少到 1 帧。
ImageIO
然后将在小得多的位图上做它的事情,并用一帧写出单个 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];
}
最佳答案
您可以使用 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) }};
}
AVAssetWriterInput
带媒体类型
AVMediaTypeVideo
和您的输出设置。
NSDictionary *outputSettings = [self videoOutputSettings];
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];
- (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];
[input requestMediaDataWhenReadyOnQueue:adaptorQueue usingBlock:^{
while (input.readyForMoreMediaData && self.frameGenerator.hasNextFrame) {
self.frameGenerator
实际绘制框架。稍后我将展示该代码。
frameGenerator
决定视频何时结束(通过从
hasNextFrame
返回 NO )。它还知道每个帧应该何时出现在屏幕上:
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;
}
UIImage
, 而 UIKit 喜欢把原点放在左上角。
- (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;
if (self.frameGeneratedCallback != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self.frameGeneratedCallback();
});
}
if (self.framesEmittedCount < self.totalFramesCount / 2) {
nextTime.value += 1;
} else {
nextTime.value += 2;
}
}
@end
关于ios - 使用 CGImageDestinationFinalize 创建大型 GIF - 内存不足,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29723024/
我的应用允许用户使用照片框架编辑照片。我看到一些崩溃报告,似乎是在生成输出图像时发生崩溃,但我不确定问题出在哪里。此崩溃发生在多个硬件设备和多个版本的 iOS 9 上,包括最新的 9.1。我的应用最后
在创建具有大量帧的 GIF 时,我正在尝试解决性能问题。例如,某些 GIF 可能包含 > 1200 帧。使用我当前的代码,我的内存不足。我正在想办法解决这个问题;这可以分批完成吗?我的第一个想法是是否
我正在尝试复制 uiimage 并更改元数据,而不重新编码图像,以免导致 jpg 伪影(我知道下面的代码重新编码,但这只是为了可以运行它进行测试)。 CGImageDestinationCopyIma
我正在尝试为更大的图像创建图 block ,似乎从 IOS 10 开始,以下代码不再有效并因 EXC_BAD_ACCESS 而崩溃。 这只发生在 IOS 10 设备上,IOS 9 工作正常。 任何大于
使用这个 block ,我的应用程序每次尝试都会崩溃 CGImageDestinationFinalize(gifDest!) 阻止 func createGIF(with images: [CGIm
编辑答案: - (UIImage*) maskImage:(UIImageView *)maskImage withMask:(UIImageView *)cropImage { UIImag
我是一名优秀的程序员,十分优秀!