代码请教:无法后台动态更新 UICollectionView

seedante 发布于 2014年03月22日 | 更新于 2014年04月03日
无人欣赏。

这个问题其实我今天在 stackoverflow 上提问了,但还一直没有人回答,我也没有找到类似的问题。如有违规,请@tinyfool 删帖。 我使用Face++的 iOS离线检测器来检测照片中的人脸,由于是直接检测 iPad 拍摄的图片,检测过程比较长,我希望在后台检测人脸,一旦检测到就更新到屏幕上,以下是我的代码。但检测到的人脸总是在最后一起更新,而不是我希望的那样,检测到一张就更新一张。

#import "FacesetController.h"
#import <AssetsLibrary/AssetsLibrary.h>
#import "FaceppLocalDetector.h"
#import "APIKey+APISecret.h"
#import "FaceppAPI.h"

@interface FacesetController ()
@property (nonatomic, strong) ALAssetsLibrary *photoLibrary;
@property (nonatomic, strong) NSMutableArray *allFaces;
//离线人脸检测器
@property (nonatomic, strong) FaceppLocalDetector *localDetector;
@end

@implementation FacesetController

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    if (self.photoLibrary == nil) {
        _photoLibrary = [[ALAssetsLibrary alloc] init];
    }
    if (self.allFaces == nil) {
        _allFaces = [[NSMutableArray alloc] init];
    }
    if (self.localDetector == nil) {
        NSDictionary *options = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@NO, @20, FaceppDetectorAccuracyHigh, nil] forKeys:[NSArray arrayWithObjects:FaceppDetectorTracking, FaceppDetectorMinFaceSize, FaceppDetectorAccuracy, nil]];
        _localDetector = [FaceppLocalDetector detectorOfOptions:options andAPIKey:_API_KEY];
    }

    ALAssetsGroupEnumerationResultsBlock assetsEnumerationBlock = ^(ALAsset *result, NSUInteger index, BOOL *stop){
        NSLog(@"Block: assetsEnumerationBlock");
        if (result) {
            @autoreleasepool {
                __autoreleasing ALAssetRepresentation *assetRepresentation = [result defaultRepresentation];
                CGImageRef fullImage = [assetRepresentation fullResolutionImage];
                __autoreleasing UIImage *fullResolutionImage = [UIImage imageWithCGImage:fullImage];
                FaceppLocalResult *detectResult = [_localDetector detectWithImage:fullResolutionImage];
                if (detectResult.faces.count > 0) {
                    NSLog(@"Find face in Photo: %@", assetRepresentation.filename);
                    for (FaceppLocalFace *face in detectResult.faces) {
                        CGImageRef faceCGImage = CGImageCreateWithImageInRect(fullImage, face.bounds);
                        __autoreleasing UIImage *faceImage = [UIImage imageWithCGImage:faceCGImage];
                        [self.allFaces addObject:faceImage];
                        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.allFaces.count-1 inSection:0];
                        [self.collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
                        CGImageRelease(faceCGImage);
                    }

                        CGImageRelease(fullImage);
                        NSLog(@"UPDATE THE MAIN SCREEN!");
                        //在主线程中更新屏幕
                        dispatch_async(dispatch_get_main_queue(), ^{
                            [self.collectionView reloadData];
                        });
                }
            }
        }
    };

    // setup our failure view controller in case enumerateGroupsWithTypes fails
    ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError *error) {
        NSLog(@"Some thing wrong");
    };

    ALAssetsLibraryGroupsEnumerationResultsBlock listGroupBlock = ^(ALAssetsGroup *group, BOOL *stop) {
        ALAssetsFilter *onlyPhotosFilter = [ALAssetsFilter allPhotos];
        [group setAssetsFilter:onlyPhotosFilter];
        [group enumerateAssetsUsingBlock:assetsEnumerationBlock];
    };

    // enumerate only photos
    NSUInteger groupTypes = ALAssetsGroupAlbum | ALAssetsGroupEvent | ALAssetsGroupFaces | ALAssetsGroupSavedPhotos;
    [self.photoLibrary enumerateGroupsWithTypes:groupTypes usingBlock:listGroupBlock failureBlock:failureBlock];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"Method: %@", NSStringFromSelector(_cmd));
}

