使用KVO保留发送到解除分配的实例(EXC_BREAKPOINT)

时间:2014-07-05 16:07:06

标签: ios objective-c key-value-observing nsoperation

当我在iOs应用程序中更改视图控制器时,我发生了这个非常奇怪的错误。 首先是一些背景信息。

我正在后台NSOperation从网络上检索图片。此操作从在集合视图单元格中实例化的另一个(后台)操作开始。这种方法的工作方式是单元格创建一个对象,然后将自己设置为该对象的观察者,然后创建一个NSOperation,并将该对象作为参数。第一级操作将启动第二个操作,该操作将从Web获取图像,另一个NSOperation将尝试从文件中获取数据(如果可能),并通过委派将其报告给第一个操作。第一级操作将改变观察对象的属性,从而触发KVO。 collection / tableView单元格将从- observeValueChange方法更新。

问题在于: 有时单元格会消失(重用或取消分配),当后台线程尝试在观察对象上设置值时,它会触发EXC_BREAKPOINT异常([collectionViewCell message retain sent to deallocated instance])

为了防止这种情况,我尝试在单元格上实现-prepareForReuse和-dealloc。但错误不断发生。 流程看起来像这样:

- 用户VC

加载collectionViewWithCells

-cell创建对象和NSOperation 1

  • NSoperation 1创建了NSOperation2(这是从web获取的两种类型或从文件中获取)

  • NSOpeartion 2从互联网或本地文件获取图像

  • NSoperation 2将数据发送到NSOperation1

  • 用户已离开此屏幕

  • NSOperation 1尝试设置观察对象的数据

- CRASH

这是单元格内的代码:

    @interface CustomCollectionViewCell ()

@property (strong, nonatomic) NSOperationQueue *imagesOperationQueue;
@property (strong, nonatomic) ImageObject *imgObj;

@end

@implementation CustomCollectionViewCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}
- (void)prepareForReuse{
    [self clearDelegatesAndObservers];
    [super prepareForReuse];
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/


- (void) getImage {

    self.imgObj = [ImageObject newRefrenceWithId:obj_ref];
    [self.imgObj addObserver:self forKeyPath:@"data" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    TaskImageReqCache *imgReq = [[TaskImageReqCache alloc] initWithUrl:imgUrl andImageObject:self.imgObj];
    [self.imagesOperationQueue addOperation:imgReq];
}

#pragma mark - KVO

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == self.imgObj) {
        UIImage *img = [UIImage imageWithData:self.imgObj.data];
        self.thumbnailImage.image = img;
    }
}

- (void)dealloc
{
    [self clearDelegatesAndObservers];
}

- (void)clearDelegatesAndObservers
{
    [self.imagesOperationQueue cancelAllOperations];
    self.thumbnailImage.image = nil;
    [self.imgObj removeObserver:self forKeyPath:@"data"];
    [self.pageListAdapter removeDelegateAtIndex:self.myIndexInCollection];
    self.imgObj = nil;

}

在第一级NSOperation中,这是异常断点显示发生崩溃的地方:

   - (void)didLoadDataFromFile:(NSData *)data
{
    if (self.isCancelled) {
        [self.opQueue cancelAllOperations];
        [self completeOperation];
        return;
    }

    if (!fileDownloadedFromWeb)  {
        self.observedObject.data = data; // CRASH 
    }
    dataFromDisk = data;
    fileReadDone = YES;
    if (debugLog) {
        NSLog(@"image loaded from local cache (%@)",self.sUrl);
    }
}

有关如何防止此次崩溃的任何建议吗? 谢谢。

编辑添加: 我想要实现的是:当显示一个tableView单元格时,会激活一个nsoperation来从网络中获取图像。如果用户快速滚动并且操作尚未完成,我需要结束它并取消分配任何数据,并且当重复使用该单元时,启动一个新操作以从互联网获取适当的图像...

3 个答案:

答案 0 :(得分:0)

这需要很长时间才能成为评论,所以我会给它一个答案。

崩溃的原因与UICollectionViewCell被回收和解除分配的事实有关。 ARC已将[cvcell retain]置于错误的位置。所以,有几个选择:

  1. 解决此问题的一种方法是,不要从NSOperation创建UICollectionViewCell
  2. 强制用户留在UICollectionViewController / UICollectionView,以便留在内存中。
  3. 保留指向UICollectionViewController / UICollectionView的属性/指针,以便即使用户离开它也会保留在内存中。 (请确保将其保留为strongretain)。
  4. 注意:所有这些解决方案都做同样的事情,强制ARC将保留调用放在其他地方或完全删除它。

答案 1 :(得分:0)

在没有您控制的情况下,单元格会被频繁重用和重新分配,因此您应该避免将未决请求或操作分配给它们。

而是处理集合视图数据源(视图控制器)中的操作,并跟踪每个单元格但不是字典中indexPath的操作。


更好地保持这是一种良好的体验,并使用可信和经过测试的内容,例如SDwebImage

答案 2 :(得分:0)

根据以下评论,我们知道:

- (void)didLoadDataFromFile:(NSData *)data

在另一个线程上调用dealloc,因此存在竞争条件。您需要在与其解除分配的线程相同的线程上访问self.observedObject。我假设"观察对象"是弱参考?

dispatch_sync(dispatch_get_main_queue(), ^{
     if (!fileDownloadedFromWeb)  {
       // Get a strong reference. This will retain observedObject - we must do this
       // on the same thread as observedObject:dealloc is called, to prevent retaining
       // an object during (or after) dealloc.
       ObservedObject *strongRef = self.observedObject;
       // This will do nothing if strongRef is nil.
       strongRef.data = data; 
     }
});

更结构化的方法是让单元格从单例缓存中获取其所有图像(看起来好像目前没有缓存)。该单元显然需要将自己注册为缓存中特定URL的观察者,并且缓存将在URL下载时通知单元。缓存应该在主线程上发布该通知。

缓存本身将管理所有下载,并且不存在背景释放问题,因为它将是单例。

如果您不想缓存,那很好。使用相同的体系结构,但是将缓存称为图像提取器。如果您愿意,可以随后添加缓存。

编辑 - 如果你的对象可以被重用,而不是像UITableViewCells那样被解除分配,那么单元格需要小心忽略与先前获取相关的图像的通知。这些模型中的任何一个都可以使用

a)单元格保留对NSOperation的引用,直到NSOperation将其调回,或者直到调用prepareForReuse为止。来自无法识别的NSOperation的任何回调必须是先前的提取(我们试图取消),并且应该被忽略。我不是真的推荐这个模型,让细胞了解操作,反之亦然。

b)NSOperation在完成时(在主线程上)发送通知,并在用户信息中指定所请求的url /路径。 UITableViewCell会记住它尝试获取的URL /路径,并忽略与其他图像相关的通知。它在dealloc / prepareForReuse中未注意到该路径。