强制异步任务按顺序运行

Posted

技术标签:

【中文标题】强制异步任务按顺序运行【英文标题】:Force async tasks to run in sequence 【发布时间】:2013-11-28 07:05:05 【问题描述】:

我正在使用ALAssetsLibrary 将图像保存到照片库。当它进入循环时,它会同时运行并导致内存问题。如何在不导致内存问题的情况下运行它

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
for(UIImage *image in images)
    [library writeImageToSavedPhotosAlbum:[image CGImage] 
                              orientation:(ALAssetOrientation)[image imageOrientation]       
                          completionBlock:^(NSURL *assetURL, NSError *error) 
    
        ... //
    ];

【问题讨论】:

重写您的代码,以便在调用第一个图像的完成块后保存第二个图像,依此类推。 我尝试以递归方式编写它,但仍然收到内存警告 【参考方案1】:

如果您想确保这些写入连续发生,您可以使用信号量等待图像完成,然后再开始下一次写入:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

for (UIImage *image in images) 
    [library writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error) 
        dispatch_semaphore_signal(semaphore);                  // signal when done
    ];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // wait for signal before continuing

而且,由于您可能不想在此过程中阻塞主队列,因此您可能希望将整个事情分派到某个后台队列:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    for (UIImage *image in images) 
        [library writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error) 
            dispatch_semaphore_signal(semaphore);                  // signal when done
        ];
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // wait for signal before continuing
    
);

您也可以将这个writeImageToSavedPhotosAlbum 包装在一个自定义的NSOperation 中,直到完成块才发布isFinished,但这对我来说似乎有点矫枉过正。


话虽如此,我还是有点担心images 的这个数组,你在内存中同时保存了所有UIImage 对象。如果它们很大,或者如果你有很多图像,那可能会有问题。通常你会想要简单地维护一组图像名称,然后一次实例化一个图像,例如:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    for (NSString *path in imagePaths) 
        @autoreleasepool 
            ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];

            UIImage *image = [UIImage imageWithContentsOfFile:path];

            [library writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error) 
                dispatch_semaphore_signal(semaphore);                  // signal when done
            ];

            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // wait for signal before continuing
        
    
);

人们通常应该警惕任何假定同时在内存中保存一组大型对象(如图像)的编码模式。

如果您需要经常访问这些图像,但又不想每次都从持久存储中重新检索,您可以使用NSCache 模式(例如,尝试从缓存中检索图像;如果找不到,从持久存储中检索并将其添加到缓存中;在内存压力下,清空缓存),这样您就可以享受将图像保存在内存中的性能优势,但可以优雅地处理图像缓存消耗过多内存的情况。

【讨论】:

谢谢你,我认为我的代码在你的帮助下得到了改进。但是保存3张图片后它仍然会关闭 它关闭并在 Xcode 中生成错误消息说“由于内存压力而终止”。 BTW 刚刚发现同时调用 library writeImageToSavedPhotosAlbum 会在每次调用后导致更高的内存峰值,无论调用的时间间隔如何。 当 Xcode 记录“因内存压力而终止”时,您的应用程序是否在后台? @DulguunLst 然后您可以通过将库的实例化移动到自动释放池中来解决这个问题。这可能会对性能造成适度的影响(为每个图像重新实例化它),但应该可以解决内存问题。请参阅修改后的答案(最后一段代码)。 尝试将实例化放入自动释放池并在每次完成后将其设置为 Nil 并在下一个图像上重新创建它。结果同样开始怀疑这是 xcode 或 ios 错误。或者我错过了一些清除一些未知内容的电话。现在真的很困惑。【参考方案2】:

另一种方法是实现“异步循环”:

typedef void(^completion_t)(id result, NSError* error);

- (void) saveImages:(NSMutableArray*)images 
     toAssetLibrary:(ALAssetsLibrary*)library 
         completion:(completion_t)completionHandler

    if ([images count] > 0) 
        UIImage* image = [images firstObject];
        [images removeObjectAtIndex:0];
        
        [library writeImageToSavedPhotosAlbum:[image CGImage]
                                  orientation:(ALAssetOrientation)[image imageOrientation]
                              completionBlock:^(NSURL *assetURL, NSError *error)
        
            if (error) 
                
            
            else 
            
            [self saveImages:images toAssetLibrary:library completion:completionHandler];
        ];
    
    else 
        // finished
        if (completionHandler) 
            completionHandler(@"Images saved", nil);
        
    

注意:

方法saveImages:toAssetLibrary:completion:是一个异步方法。

按顺序处理图像列表。

当所有图像都保存后,将调用完成处理程序。

为了完成这个,上面的实现在writeImageToSavedPhotosAlbum:orientation:completionBlock:的完成处理程序中调用自己。

不过,这不是递归方法调用:当完成处理程序调用方法 saveImages:toAssetLibrary:completion: 时,该方法已经返回。

可能的改进:

为简洁起见,示例没有错误处理。这应该在实际实现中得到改进。

最好不要使用图像列表,而是使用图像的 URL 列表。

用法

你可以这样使用它:

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];   
[self saveImages:[self.images mutableCopy] toAssetLibrary:library 
completion:^(id result, NSError*error) 
    ... 
];

【讨论】:

我实现了类似的东西,但没有完成处理程序,它仍然导致内存警告。并尝试了你的,仍然引起内存警告。我认为这是因为这个 ALAssetsLibrary。这是多么令人讨厌的事情。 也许使用 URL 列表而不是图像列表? (参见 Rob 的第二个建议) 是的,我使用 URL 列表作为 NSStrings。但我想我可能没有发现什么改进。尝试让方法创建它自己的 ALAssetsLibrary 并将其设置为Nil,然后再次调用自身。应用程序收到内存警告但有时没有关闭。 BTW 刚刚发现同时调用 library writeImageToSavedPhotosAlbum 会在每次调用后导致更高的内存峰值,无论调用的时间间隔如何。 @DulguunLst 确保,writeImageToSavedPhotosAlbum:.. 将一个接一个地被调用。确保在写入资产库之前使用 URL 并加载图像。完成对库的写入后,将图像设置为零。除此之外,使用自动释放池来包装所有这些方法,以确保释放临时对象。如果这没有帮助,您需要更深入地挖掘并尝试使用 Instruments 找出内存消耗的原因。【参考方案3】:

如果您希望更新使用 dispatch_async 代替。

// This will wait to finish
dispatch_async(dispatch_get_main_queue(), ^
    // Update the UI on the main thread.
);

【讨论】:

以上是关于强制异步任务按顺序运行的主要内容,如果未能解决你的问题,请参考以下文章

如何确保两个“开始”的异步任务在运行另一个任务之前完成?

06 Spring 异步执行,任务调度(@Schedule、@Async)

理解JS异步和单线程

软件架构:同步与异步问题

软件架构:同步与异步问题

使用异步任务将数据显示到列表视图时强制关闭应用程序