多个 NSURLSession 导致 UITableView 问题
Multiple NSURLSessions Causing UITableView Problems
我运行遇到了一个奇怪的问题。我的一个 NSURLSession 负责获取我存储的餐厅信息(餐厅名称、餐厅徽标 URL 等),然后第二个 NSURLSession 是负责使用餐厅的徽标 URL 检索特定图像并为每个 UITableView 的单元格设置它。
但是,问题是我的 UITableView 有时根本不加载任何内容,因此单元格为空,但在其他时候,当我在 NSURLSessions 中添加额外的 [_tableView reload]
时' fetchPosts 方法中的完成块,它会工作,但如果我重新 运行 它,单元格将再次停止显示任何内容。肯定有问题。看看下面我的代码:
#import "MainViewController.h"
#import "SWRevealViewController.h"
#import "RestaurantNameViewCell.h"
#import "RestaurantList.h"
@interface MainViewController ()
@end
@implementation MainViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//List of restaurants needed to load home page
_restaurantInformationArray = [[NSMutableArray alloc] init];
self.tableView.dataSource = self;
self.tableView.delegate = self;
//setup for sidebar
SWRevealViewController *revealViewController = self.revealViewController;
if ( revealViewController )
{
[self.sidebarButton setTarget: self.revealViewController];
[self.sidebarButton setAction: @selector( revealToggle: )];
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
}
//Get list of restaurants and their image URLs
[self fetchPosts];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_restaurantInformationArray count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RestaurantNameViewCell *cell = (RestaurantNameViewCell *)[_tableView dequeueReusableCellWithIdentifier:@"restaurantName" forIndexPath:indexPath];
RestaurantList *currentRestaurant = [_restaurantInformationArray objectAtIndex:indexPath.row];
cell.restaurantName.text = currentRestaurant.name;
cell.imageAddress = currentRestaurant.imageURL;
cell.restaurantClicked = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected:)];
cell.restaurantClicked.numberOfTapsRequired = 1;
cell.restaurantLogo.userInteractionEnabled = YES;
[cell.restaurantLogo addGestureRecognizer:cell.restaurantClicked];
cell.restaurantLogo.tag = indexPath.row;
//Add restaurant logo image:
NSString *URL = [NSString stringWithFormat:@"http://private.com/images/%@.png",cell.imageAddress];
NSURL *url = [NSURL URLWithString:URL];
NSURLSessionDownloadTask *downloadLogo = [[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]];
cell.restaurantLogo.image = downloadedImage;
}];
[downloadLogo resume];
return cell;
}
-(void)fetchPosts {
NSString *address = @"http://localhost/xampp/restaurants.php";
NSURL *url = [NSURL URLWithString:address];
NSURLSessionDataTask *downloadRestaurants = [[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSError *someError;
NSArray *restaurantInfo = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&someError];
for(NSDictionary *dict in restaurantInfo) {
RestaurantList *newRestaurant = [[RestaurantList alloc]init];
newRestaurant.name = [dict valueForKey:@"name"];
newRestaurant.imageURL = [dict valueForKey:@"image"];
[_restaurantInformationArray addObject:newRestaurant];
//Refresh table view to make sure the cells have info AFTER the above stuff is done
[_tableView reloadData];
}
}];
[downloadRestaurants resume];
}
@end
这可能是我犯的一个非常愚蠢的错误,但我不确定我应该如何纠正它。我是 iOS 开发的新手,非常感谢一些指导:)
除了假设您的网络请求没有错误(如果有网络错误,您至少应该记录),还有线程问题。
您的 NSURLSession
回调可能 运行 在后台线程上。这使得调用 UIKit(又名 - [_tableView reloadData]
)变得不安全。 UIKit 不是线程安全的。这意味着从另一个线程调用任何 UIKit 的 API 都会产生不确定的行为。您需要 运行 主线程上的那段代码:
dispatch_async(dispatch_get_main_queue(), ^{
[_tableView reloadData];
});
同样用于获取图像。它稍微复杂一些,因为 table 视图单元格重用可能导致滚动时显示错误的图像。这是因为当用户滚动时,相同的单元格实例用于数组中的多个值。当这些回调中的任何一个触发时,它将替换恰好位于该单元格中的任何图像。重现此问题的一般步骤如下:
- TableView 请求 5 个单元格
- MainViewController 请求 5 张图像(每个单元格一张)
- 用户向下滚动一个单元格
- 第一个单元格被重新用作第 6 个单元格。
- MainViewController 请求第 6 个单元格的另一个图像。
- 获取到第6张图片,触发回调,第一个cell的图片设置为#6图片。
- 获取到第一张图片,触发回调,第一个单元格的图片设置为图片#1(不正确)。
在尝试将图像分配给单元格之前,您需要确保单元格显示的是正确的单元格。如果您不想在单元格中实现图像获取逻辑,则可以使用 SDWebImage instead. Using SDWebImage's [UIImageView sd_setImageWithURL:]
is thread safe (it will set the image on the main thread).
旁注:
- 您只需要在所有更改都在
_restaurantInformationArray
中重新加载数据,而不是每次都在 for 循环中。
我运行遇到了一个奇怪的问题。我的一个 NSURLSession 负责获取我存储的餐厅信息(餐厅名称、餐厅徽标 URL 等),然后第二个 NSURLSession 是负责使用餐厅的徽标 URL 检索特定图像并为每个 UITableView 的单元格设置它。
但是,问题是我的 UITableView 有时根本不加载任何内容,因此单元格为空,但在其他时候,当我在 NSURLSessions 中添加额外的 [_tableView reload]
时' fetchPosts 方法中的完成块,它会工作,但如果我重新 运行 它,单元格将再次停止显示任何内容。肯定有问题。看看下面我的代码:
#import "MainViewController.h"
#import "SWRevealViewController.h"
#import "RestaurantNameViewCell.h"
#import "RestaurantList.h"
@interface MainViewController ()
@end
@implementation MainViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//List of restaurants needed to load home page
_restaurantInformationArray = [[NSMutableArray alloc] init];
self.tableView.dataSource = self;
self.tableView.delegate = self;
//setup for sidebar
SWRevealViewController *revealViewController = self.revealViewController;
if ( revealViewController )
{
[self.sidebarButton setTarget: self.revealViewController];
[self.sidebarButton setAction: @selector( revealToggle: )];
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
}
//Get list of restaurants and their image URLs
[self fetchPosts];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_restaurantInformationArray count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RestaurantNameViewCell *cell = (RestaurantNameViewCell *)[_tableView dequeueReusableCellWithIdentifier:@"restaurantName" forIndexPath:indexPath];
RestaurantList *currentRestaurant = [_restaurantInformationArray objectAtIndex:indexPath.row];
cell.restaurantName.text = currentRestaurant.name;
cell.imageAddress = currentRestaurant.imageURL;
cell.restaurantClicked = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected:)];
cell.restaurantClicked.numberOfTapsRequired = 1;
cell.restaurantLogo.userInteractionEnabled = YES;
[cell.restaurantLogo addGestureRecognizer:cell.restaurantClicked];
cell.restaurantLogo.tag = indexPath.row;
//Add restaurant logo image:
NSString *URL = [NSString stringWithFormat:@"http://private.com/images/%@.png",cell.imageAddress];
NSURL *url = [NSURL URLWithString:URL];
NSURLSessionDownloadTask *downloadLogo = [[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]];
cell.restaurantLogo.image = downloadedImage;
}];
[downloadLogo resume];
return cell;
}
-(void)fetchPosts {
NSString *address = @"http://localhost/xampp/restaurants.php";
NSURL *url = [NSURL URLWithString:address];
NSURLSessionDataTask *downloadRestaurants = [[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSError *someError;
NSArray *restaurantInfo = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&someError];
for(NSDictionary *dict in restaurantInfo) {
RestaurantList *newRestaurant = [[RestaurantList alloc]init];
newRestaurant.name = [dict valueForKey:@"name"];
newRestaurant.imageURL = [dict valueForKey:@"image"];
[_restaurantInformationArray addObject:newRestaurant];
//Refresh table view to make sure the cells have info AFTER the above stuff is done
[_tableView reloadData];
}
}];
[downloadRestaurants resume];
}
@end
这可能是我犯的一个非常愚蠢的错误,但我不确定我应该如何纠正它。我是 iOS 开发的新手,非常感谢一些指导:)
除了假设您的网络请求没有错误(如果有网络错误,您至少应该记录),还有线程问题。
您的 NSURLSession
回调可能 运行 在后台线程上。这使得调用 UIKit(又名 - [_tableView reloadData]
)变得不安全。 UIKit 不是线程安全的。这意味着从另一个线程调用任何 UIKit 的 API 都会产生不确定的行为。您需要 运行 主线程上的那段代码:
dispatch_async(dispatch_get_main_queue(), ^{
[_tableView reloadData];
});
同样用于获取图像。它稍微复杂一些,因为 table 视图单元格重用可能导致滚动时显示错误的图像。这是因为当用户滚动时,相同的单元格实例用于数组中的多个值。当这些回调中的任何一个触发时,它将替换恰好位于该单元格中的任何图像。重现此问题的一般步骤如下:
- TableView 请求 5 个单元格
- MainViewController 请求 5 张图像(每个单元格一张)
- 用户向下滚动一个单元格
- 第一个单元格被重新用作第 6 个单元格。
- MainViewController 请求第 6 个单元格的另一个图像。
- 获取到第6张图片,触发回调,第一个cell的图片设置为#6图片。
- 获取到第一张图片,触发回调,第一个单元格的图片设置为图片#1(不正确)。
在尝试将图像分配给单元格之前,您需要确保单元格显示的是正确的单元格。如果您不想在单元格中实现图像获取逻辑,则可以使用 SDWebImage instead. Using SDWebImage's [UIImageView sd_setImageWithURL:]
is thread safe (it will set the image on the main thread).
旁注:
- 您只需要在所有更改都在
_restaurantInformationArray
中重新加载数据,而不是每次都在 for 循环中。