NSOperation类运行多个操作

时间:2014-04-24 17:49:07

标签: ios objective-c nsoperation

所以我问了几个关于UICollectionView的问题。了解它是如何工作的,我试图实现延迟加载以将15个图像加载到视图控制器上。我发现了很多例子123 ......第一个和第三个例子只涉及一个操作,第二个例子我认为根本不使用操作,只有线程。我的问题是可以使用NSOperation类并使用/重用操作吗?我读到你不能重新开始运营,但我认为你可以再次初始化它们。这是我的代码:

查看控制器:

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20);
[layout setItemSize:CGSizeMake(75, 75)];
self.images = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout];
self.images.delegate = self;
self.images.dataSource = self;
[self.images registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellIdentifier"];
[self.view addSubview:self.images];
self.operation = [[NSOperationQueue alloc]init];
[self.operation addOperationWithBlock:^{
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"getgallery.php?user=%@", userId] relativeToURL:[NSURL URLWithString:@"http://www.mywebsite.com/"]];
    NSData *data = [NSData dataWithContentsOfURL:url];
//datasource for all images
    self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
    [[NSOperationQueue mainQueue]addOperationWithBlock:^{
        [self.images reloadData];
//reload collection view to place placeholders
    }];
}];

- (void)viewDidAppear:(BOOL)animated{
//get the visible cells right away
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
self.processedImages = [[NSMutableDictionary alloc]initWithCapacity:visibleCellPaths.count];
}
#pragma mark - collection view
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
int i;
if (self.imagesGalleryPaths.count == 0)
    i = 25;
else
    i = self.imagesGalleryPaths.count;
return i;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
    if ([visibleCellPaths containsObject:indexPath]) {
        [self setUpDownloads:visibleCellPaths];
    }
    image.image = [UIImage imageNamed:@"default.png"];
    image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
    [cell.contentView addSubview:image];
}
return cell;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}

- (void)setUpDownloads:(NSArray *)visiblePaths{
//I want to pass the visiblePaths to the NSOperation class if visible cells changed
GalleryOps *gallery = [[GalleryOps alloc]init];
//I will use a dictionary to keep track of which indexPaths are being downloaded...
//...so there are no duplicate downloads
}

GalleryOps.m

@implementation GalleryOps

- (void)main{
    //would I initialize all the operations here and perform them?
}

显示空的GalleryOps类几乎没有意义,因为我不知道如何使用多个操作初始化它。我知道我必须覆盖main方法,一旦我从URL获取图像数据,我就需要更新UI,我需要一个委托方法......我还没有其他的东西知道怎么做,但有很多例子可以解决这个问题。我最大的问题是如何将可见单元格传递到此类并运行多个操作?当有新的可见细胞进入时,我会检查哪个要取消,哪个要保留。有什么建议吗?提前谢谢!

4 个答案:

答案 0 :(得分:1)

您可以使用队列和操作本身来管理多个操作。如果我正确地阅读您的问题,您有一个操作(从JSON获取图像URL列表),您想要生成子操作。您可以通过让父操作将子操作添加到队列中,或者使用依赖操作(child将父项作为依赖项)来执行此操作。

对于您尝试做的事情,您不需要继承NSOperation,NSBlockOperation应该满足您的需求。由于KVO的依赖性(它很容易出错),因此NSOperation的子类化比它看起来更棘手。

但是对于你问题的具体细节:

  

我的问题是可以使用NSOperation类并使用/重用操作吗?我读到你不能重新开始运营,但我认为你能够再次初始化它们

如果您再次初始化它们,那么它们就是新对象(或者至少应该是新对象)。 NSOperations无法重新运行,因为它们具有内部状态 - 我上面提到的棘手的KVO位。一旦他们进入"完成",该实例就无法恢复到干净状态。

操作应该是相当轻量级的对象,重用它们应该没有任何重要价值,并且存在很多潜在的麻烦。创建新的操作应该是最佳选择。

Apple示例代码" LazyTableImages"可能会给你一些提示,说明如何完成你想要做的事情。

答案 1 :(得分:1)