#pragma mark - UICollectionViewDelegate

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.allFaces.count;
}

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return 1;
}

#define kImageViewTag 1

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"faceCell";
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
    if ([self.allFaces count]) {
        UIImageView *faceView = (UIImageView *)[cell viewWithTag:kImageViewTag];
        [faceView setImage:[self.allFaces objectAtIndex:[indexPath row]]];
    }
    return cell;
}

@end
共31条回复
lionlee 回复于 2014年03月22日

看上去没什么问题啊

seedante 回复于 2014年03月22日

1楼 @lionlee 我也这么觉得呢,可是所有图片全部都是最后一起出来,而我使用 NSLog来跟踪函数调用,发现每次在 data source 加入新的数据后,(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath也被调用了,可是 UI 上没有反应。

lionlee 回复于 2014年03月22日

在哪上面测试的?硬件,软件版本,都可能有差异,换个设备再试

seedante 回复于 2014年03月22日

3楼 @lionlee 我只有个 ipad mini 而且使用的 iOS离线检测器只能在真机上运行。

lionlee 回复于 2014年03月22日

第二代还是第一代,会不会是和这次的64位调用有关?

seedante 回复于 2014年03月22日

5楼 @lionlee iPad mini 1代,支持64bit,我觉得应该和这个没什么关系,可能是 GCD 的使用问题,我刚了解这个GCD。

lionlee 回复于 2014年03月22日

嗯,再找找吧,不过话说即使是dropbox到现在这个版本后台自动备份照片都常常有问题,很多人在说啊,但是他们的工程师到现在都还没找出问题,后台自动刷新方面可能Apple还没成熟,或者是调取过于频繁,总之几分钟之内就会被kill掉,你死的也不冤...

lionlee 回复于 2014年03月22日

btw ,能看到Diagnostics 吗 ?

seedante 回复于 2014年03月22日

8楼 @lionlee Diagnostics是调试用的吧 我还不会 暂时用 NSLog 来跟踪。dropbox 应该是有收到新照片的通知来调用进程更新,但我这里只是简单地,主动调用主线程来更新数据而已,除非我的(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath实现得有问题。

lionlee 回复于 2014年03月22日

http://www.zhihu.com/question/23019630/answer/23369396?utmcampaign=rss&utmmedium=rss&utmsource=rss&utmcontent=title ,推荐你看看,找bug的方法也很重要,毕竟是你自己的东西,good luck

seedante 回复于 2014年03月22日

10楼 @lionlee 多谢。

tinyfool 回复于 2014年03月22日

召唤 @sycx

sycx 回复于 2014年03月22日

你的assetsEnumerationBlock是运行在主线程的,你在主线程for (FaceppLocalFace *face in detectResult.faces)循环添加数据,结果自然是主线程得等你数据全部添加完,才有机会渲染。


正确做法是把你的[self.photoLibrary enumerateGroupsWithTypes:groupTypes usingBlock:listGroupBlock failureBlock:failureBlock];这句调用放入后台执行

seedante 回复于 2014年03月23日

多谢 @sycx 指出了正确的思路,我把GCD用错了,动态更新的这个问题解决了。

tinyfool 回复于 2014年03月23日

13楼 @sycx 赞,无敌召唤兽

seedante 回复于 2014年03月29日

多谢 @tinyfool 的召唤,但看不到回帖提醒,今天又遇到新的问题又把帖子翻出来。我的这段代码在扫描了大约30张图片后就因为内存压力被 kill 了,我看了下被 CGImage吃掉了150M 的空间,但是我的代码里明明都有释放所有生成的 CGImage。@sycx 有空的话帮我看下。我代码格式贴进来总是不对,我用 MarkDown 编辑器里编辑好了贴进来还是一直出错。代码部分的改写了下,主要是 之前不熟悉ARC,居然显式使用了那么多__autoreleasing,逻辑基本还是那样。

sycx 回复于 2014年03月29日

16楼 @seedante 你的self.allFaces保存了所有图片,内存当然会爆...

对于每个Face,你应该保存的是图片的URL,以及face的bounds,然后在- collectionView:cellForItemAtIndexPath:里,动态创建face image

seedante 回复于 2014年03月29日

17楼 @sycx 即是说占内存的是 self.allFaces,但这里全是 UIImage,并不是占掉内存的 CGImage 对象。下面是 Instruments 里的截图。图好像不能显示。截图

seedante 回复于 2014年03月29日

17楼 @sycx 你指出来的是对的,我应该在

  • collectionView:cellForItemAtIndexPath:里动态创建,但是还是搞不懂那些 CGImage 哪里来的
sycx 回复于 2014年03月29日

18楼 @seedante 基本上 UIImage就是CGImage的一个包装...

seedante 回复于 2014年03月29日

20楼 @sycx 那么说我还是没有把那些 CGImage释放掉吗?

sycx 回复于 2014年03月29日

21楼 @seedante

__autoreleasing UIImage *faceImage = [UIImage imageWithCGImage:faceCGImage];
[self.allFaces addObject:faceImage];

你的self.faceImage保持着对faceImage的引用,所以faceImage不会释放,你加不加__autoreleasing都一个样

你的每个faceImage都保持着对faceCGImage的引用,所以所有faceCGImage的也不会释放,这就是你在Instruments里看到的CGImage

seedante 回复于 2014年03月29日

22楼 @sycx 原来如此,我以为这个引用没有增加引用数,对内存管理这个理解的太差劲了,我按你的建议去改程序去。

seedante 回复于 2014年03月29日

22楼 @sycx while (CFGetRetainCount(fullCGImage)) { CGImageRelease(fullCGImage); }我先使用了最简单的办法,但结果却出现了僵尸对象。

seedante 回复于 2014年03月29日

22楼 @sycx 今晚不弄了,得回宿舍了。多谢解答。

seedante 回复于 2014年03月31日

15楼 @tinyfool 我是用 Mou 这个 MarkDown编辑器编辑好代码后再贴到这里来格式不对,但我贴到 stackoverflow 里显示是正确的。可有人有这种问题?

seedante 回复于 2014年03月31日
    //faceInfo的结构是@{NSURL:NSValue(wrap a CGRect)}
NSDictionary *faceInfo = [self.allFaces objectAtIndex:[indexPath row]];
ALAssetsLibraryAssetForURLResultBlock assetAccessBlock = ^(ALAsset *asset){
    CGRect headBounds = [[faceInfo allValues][0] CGRectValue];
    ALAssetRepresentation *representation = [asset defaultRepresentation];
    //这里的 faceCGImage 没有所有权,不用负责释放
    CGImageRef fullCGImage = [representation fullResolutionImage];
    //由 含create的函数生成,所有后面要负责释放所有权
    CGImageRef faceCGImage = CGImageCreateWithImageInRect(fullCGImage, headBounds);
    NSLog(@"fullCGImage Retain Count: %d", (int)CFGetRetainCount(fullCGImage));//这里引用数已经为2
    UIImage *faceUIImage = [UIImage imageWithCGImage:faceCGImage scale:rep.scale orientation:rep.orientation];//faceCGImage 对象引用书加1
    CGImageRelease(faceCGImage);
    [faceView setImage:faceUIImage];
    /*到了这里最大的 fullCGImage 引用数为2,我无权释放。而在这里我只能增大 cell 的面积来避免屏幕上出现过多 cell,但是一旦滚动视图,内存压力飙升,因为fullCGImage 实在太大了,立马崩溃。但我必须使用[representation fullResolutionImage]来,因为 faceInfo 里面的 CGRect是按fullResolutionImage 的尺寸来的。怎么在这里把 fullCGImage 和 faceCGImage 释放掉缓解内存压力呢,但是有 faceUIImage 在,前两者的内存就不会被废弃掉,虽然滚动视图时,他们都会被释放,但这时候来不及,APP 立马崩掉。有什么办法呢。我还能想到的办法是,在 viewDidLoad 或是 viewWillAppear 里检测到人脸后,在后台将人脸区域分割下来保存到数据库或是文件,然后在这里读取文件或是缓存,内存压力会小得多。 */
};
[self.photoLibrary assetForURL:[faceInfo allKeys][0] resultBlock:assetAccessBlock failureBlock:nil];

囧,发现先贴代码再插入文字就可以正常显示。请@syxc 指教。忘了说了,这是在(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath里的代码。在之前的代码里allFaces 里直接保存的人脸部分的 UIImage 对象,现在换成了 URL+人脸区域的 CGRect信息里,动态创建人脸部分的图像遇到的内存压力问题其实和之前的是一样的,挥之不去的 faceCGImage 无法释放。

seedante 回复于 2014年03月31日

22楼 @sycx 之前@ 错了。囧。请看27楼

seedante 回复于 2014年04月01日
    CGImageRef faceCGImage = CGImageCreateWithImageInRect(fullCGImage, headBounds);
    CGImageRef faceCopyCGImage = CGImageCreateCopy(faceCGImage);
    CGImageRelease(faceCGImage);
    UIImage *faceUIImage = [UIImage imageWithCGImage:faceCopyCGImage scale:rep.scale orientation:rep.orientation];
    [faceView setImage:faceUIImage];
    //NSLog(@"faceCGImage Retain Count: %d", (int)CFGetRetainCount(faceCGImage));

想了个方法,拷贝 faceCGImage 的一个副本,用作生成 faceUIImage,原来的 faceCGImage 释放,这样一来fullCGImage 也能释放掉。这样解决了及时释放内存的问题,但后期还是因为内存压力被系统 kill 了,这个我待会再搞清楚。刚开始保留了NSLog(@"faceCGImage Retain Count: %d", (int)CFGetRetainCount(faceCGImage));这句的时候,由于faceCGImage已经被废弃,结果 CFGetRetainCount 出错。总之,之前的问题是解决了,虽然有新的问题。告知新的进展,感谢之前@sycx 的指教。还有@tinyfool 的论坛,我在 stackoverflow 提的问题至今还没有被人回答,@sycx 真是公司之宝啊。

seedante 回复于 2014年04月01日

唉,拷贝的 faceCGImage 副本根本没有解决问题。我傻 X 了。

seedante 回复于 2014年04月03日
    CGImageRef (^flip)(CGImageRef sourceCGImage) = ^CGImageRef(CGImageRef sourceCGImage){
    CGSize size = CGSizeMake(CGImageGetWidth(sourceCGImage), CGImageGetHeight(sourceCGImage));
    UIGraphicsBeginImageContextWithOptions(size, NO, 0);
    CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, size.width, size.height), sourceCGImage);
    CGImageRef result = [UIGraphicsGetImageFromCurrentImageContext() CGImage];
    UIGraphicsEndImageContext();
    return result;
};
ALAssetsLibraryAssetForURLResultBlock assetAccessBlock = ^(ALAsset *asset){
    @autoreleasepool {
        CGRect headBounds = [[faceInfo allValues][0] CGRectValue];
        ALAssetRepresentation *rep = [asset defaultRepresentation];
        CGImageRef fullCGImage = [rep fullResolutionImage];
        CGImageRef faceCGImage = CGImageCreateWithImageInRect(fullCGImage, headBounds);
        UIGraphicsBeginImageContextWithOptions(headBounds.size, NO, 0);
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextDrawImage(context, CGRectMake(0, 0, headBounds.size.width, headBounds.size.height), flip(faceCGImage));
        UIImage *faceImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        CGImageRelease(faceCGImage);
        [faceView setImage:faceImage];
    }
};
此事终于得到了解决,代码如上。解决的关键在不再直接基于 faceCGImage 生成 faceUIImage,因为这会造成 faceCGImage 无法被释放,而是将 faceCGImage 绘制在一个 context 中,从这个 context 里使用`UIGraphicsGetImageFromCurrentImageContext()`得到 faceUIImage,终于甩掉了 fullCGImage 这个大头。内存占用问题终于解决了,花了好长时间。另外这里有一个小问题,在 context 中绘制CGImageRef 后提取的 UIImage 是上下翻转的,那么再翻转一次就得到了正常的图像。

本帖有31个回复,因为您没有注册或者登录本站,所以,只能看到本帖的10条回复。如果想看到全部回复,请注册或者登录本站。

登录 或者 注册
[顶 楼]
|
|
[底 楼]
|
|
[首 页]