为什么我的线程似乎失败后几个线程已经开始在iOS上? [英] Why does my threading seem to fail after a few threads have started on iOS?
问题描述
我有这个代码在 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
委托调用:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^ {
AVPlayerItem * playerItem = [AVPlayerItem playerItemWithURL:[webUrls objectAtIndex:indexPath .row]];
CMTime timeduration = playerItem.duration;
float seconds = CMTimeGetSeconds(timeduration);
NSString * duration = [NSString stringWithFormat:@%f,seconds] b
$ b dispatch_async(dispatch_get_main_queue(),^ {
UITableViewCell * updatecell = [tblView cellForRowAtIndexPath:indexPath];
updatecell.detailTextLabel.text = duration;
[updatecell setNeedsLayout];
});
});
每个单元格在后台缓慢加载秒
导入单元格中的 updatecell.detailTextLabel.text
。问题是我滚动后,约3或4个单元格加载后,其余只是显示0在detailTextLabel快速,不加载。
任何想法为什么这是?
解决方案有几个想法:
-
许多服务器对从给定客户端接受的并发请求数量有约束。我建议您使用 NSOperationQueue
将您对服务器的并发请求数限制为4或5,而不是使用调度队列。
-
您可能会使问题比需要的更糟糕,因为如果您向下滚动表格视图,然后备份,当您重新显示前几个单元格,下载 AVPlayerItem
并尝试对您的服务器发出其他并发请求。您真的应该保存您之前下载的结果,以避免对相同数据的冗余重新请求。
-
您目前未检查查看刚下载的单元格是否在尝试更新UI之前仍然可见。你真的应该检查一下。
所以,我可能会建议如下:
-
在您的视图控制器的 viewDidLoad
中,创建 NSOperationQueue
我们将用于下载。还指定您的服务器将允许多少并发操作:
downloadQueue = [[NSOperationQueue alloc] init];
downloadQueue.maxConcurrentOperationCount = 4; //用适当的值替换你的服务器
-
webUrls
,它是一个 NSURL
对象的数组。在下面的第4点,我们将讨论退出该数组,并创建一个新的行对象数组。但在我们可以这样做之前,我们应该创建这个新的 RowData
对象。
webURL
,还有其他一些事情,比如 durationText
,甚至还有 AVPlayerItem
本身。 (通过保留这些其他对象属性,当单元格回滚到视图中时,我们不需要重新下载数据。)因此,这个新类的公共接口可能如下所示:
//
// RowData.h
//
#import< Foundation / Foundation.h>
@class AVPlayerItem;
@interface RowData:NSObject
@property(nonatomic,strong)NSURL * webURL;
@property(nonatomic,strong)NSString * durationText;
@property(nonatomic,strong)AVPlayerItem * playerItem;
@property(nonatomic,getter = isDownloaded,readonly)BOOL下载;
@property(nonatomic,getter = isDownloading,readonly)BOOL下载;
- (void)downloadInQueue:(NSOperationQueue *)队列完成:(void(^)(BOOL success))block;
- (void)cancelDownload;
@end
顺便说一句,类名, RowData
。这有点太含糊。但我不知道你的模型数据的性质,以建议一个更好的名字。
-
您的新 RowData
类可以有一个实例方法,调用 downloadInQueue
,执行下载,适当地设置 durationText
等。通过移动下载逻辑,我们从下载涉及的某些细节成功地隔离了 cellForRowAtIndexPath
。同样重要的是, downloadInQueue
方法不会更新用户界面本身,而是完成
由 cellForRowAtIndexPath
提供(在下面的第5点演示),因此这个 downloadInQueue
方法不必担心UI注意事项。无论如何, downloadInQueue
的实现可能如下所示:
//
// RowData.m
//
#importRowData.h
#import< AVFoundation / AVFoundation.h>
@interface RowData()
@property(非原子,getter = isDownloaded)BOOL下载;
@property(nonatomic,getter = isDownloading)BOOL下载;
@property(nonatomic,weak)NSOperation * operation;
@end
@implementation RowData
- (void)downloadInQueue:(NSOperationQueue *)队列完成: ))completion
{
if(!self.isDownloading)
{
self.downloading = YES;
NSOperation * currentOperation = [NSBlockOperation blockOperationWithBlock:^ {
BOOL success = NO;
self.playerItem = [AVPlayerItem playerItemWithURL:self.webURL];
if(self.playerItem)
{
success = YES;
CMTime timeduration = self.playerItem.duration;
float seconds = CMTimeGetSeconds(timeduration);
self.durationText = [NSString stringWithFormat:@%f,seconds];
}
self.downloading = NO;
self.downloaded = YES;
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
completion(success);
}];
}];
[queue addOperation:currentOperation];
self.operation = currentOperation;
}
}
- (void)cancelDownload
{
if([self isDownloading]&& self.operation)
{
self.downloading = NO;
[self.operation cancel];
}
}
@end
-
在您的主视图控制器中,而不是创建 webUrls
的旧数组,创建一个新的数组 RowData
调用的对象,例如 objects
。当然,为每个 RowData
对象设置 webURL
属性。 (同样,我对 objects
这个模糊的名称不感冒,但是我对你的应用程序不够了解,以提出更具体的建议。 。
-
最后,修改你的 cellForRowAtIndexPath
使用此新的 RowData
对象和 downloadInQueue
方法。此外,请注意,完成
阻止检查以确保单元格仍然可见:
RowData * rowData = self.objects [indexPath.row];
if([rowData isDownloaded])
{
cell.detailTextLabel.text = rowData.durationText;
}
else
{
cell.detailTextLabel.text = @... //你真的应该初始化这个,所以我们下载期间显示的东西或删除以前的任何
[rowData downloadInQueue:self.downloadQueue完成:^(BOOL成功){
//注意,如果你想根据
//下载是否成功改变你的行为,只需使用`success'变量
UITableViewCell * updateCell = [tblView cellForRowAtIndexPath:indexPath];
//顺便说一句,确保单元格仍在屏幕上
if(updateCell)
{
updateCell.detailTextLabel.text = rowData .durationText;
[updateCell setNeedsLayout];
}
}];
}
-
如果使用iOS 6,一个单元格在屏幕上滚动,可以使用 UITableViewDelegate
协议的 didEndDisplayingCell
方法。
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
RowData * rowData = self.objects [indexPath.row];
if([rowData isDownloading])
[rowData cancelDownload];
}
如果支持早期版本的iOS,您必须使用 UIScrollViewDelegate
协议方法,例如 scrollViewDidScroll
,确定哪些单元格已经从屏幕上滚动
包含在 indexPathsForVisibleRows
顺便说一下,在我的示例 RowData
上面,我保存 AVPlayerItem
。你应该这样做,如果你需要 AVPlayerItem
以后。我们已经保存了 duration
,它实现了 UITableViewCell
所需的所有功能,但我假设您可能需要使用 AVPlayerItem
做一些事情,所以我也保存。但是如果以后不再需要 AVPlayerItem
,那么不要将其保存在 RowData
对象中。另外,我不知道这些是多大,但你可能想写一个 didReceiveMemoryWarning
,将迭代你的对象
并将每个项目的 playerItem
对象设置为 nil
。
I have this code in the - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
delegate call:
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:[webUrls objectAtIndex:indexPath.row]];
CMTime timeduration = playerItem.duration;
float seconds = CMTimeGetSeconds(timeduration);
NSString *duration = [NSString stringWithFormat:@"%f", seconds];
dispatch_async( dispatch_get_main_queue(), ^{
UITableViewCell *updatecell = [tblView cellForRowAtIndexPath:indexPath];
updatecell.detailTextLabel.text = duration;
[updatecell setNeedsLayout];
});
});
Each cell slowly, in the background, loads up the seconds
into the updatecell.detailTextLabel.text
on the cell. The problem is after I scroll, after about 3 or 4 cells loaded, the rest just show 0 in the detailTextLabel quickly and don't load.
Any ideas why this is? Am I not doing my threading correctly?
解决方案 A couple of thoughts:
Many servers impose a constraint of how many concurrent requests they accept from a given client. I'd suggest you use NSOperationQueue
to constrain how many concurrent requests you make of your server to 4 or 5, rather than using dispatch queues.
You're potentially making the issue worse than it needs to be, because if you scroll the table view down and then back up, when you redisplay the first few cells, you're re-downloading the AVPlayerItem
and trying to make additional concurrent requests of your server. You really should save the results of your previous downloads, to eliminate the need for redundant re-requests of the same data.
You're not currently checking to see if the cell that just downloaded is still visible before trying to update the UI. You really should check that.
So, I might suggest the following:
In your view controller's viewDidLoad
, create the NSOperationQueue
that we'll use for downloading. Also specify how many concurrent operations your server will permit:
downloadQueue = [[NSOperationQueue alloc] init];
downloadQueue.maxConcurrentOperationCount = 4; // replace this with an appropriate value for your server
Before, you had an array, webUrls
, which was an array of NSURL
objects. In point #4, below, we'll discuss retiring that array, and create a new array of row objects. But before we can do that, we should create this new RowData
object.
Each row object will have not only the webURL
, but also other things, such as the durationText
and perhaps even the AVPlayerItem
itself. (By keeping these other object properties, when a cell scrolls back into view, we don't need to re-download the data.) So the public interface for this new class might look like:
//
// RowData.h
//
#import <Foundation/Foundation.h>
@class AVPlayerItem;
@interface RowData : NSObject
@property (nonatomic, strong) NSURL *webURL;
@property (nonatomic, strong) NSString *durationText;
@property (nonatomic, strong) AVPlayerItem *playerItem;
@property (nonatomic, getter = isDownloaded, readonly) BOOL downloaded;
@property (nonatomic, getter = isDownloading, readonly) BOOL downloading;
- (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))block;
- (void)cancelDownload;
@end
By the way, I'm not crazy about the class name, RowData
. It's a little too ambiguous. But I don't know enough about the nature of your model data to suggest a better name. Feel free to call this class whatever you think is appropriate.
Your new RowData
class can have an instance method, called downloadInQueue
, that performs the download, sets the durationText
appropriately, etc. By moving the download logic here, we successfully isolate cellForRowAtIndexPath
from some of the gory details involved with downloading. Just as importantly, though, this downloadInQueue
method won't update the user interface itself, but rather it has completion
block provided by cellForRowAtIndexPath
(demonstrated in point #5, below), so this downloadInQueue
method doesn't have to worry about UI considerations. Anyway, the implementation of downloadInQueue
might look like:
//
// RowData.m
//
#import "RowData.h"
#import <AVFoundation/AVFoundation.h>
@interface RowData ()
@property (nonatomic, getter = isDownloaded) BOOL downloaded;
@property (nonatomic, getter = isDownloading) BOOL downloading;
@property (nonatomic, weak) NSOperation *operation;
@end
@implementation RowData
- (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))completion
{
if (!self.isDownloading)
{
self.downloading = YES;
NSOperation *currentOperation = [NSBlockOperation blockOperationWithBlock:^{
BOOL success = NO;
self.playerItem = [AVPlayerItem playerItemWithURL:self.webURL];
if (self.playerItem)
{
success = YES;
CMTime timeduration = self.playerItem.duration;
float seconds = CMTimeGetSeconds(timeduration);
self.durationText = [NSString stringWithFormat:@"%f", seconds];
}
self.downloading = NO;
self.downloaded = YES;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completion(success);
}];
}];
[queue addOperation:currentOperation];
self.operation = currentOperation;
}
}
- (void)cancelDownload
{
if ([self isDownloading] && self.operation)
{
self.downloading = NO;
[self.operation cancel];
}
}
@end
In your main view controller, rather than creating your old array of webUrls
, create a new array of these RowData
objects called, for example, objects
. Set the webURL
property for each of those RowData
objects, of course. (Again, I'm not crazy about the ambiguous name of objects
, but I don't know enough about your app to make a more specific suggestion. Call this whatever you want. But my code below will use objects
.)
Finally, modify your cellForRowAtIndexPath
to use this new RowData
object and it's downloadInQueue
method. Also, note, the completion
block checks to make sure the cell is still visible:
RowData *rowData = self.objects[indexPath.row];
if ([rowData isDownloaded])
{
cell.detailTextLabel.text = rowData.durationText;
}
else
{
cell.detailTextLabel.text = @"..."; // you really should initialize this so we show something during download or remove anything previously there
[rowData downloadInQueue:self.downloadQueue completion:^(BOOL success) {
// note, if you wanted to change your behavior based upon whether the
// download was successful or not, just use the `success` variable
UITableViewCell *updateCell = [tblView cellForRowAtIndexPath:indexPath];
// by the way, make sure the cell is still on screen
if (updateCell)
{
updateCell.detailTextLabel.text = rowData.durationText;
[updateCell setNeedsLayout];
}
}];
}
If using iOS 6, if you want to cancel pending downloads when a cell scrolls off the screen, you can use the didEndDisplayingCell
method of the UITableViewDelegate
protocol.
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
RowData *rowData = self.objects[indexPath.row];
if ([rowData isDownloading])
[rowData cancelDownload];
}
If supporting earlier versions of iOS, you'd have to use UIScrollViewDelegate
protocol methods, such as scrollViewDidScroll
, determine which cells have scrolled off the screen (e.g. are not included in indexPathsForVisibleRows
) manually, but the idea is the same.
By the way, in my sample RowData
above, I'm saving the AVPlayerItem
. You should only do that if you need the AVPlayerItem
later. We've saved the duration
, which achieves all we need for the UITableViewCell
, but I assume you might later want to do something with the AVPlayerItem
, so I save that, too. But if you're not going to need that AVPlayerItem
later, then don't save it in the RowData
object. Also, I don't know how big those are, but you might want to write a didReceiveMemoryWarning
that will iterate through your objects
and set each item's playerItem
object to nil
.
这篇关于为什么我的线程似乎失败后几个线程已经开始在iOS上?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!