基于NSOperation的图像延迟加载的组成元素可能包括:

  1. 创建一个专用的NSOperationQueue,用于下载操作。通常,这配置为maxConcurrentOperationCount为4或5,以便您享受并发性,但不会超过最大并发网络操作数。

    如果您不使用此maxConcurrentOperationCount,网络连接速度较慢(例如移动电话),则可能会出现网络请求超时的风险。

  2. 拥有一个支持集合视图或表视图的模型对象(例如数组)。这通常只有图像的一些标识符(例如URL)而不是图像本身。

  3. 实施缓存机制以存储下载的图像,以防止需要重新下载已下载的图像。某些实现仅执行基于内存的缓存(通过NSCache)。其他(例如SDWebImage)将执行两层缓存,包括内存(NSCache,以获得最佳性能)和辅助持久存储缓存(以便当内存压力迫使您清除{{1}时},您仍然在设备上保存了一个节目,因此您不必从网络中重新检索它。其他人(例如AFNetworking)依赖NSCache将来自服务器的响应缓存到持久存储中。

  4. 编写NSURLCache子类以下载单个图像。您想要进行此可取消操作。这意味着两种不同的设计考虑因素

    • 首先,关于操作本身,您可能希望进行并发操作(请参阅并发编程指南中的Configuring Operations for Concurrent Execution部分)。

    • 其次,关于网络请求,您需要一个可取消的网络请求。如果使用NSOperation,这意味着使用基于委托的再现(不是任何便捷方法)。如果使用NSURLConnection,诀窍是你必须在一个持续存在的运行循环中安排它。有很多技巧可以实现这一点,但最简单的方法是在主运行循环中安排它(使用NSURLConnection)(尽管有更优雅的方法),即使你将从一个操作中运行它scheduleInRunLoop:forMode:。就个人而言,我为此目的启动了一个新的专用线程(如AFNetworking),但主运行循环更容易,适用于这种过程。

      如果使用NSOperationQueue,这个过程可能会更容易一些,因为您可以使用NSURLSession的完成块再现,而不是进入基于代理的实现,如果你不在&#39我想。但这只是iOS 7+(如果您需要做任何像处理身份验证质询请求那样的事情,那么您最终会采用基于代理的方法)。

    • 结合前两个点,自定义dataTaskWithRequest子类将检测操作何时被取消,然后取消网络请求并完成操作。

    顺便说一下,操作实例永远不会被重用。您为要下载的每个图像创建一个新的操作实例。

  5. 顺便提一下,如果您下载的图片很大(例如尺寸大于图片视图所需的像素数),您可能需要在使用之前调整图像大小。当下载JPG或PNG图像时,它们会被压缩,但是当你在图像视图中使用它们时它们是未压缩的,通常每个像素需要4个字节(例如,1000x1000图像需要4mb,即使JPG比这个小得多)

    有很多图像调整大小的算法,但这里有一个:https://stackoverflow.com/a/10859625/1271826

  6. 您需要一个NSOperation然后将上述部分拉到一起,即:

    • 检查图像是否已在缓存中,如果是,请使用它。

    • 如果没有,您将启动网络请求以检索图像。您可能想要查看此单元格(可能是表视图中的重用单元格)是否已经有正在进行的图像操作,如果是,则只需取消它。

      无论如何,您可以实例化一个新的cellForItemAtIndexPath子类来下载图像,并让完成块更新缓存,然后更新单元的图像视图。

    • 顺便说一句,当您异步更新单元格的图像视图时,确保单元格仍然可见并且单元格未被重复使用临时。您可以执行此操作NSOperation(不应与您在此处撰写的类似名称[collectionView cellForItemAtIndexPath:indexPath]方法混淆)。

  7. 这些是该过程的组成部分,我建议您一次解决一个问题。编写一个优雅的延迟加载实现有很多参与。

    最简单的解决方案是考虑使用现有的UICollectionViewDataSource类别(例如SDWebImage提供的),为您完成所有这些操作。即使您不使用该库,您也可以通过查看源代码来学习相当多的东西。

答案 2 :(得分:1)

查看您的proposed solution,看起来您想要推迟使操作取消的问题。此外,看起来您希望推迟使用缓存(即使它不比您的NSMutableDictionary属性复杂)。

因此,将此放在一边,您修改后的代码示例有两个"大图片"的问题:

  1. 您可以大大简化图像检索过程。 startOperationForVisibleCells和两个滚动委托的使用不必要地复杂化。有一个更简单的模式,你可以退休这三种方法(并实现更好的用户体验)。

  2. 您的cellForItemForIndexPath有问题,您正在添加子视图。问题是细胞被重复使用,因此每次重复使用细胞时,您都会添加更多的冗余子视图。

    你真的应该在我的下面的例子中继承UICollectionViewCellCustomCell),放置单元格的配置,包括添加子视图。它简化了您的cellForItemAtIndexPath,并消除了添加额外子视图的问题。

  3. 除了这两个主要问题外,还有一些小问题:

    1. 您忽略了为您的操作队列设置maxConcurrentOperationCount。您确实希望将其设置为45以避免操作超时错误。

    2. 您正在使用imageGalleryData键入NSIndexPath。问题是,如果您删除了一行,则所有后续索引都将出错。我怀疑这不是一个问题(你可能不会预期删除项目),但如果你用其他东西键入它,比如URL,它就像容易一样,但是它更具有前瞻性。

    3. 我建议您将操作队列从operation重命名为queue。同样,我将UICollectionViewimages(可能被错误地推断为图像数组)重命名为collectionView。这是风格的,如果你不想要的话,你不必这样做,但这是我在下面使用的惯例。

    4. 您可能希望保存NSData,而不是将NSMutableDictionary保存在名为imageGalleryData的{​​{1}}中。这样,您就可以在向后滚动回以前下载的单元格时,不必从UIImage重新转换为NSData(这会使滚动过程更加顺畅)。

    5. 所以,把它们拉到一起,就会得到类似的东西:

      UIImage

      现在,很明显我无法访问您的服务器,所以我无法检查这一点(特别是,我不知道您的JSON是返回完整的网址还是仅返回文件名,或者是否有一些嵌套的字典数组)。但是我不想让你迷失在我的代码细节中,而是看看基本模式:消除你通过可见单元格的循环并响应滚动事件,让static NSString * const kCellIdentifier = @"CustomCellIdentifier"; - (void)viewDidLoad { [super viewDidLoad]; UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init]; layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20); [layout setItemSize:CGSizeMake(75, 75)]; // renamed `images` collection view to `collectionView` to follow common conventions self.collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout]; self.collectionView.delegate = self; self.collectionView.dataSource = self; // you didn't show where you instantiated this in your examples, but I'll do it here self.imageGalleryData = [NSMutableDictionary dictionary]; // register a custom class, not `UICollectionViewCell` [self.collectionView registerClass:[CustomCell class] forCellWithReuseIdentifier:kCellIdentifier]; [self.view addSubview:self.collectionView]; // (a) change queue variable name; // (b) set maxConcurrentOperationCount to prevent timeouts self.queue = [[NSOperationQueue alloc]init]; self.queue.maxConcurrentOperationCount = 5; [self.queue addOperationWithBlock:^{ NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"getgallery.php?user=%@", userId] relativeToURL:[NSURL URLWithString:@"http://www.mywebsite.com/"]]; NSData *data = [NSData dataWithContentsOfURL:url]; //datasource for all images self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; [[NSOperationQueue mainQueue]addOperationWithBlock:^{ [self.collectionView reloadData]; }]; }]; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self.imagesGalleryPaths count]; // just use whatever is the right value here, don't make this unnecessarily smaller } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath]; NSString *key = self.imagesGalleryPaths[indexPath.row]; // I don't know whether this was simply array, or some nested structure, so tweak this accordingly UIImage *image = self.imageGalleryData[key]; if (image) { cell.imageView.image = image; // if we have image already, just use it } else { cell.imageView.image = [UIImage imageNamed:@"profile_default.png"]; // otherwise set the placeholder ... [self.queue addOperationWithBlock:^{ // ... and initiate the asynchronous retrieval NSURL *url = [NSURL URLWithString:...]; // build your URL from the `key` as appropriate NSData *responseData = [NSData dataWithContentsOfURL:url]; if (responseData != nil) { UIImage *downloadedImage = [UIImage imageWithData:responseData]; if (downloadedImage) { [[NSOperationQueue mainQueue]addOperationWithBlock:^{ self.imageGalleryData[key] = downloadedImage; CustomCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath]; if (updateCell) { updateCell.imageView.image = downloadedImage; } }]; } } }]; } return cell; } // don't forget to purge your gallery data if you run low in memory - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; [self.imageGalleryData removeAllObjects]; } 做所有为你工作。

      现在,我介绍的一件事是cellForItemAtIndexPath的概念,它是一个CustomCell子类,可能看起来像:

      UICollectionViewCell

      然后移动单元格配置并在此处将子视图添加到// CustomCell.h #import <UIKit/UIKit.h> @interface CustomCell : UICollectionViewCell @property (nonatomic, weak) UIImageView *imageView; @end

      @implementation

      顺便说一句,我仍然认为如果你想要做到这一点,你应该参考my other answer。如果您不想迷失在正确实现的杂草中,那么只需使用支持异步图像检索,缓存,优先处理可见单元的网络请求等的第三方// CustomCell.m #import "CustomCell.h" @implementation CustomCell - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.layer.borderWidth = 1; self.layer.borderColor = [[UIColor whiteColor]CGColor]; UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; [self addSubview:imageView]; _imageView = imageView; } return self; } @end 类别,例如SDWebImage

