所以我问了几个关于UICollectionView的问题。了解它是如何工作的,我试图实现延迟加载以将15个图像加载到视图控制器上。我发现了很多例子1,2,3 ......第一个和第三个例子只涉及一个操作,第二个例子我认为根本不使用操作,只有线程。我的问题是可以使用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,我需要一个委托方法......我还没有其他的东西知道怎么做,但有很多例子可以解决这个问题。我最大的问题是如何将可见单元格传递到此类并运行多个操作?当有新的可见细胞进入时,我会检查哪个要取消,哪个要保留。有什么建议吗?提前谢谢!
答案 0 :(得分:1)
您可以使用队列和操作本身来管理多个操作。如果我正确地阅读您的问题,您有一个操作(从JSON获取图像URL列表),您想要生成子操作。您可以通过让父操作将子操作添加到队列中,或者使用依赖操作(child将父项作为依赖项)来执行此操作。
对于您尝试做的事情,您不需要继承NSOperation,NSBlockOperation应该满足您的需求。由于KVO的依赖性(它很容易出错),因此NSOperation的子类化比它看起来更棘手。
但是对于你问题的具体细节:
我的问题是可以使用NSOperation类并使用/重用操作吗?我读到你不能重新开始运营,但我认为你能够再次初始化它们
如果您再次初始化它们,那么它们就是新对象(或者至少应该是新对象)。 NSOperations无法重新运行,因为它们具有内部状态 - 我上面提到的棘手的KVO位。一旦他们进入"完成",该实例就无法恢复到干净状态。
操作应该是相当轻量级的对象,重用它们应该没有任何重要价值,并且存在很多潜在的麻烦。创建新的操作应该是最佳选择。
Apple示例代码" LazyTableImages"可能会给你一些提示,说明如何完成你想要做的事情。
答案 1 :(得分:1)
基于NSOperation
的图像延迟加载的组成元素可能包括:
创建一个专用的NSOperationQueue
,用于下载操作。通常,这配置为maxConcurrentOperationCount
为4或5,以便您享受并发性,但不会超过最大并发网络操作数。
如果您不使用此maxConcurrentOperationCount
,网络连接速度较慢(例如移动电话),则可能会出现网络请求超时的风险。
拥有一个支持集合视图或表视图的模型对象(例如数组)。这通常只有图像的一些标识符(例如URL)而不是图像本身。
实施缓存机制以存储下载的图像,以防止需要重新下载已下载的图像。某些实现仅执行基于内存的缓存(通过NSCache
)。其他(例如SDWebImage)将执行两层缓存,包括内存(NSCache
,以获得最佳性能)和辅助持久存储缓存(以便当内存压力迫使您清除{{1}时},您仍然在设备上保存了一个节目,因此您不必从网络中重新检索它。其他人(例如AFNetworking)依赖NSCache
将来自服务器的响应缓存到持久存储中。
编写NSURLCache
子类以下载单个图像。您想要进行此可取消操作。这意味着两种不同的设计考虑因素
首先,关于操作本身,您可能希望进行并发操作(请参阅并发编程指南中的Configuring Operations for Concurrent Execution部分)。
其次,关于网络请求,您需要一个可取消的网络请求。如果使用NSOperation
,这意味着使用基于委托的再现(不是任何便捷方法)。如果使用NSURLConnection
,诀窍是你必须在一个持续存在的运行循环中安排它。有很多技巧可以实现这一点,但最简单的方法是在主运行循环中安排它(使用NSURLConnection
)(尽管有更优雅的方法),即使你将从一个操作中运行它scheduleInRunLoop:forMode:
。就个人而言,我为此目的启动了一个新的专用线程(如AFNetworking),但主运行循环更容易,适用于这种过程。
如果使用NSOperationQueue
,这个过程可能会更容易一些,因为您可以使用NSURLSession
的完成块再现,而不是进入基于代理的实现,如果你不在&#39我想。但这只是iOS 7+(如果您需要做任何像处理身份验证质询请求那样的事情,那么您最终会采用基于代理的方法)。
结合前两个点,自定义dataTaskWithRequest
子类将检测操作何时被取消,然后取消网络请求并完成操作。
顺便说一下,操作实例永远不会被重用。您为要下载的每个图像创建一个新的操作实例。
顺便提一下,如果您下载的图片很大(例如尺寸大于图片视图所需的像素数),您可能需要在使用之前调整图像大小。当下载JPG或PNG图像时,它们会被压缩,但是当你在图像视图中使用它们时它们是未压缩的,通常每个像素需要4个字节(例如,1000x1000图像需要4mb,即使JPG比这个小得多)
有很多图像调整大小的算法,但这里有一个:https://stackoverflow.com/a/10859625/1271826
您需要一个NSOperation
然后将上述部分拉到一起,即:
检查图像是否已在缓存中,如果是,请使用它。
如果没有,您将启动网络请求以检索图像。您可能想要查看此单元格(可能是表视图中的重用单元格)是否已经有正在进行的图像操作,如果是,则只需取消它。
无论如何,您可以实例化一个新的cellForItemAtIndexPath
子类来下载图像,并让完成块更新缓存,然后更新单元的图像视图。
顺便说一句,当您异步更新单元格的图像视图时,确保单元格仍然可见并且单元格未被重复使用临时。您可以执行此操作NSOperation
(不应与您在此处撰写的类似名称[collectionView cellForItemAtIndexPath:indexPath]
方法混淆)。
这些是该过程的组成部分,我建议您一次解决一个问题。编写一个优雅的延迟加载实现有很多参与。
最简单的解决方案是考虑使用现有的UICollectionViewDataSource
类别(例如SDWebImage提供的),为您完成所有这些操作。即使您不使用该库,您也可以通过查看源代码来学习相当多的东西。
答案 2 :(得分:1)
查看您的proposed solution,看起来您想要推迟使操作取消的问题。此外,看起来您希望推迟使用缓存(即使它不比您的NSMutableDictionary
属性复杂)。
因此,将此放在一边,您修改后的代码示例有两个"大图片"的问题:
您可以大大简化图像检索过程。 startOperationForVisibleCells
和两个滚动委托的使用不必要地复杂化。有一个更简单的模式,你可以退休这三种方法(并实现更好的用户体验)。
您的cellForItemForIndexPath
有问题,您正在添加子视图。问题是细胞被重复使用,因此每次重复使用细胞时,您都会添加更多的冗余子视图。
你真的应该在我的下面的例子中继承UICollectionViewCell
(CustomCell
),放置单元格的配置,包括添加子视图。它简化了您的cellForItemAtIndexPath
,并消除了添加额外子视图的问题。
除了这两个主要问题外,还有一些小问题:
您忽略了为您的操作队列设置maxConcurrentOperationCount
。您确实希望将其设置为4
或5
以避免操作超时错误。
您正在使用imageGalleryData
键入NSIndexPath
。问题是,如果您删除了一行,则所有后续索引都将出错。我怀疑这不是一个问题(你可能不会预期删除项目),但如果你用其他东西键入它,比如URL,它就像容易一样,但是它更具有前瞻性。
我建议您将操作队列从operation
重命名为queue
。同样,我将UICollectionView
从images
(可能被错误地推断为图像数组)重命名为collectionView
。这是风格的,如果你不想要的话,你不必这样做,但这是我在下面使用的惯例。
您可能希望保存NSData
,而不是将NSMutableDictionary
保存在名为imageGalleryData
的{{1}}中。这样,您就可以在向后滚动回以前下载的单元格时,不必从UIImage
重新转换为NSData
(这会使滚动过程更加顺畅)。
所以,把它们拉到一起,就会得到类似的东西:
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
}
我确信这是一种非常糟糕的做法,但就目前而言,它有效。图像逐个加载,当用户滚动时它们停止加载。如果它如此糟糕并且人们投票,我会删除这个答案,以防有些人从我那里学习错误的方法。任何评论都赞赏这种做法。