答案 3 :(得分:0)

所以我想通了,我想我会把它称为“肮脏的方式”,做我想做的事情。我认为我的收藏视图不是用户会一次又一次地回来的东西。所以没有理由缓存所有图像。我知道@Rob会因为这样做而对我大喊大叫,但在我学习代表并对他们感到满意之前,我觉得这很舒服。

viewDidAppear仍然是相同的,我立刻得到了可见的单元格。我最初将25个单元格放入numberOfItems中的原因只是为了立即获得可见单元格。所以现在我的cellForItemAtIndexPath是这样的:

UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
    image.image = [UIImage imageNamed:@"profile_default.png"];
    image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
    [cell.contentView addSubview:image];
}
return cell;

在viewDidLoad中我添加了这个:

if (self.imagesGalleryPaths.count != 0) {
    [[NSOperationQueue mainQueue]addOperationWithBlock:^{
        [self.images reloadData];
        [self startOperationForVisibleCells];
    }];
}

这是我的startOperationForVisibleCells:

[self.operation addOperationWithBlock:^{
    int i=0;
    while (i < visibleCellPaths.count) {
        NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
        if (![self.imageGalleryData.allKeys containsObject:indexPath]) {
            NSURL *url = [@"myurl"];
            NSData *responseData = [NSData dataWithContentsOfURL:url];
            if (responseData != nil) {
                [self.imageGalleryData setObject:responseData forKey:indexPath];
                [[NSOperationQueue mainQueue]addOperationWithBlock:^{
                    UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
                    UIImageView *image = [UIImageView new];
                    image.image = [UIImage imageWithData:responseData];
                    image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
                    [cell.contentView addSubview:image];
                }];
            }
        }
        i++;
    }
}];

这就是我如何逐个更新单元格。当用户滚动时:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
for (int i=0; i<visibleCellPaths.count; i++) {
    NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
    if ([self.imageGalleryData.allKeys containsObject:indexPath]) {
        UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
        UIImageView *image = [UIImageView new];
        image.image = [UIImage imageWithData:[self.imageGalleryData objectForKey:indexPath]];
        image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
        [cell.contentView addSubview:image];
    }else{
        [self startOperationForVisibleCells];
    }
}
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
//same functions as previous
}

我确信这是一种非常糟糕的做法,但就目前而言,它有效。图像逐个加载,当用户滚动时它们停止加载。如果它如此糟糕并且人们投票,我会删除这个答案,以防有些人从我那里学习错误的方法。任何评论都赞赏这种做法。

相关问题