如何在iOS上展现Web Service数据
2013-04-03 14:50
218 查看
在iOS开发中,需要和WEB服务器进行交互,如将一批来自WEB SERVICE的数据展现在表格上。数据交互格式是XML,使用的协议是SOAP。请求的数据中有图片,通常图片都会是一个URL重连接,需要再得到这个URL后下载到终端才展现出来。
如果你使用的是浏览器,那么这一切它都做好了。但如果你要更灵活的展现和处理这些数据,这需要开发一个应用。
在该类中手工添加UITableView对象resultTableView,用于展现WEB Service中请求来的数据。WEB SERVICE使用SOAP协议交互。建一个数据请求类XYQueryHotel,使用它的delegate将数据以数组的形式回调回来。
在这个数据请求类中,使用异步请求数据,将收到的XML格式的数据使用NSXMLParser类进行分析。
在视图控制器类XYViewController请求数据过程中,不可避免地会有一个等待出现,但UI可以继续,因为是异步请求操作。这个上面可以设置一些用于杀时间的有趣味的小图片,避免枯燥的等待,提升UI友好度。
在数据请求类操作完成后,通过delegate方式返回了数据给视图控制器类XYViewController中一个属性resultHotels。视图控制器类XYViewController将该属性的数据展现在UITableView对象resultTableView中。
对于resultTableView,通过的datasource方法(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath,将能显示在屏幕上的cell设置好数据。
在resultTableView中,还有一个图片信息需要展现,这个需要通过resultHotels中图片URL去二次请求web service。
这个过程也需要异步去实现,包括请求到图片uiimage数据,请求到的数据的优化,请求到的数据展现等操作,反正不能影响到UI。这是整个实现过程中的关键点,也是难点。
这个请求操作是数据一开始加载时就发出。(UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath,这个方法中在cell填充时使用多线程发出请求。它的实现代码如下:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
static NSString *CellIdentifier =@"resultCell";
//初始化cell并指定其类型,也可自定义cell
hotelCell = (XYHotelCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(hotelCell == nil)
{
//将Custom.xib中的所有对象载入
NSArray *nib = [[NSBundle mainBundle]loadNibNamed:@"KaiFangTableViewCell" owner:nil options:nil];
//第一个对象就是CustomCell了
hotelCell = [nib objectAtIndex:0];
}
switch (indexPath.section) {
case 0://对应各自的分区
//修改CustomCell的控件
hotelInfo=[resultHotelsobjectAtIndex:indexPath.row];
hotelCell.name.text = [hotelInfoname];
hotelCell.addr.text = [hotelInfoaddr];
hotelCell.distance.text = [NSStringstringWithFormat:@"%@,%@",[hotelInfo lat],[hotelInfo lng]];
if(hotelInfo.hasLogoImage)
{
hotelCell.logo.image=hotelInfo.logo;
}
else
{
hotelCell.logo.image = [UIImageimageNamed:@"Placeholder.png"];
if (!resultTableView.dragging&& !resultTableView.decelerating) {
[selfstartOperationsForHotelLogo:hotelInfo atIndexPath:indexPath];
}
}
//返回CustomCell
return hotelCell;
break;
}
return hotelCell;//返回cell
}
数组resultHotels中保存类HotelInfo的对象的记录。类HotelInfo是一个单独定义的数据类,属于MVC中MODE范畴。
方法tableView:cellForRowAtIndexPath:,根据在屏幕上显示的cell行号,得到该数组中去动态取对应行的记录。然后将得到的记录信息填写到cell中,用于展现,并将得到的记录,通过一个方法 [self startOperationsForHotelLogo:hotelInfoatIndexPath:indexPath];去多线程请求展现图片。
这个方法 [self startOperationsForHotelLogo:hotelInfoatIndexPath:indexPath]中包括了图片从WEB SERVICE上下载操作,图片美化操作,图片展示到对应的CELL中的操作,并修改提交的记录中信息,再二次展现时如上下滚动时不需要再二次请求该方法了。
如果用户退出这个视图控制器类,那么该数组resultHotels如何缓存,怎不能再将WEB SERVICE交互再做一次把。
继续实现[selfstartOperationsForHotelLogo:hotelInfo atIndexPath:indexPath]方法,
-(void)startOperationsForHotelLogo:(HotelInfo *)hotel atIndexPath:(NSIndexPath*)indexPath {
if (!hotel.hasLogoImage) {
[selfstartImageDownloadingForHotelLogo:hotel atIndexPath:indexPath];
}
if (!record.isFiltered) {
[self startImageFiltrationForRecord:recordatIndexPath:indexPath];
}
}
根据传入的hotel对象中hasLogoImage来决定是否下载,再根据isFiltered来决定是否美化它。
继续实现(void)startImageDownloadingForHotelLogo:(HotelInfo*)hotel atIndexPath:(NSIndexPath *)indexPathWithLogo方法。
-(void)startImageDownloadingForHotelLogo:(HotelInfo *)hotelatIndexPath:(NSIndexPath *)indexPathWithLogo {
if (self.pendingOperations==nil)
{
self.pendingOperations=[[PendingOperationsalloc] init];
NSLog(@"pendingOperationsinitial");
}
if(![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPathWithLogo]){
HotelImageDownloader *imageDownloader = [[HotelImageDownloader alloc]initWithHotel:hotel atIndexPath:indexPathWithLogo delegate:self];
[self.pendingOperations.downloadsInProgress setObject:imageDownloaderforKey:indexPathWithLogo];
[self.pendingOperations.downloadQueueaddOperation:imageDownloader];
NSLog(@"operation count is%d",[self.pendingOperations.downloadQueue operationCount]);
}
}
首先,分配并初始化一个对象pendingOperations,用于保存操作队列的NSMutableDictionary对象和操作的NSOperationQueue对象。前者保存了哪些操作在操作队列中,然后多线程运行该操作。如果多线程操作取消了,那么也需要在操作队列中删除该操作。
接着,判断这个indexPath对应的记录在不在下载队列中,如果在,就不再二次操作了。如果不在,执行下载操作。
而这个下载操作的实现是一个非常关键的点。它需要多线程实现,并在完成后反馈到主线程中。多线程的实现方法有很多种,就我知道的有三种方式,分别是NSThread,NSOperation,Grand Central Dispatch (GCD)。
GCD的实现方法用来实现比较简单的逻辑,我不知道怎么调用delegate。
dispatch_queue_timageQueue=dispatch_queue_create("hotelInfo.logo.imageQueue", NULL);
dispatch_async(imageQueue, ^{
NSData *imageData = [NSDatadataWithContentsOfURL:[hotel logoURL]];
dispatch_async(dispatch_get_main_queue(),^{
[hotelInfo setLogo:[UIImageimageWithData:imageData]];
[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPathWithLogo]withRowAnimation:UITableViewRowAnimationNone]; });
});
NSOperation是在GCD基础上封装,使用更简单些。如果实现简单的逻辑,只需要用block;如果实现复杂的逻辑,也只需要以NSOperation为父类,重写main()方法即可。 NSOperation是一个抽象类。(我现在也不明白啥叫抽象类)
NSThread的实现方法,我没用过,不介绍了,自己gogole把。
在这里使用NSOperation的子类HotelImageDownloader来实现下载操作,再将这个HotelImageDownloader类定义对象加到NSOperationQueue中,实现后台下载操作。
将hotelInfo,indexPath和delegate传到该对象中去,这些是需要实现的操作的必备条件,并使用delegate回调到主线程,以更新UI。
HotelImageDownloader*imageDownloader = [[HotelImageDownloader alloc] initWithHotel:hotelatIndexPath:indexPathWithLogo delegate:self];
HotelImageDownloader的主要函数定义如下:
#pragmamark -
#pragmamark - Life Cycle
-(id)initWithHotel:(HotelInfo *)hotel atIndexPath:(NSIndexPath *)indexPathdelegate:(id<HotelImageDownloaderDelegate>)theDelegate
{
if (self = [super init]) {
// Set the properties.
self.delegate = theDelegate;
self.indexPathInTableView = indexPath;
self.hotelInfo = hotel;
}
return self;
}
#pragmamark -
#pragmamark - Downloading image
// 3:Regularly check for isCancelled, to make sure the operation terminates as soonas possible.
-(void)main {
// 4: Apple recommends using@autoreleasepool block instead of alloc and init NSAutoreleasePool, becauseblocks are more efficient. You might use NSAuoreleasePool instead and thatwould be fine.
if (self.isCancelled)
return;
NSData *imageData = [[NSData alloc]initWithContentsOfURL:self.hotelInfo.logoURL];
if (self.isCancelled) {
imageData = nil;
return;
}
if (imageData) {
UIImage *downloadedImage = [UIImageimageWithData:imageData];
self.hotelInfo.logo = downloadedImage;
}
else {
self.hotelInfo.failed = YES;
}
imageData = nil;
if (self.isCancelled)
return;
// NSLog(@"performSelectorOnMainThread");
// 5: Cast the operation to NSObject, andnotify the caller on the main thread.
[(NSObject *)self.delegateperformSelectorOnMainThread:@selector(logoImageDownloaderDidFinish:)withObject:self waitUntilDone:NO];
}
代码[(NSObject*)self.delegate performSelectorOnMainThread:@selector(logoImageDownloaderDidFinish:)withObject:self waitUntilDone:NO];就是实现到主线程的回调,将HotelImageDownloader类的对象imageDownloader通过参数项"withObject:self"传回去,self就是imageDownloader对象,它的属性有hotelInfo信息和indexPath信息。通过这些信息实现WEB
SERVICE的请求结果集hotelResults的更新和UITABLEVIEW对应的cell的更新(也就是更新uiimage)。
这个操作是使用delegate实现的,也有人用NSNotificationCenter实现。
它的大致实现过程是这样.
首先,在视图控制器类XYViewController中的viewDidLoad方法中增加一个通知。名称为@"hotelLogoDownloader.completed"。该通知在触发时,会调用一个方法"logoImageDownloaderDidFinish:"。这个方法就是委托中需要实现的方法。
[[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(logoImageDownloaderDidFinish:)name:@"hotelLogoDownloader.completed" object:nil];
其次,在HotelImageDownloader类中main()方法中,添加一个发送通知的代码,和这里调用delegate操作异曲同工。
NSDictionary*userInfo=[NSDictionary dictionaryWithObjectsAndKeys:[hotelobjectForKey:@"code"],@"code" ,nil];
[[NSNotificationCenterdefaultCenter] postNotificationName:@"hotelLogoDownloader.completed"object:self userInfo:userInfo];
通知提交操作的参数项"object:self"传回去,self就是imageDownloader对象,它的属性有hotelInfo信息和indexPath信息。
这样也实现了消息传递机制。
两个方法都是为了将消息传递到主线程,属于跨线程,跨方法的消息传递操作。
不管使用何种方式,在主线程的视图控制器类XYViewController中,都要实现方法"logoImageDownloaderDidFinish:"的定义。
-(void)logoImageDownloaderDidFinish:(HotelImageDownloader *)downloader {
NSLog(@"logoImageDownloaderDidFinishis executed");
NSIndexPath *indexPath =downloader.indexPathInTableView;
HotelInfo *theHotel = downloader.hotelInfo;
[hotelResultsreplaceObjectAtIndex:indexPath.row withObject:theHotel];
[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.pendingOperations.downloadsInProgressremoveObjectForKey:indexPath];
}
这个方法就是用来实现WEB SERVICE的请求结果集hotelResults的更新和UITABLEVIEW对应的cell的更新(也就是更新uiimage),并且在最后结束将下载队列字典中该键和值删除。
该逻辑的实现代码是最后三行。
代码[hotelResultsreplaceObjectAtIndex:indexPath.row withObject:theHotel];实现了根据行号更新数组hotelResults的对应行的记录。
代码[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];实现uitableview的datasource方法的调用。
这一方法会重新加载所指定indexPaths中的UITableViewCell实例,因为重新加载cell所以会请求这个UITableView实例的data source来获取新的cell;这个表会用动画效果让新的cell进入,并让旧的cell退出。会调用UITableViewDataSource协议中的所有方法来更新数据源,其中调用 (UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath
*)indexPath ,只会调用所需更新的行数,来获取新的cell。此时该cell的-(void)setSelected:(BOOL)selected animated:(BOOL)animated将被调用,所设置的selected为NO;
只是,不知道是只更新一个屏幕中正显示的cell还是会更新屏幕中正显示的所有的cell。
如果屏幕上将hotelResults的所有的记录一屏都显示完了,那么事情到这里就结果了。但是,很遗憾,这不是PC终端,没有那么大的屏幕。就是PC终端,也有一个lazy load功能,没那么啥一下子请求所有的数据,也是一屏幕满了,你下拉才会显示剩下的内容。这是基于性能考虑的。 在iphone终端上,这种需求更强烈。应用不可能将hotelResults所有记录都先写到UI内存中,而是屏幕显示多少行,显示那些行,就写入到屏幕内存中。
于是,新的问题就来了。
因为iphone屏幕会上下滚动,这是uitableview的最基本的功能。上下滚动操作过程中,屏幕上显示的hotelResults记录会变化。
如初始显示的是1-10行记录,下拉后显示的5-15行。 那么,前5行记录的logoImg信息,我们就不要再下载了,原来的下载操作队列需要取消,再增加11-15录的下载操作。如果下载了前行记录,再reloadRowsAtIndexPaths:withRowAnimation:操作时,那么显示操作就失败了,很明显没有办法更新屏幕了。
这个滚动操作,有uiscrollview的delegate实现,我们需要做的是将delegate的方法在主视图控制器类XYViewController中实现一下。
#pragmamark -
#pragmamark - UIScrollView delegate
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[self.pendingOperations.downloadQueuesetSuspended:YES];
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollViewwillDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self loadImagesForOnscreenCells];
[self.pendingOperations.downloadQueuesetSuspended:NO];
}
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 3: This delegate method tells you thattable view stopped scrolling, so you will do the same as in #2.
NSLog(@"scrollViewDidEndDecelerating");
[self loadImagesForOnscreenCells];
//[downloadLogoQueue setSuspended:NO];
[self.pendingOperations.downloadQueuesetSuspended:NO];
}
#pragmamark -
#pragmamark - Cancelling, suspending, resuming queues / operations
-(void)loadImagesForOnscreenCells {
NSSet *visibleRows = [NSSetsetWithArray:[resultTableView indexPathsForVisibleRows]];//现在展现在屏幕上的如6-15行
NSMutableSet *pendingOperations =[NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgressallKeys]];//正在下载队列的1-8行。
NSMutableSet *toBeCancelled =[pendingOperations mutableCopy];
NSMutableSet *toBeStarted = [visibleRowsmutableCopy];
[toBeStarted minusSet:pendingOperations];//现在展现在屏幕上的如6-15行,减去,正在下载队列的1-8行。得到需要加到下载队列的行。
[toBeCancelled minusSet:visibleRows];////正在下载队列的1-8行 ,减去,现在展现在屏幕上的如6-15行。得到需要取消下载队列的行。
for (NSIndexPath *anIndexPath intoBeCancelled) {
HotelImageDownloader *pendingDownload =[self.pendingOperations.downloadsInProgress objectForKey:anIndexPath];
[pendingDownload cancel];//就是调用[NSOperationcancel]; 取消该operation操作。
[self.pendingOperations.downloadsInProgressremoveObjectForKey:anIndexPath];
}
toBeCancelled = nil;
for (NSIndexPath *anIndexPath intoBeStarted) {
HotelInfo *hotelToProcess = [hotelInfosobjectAtIndex:anIndexPath.row];
[selfstartOperationsForHotelLogo:hotelToProcess atIndexPath:anIndexPath];//执行多线程下载,美化等用于图片展现的操作。
}
toBeStarted = nil;
}
方法[selfstartOperationsForHotelLogo:hotelToProcess atIndexPath:anIndexPath]也是在” (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath“方法中调用的。
视图滚动操作会刷新屏幕,也就是为调用”tableView:cellForRowAtIndexPath:“。那么,这里的bestarted调用是不是有点多余了,因为在tableView:cellForRowAtIndexPath:方法中,不是会自动调用"startOperationsForHotelLogo:atIndexPath:"方法吗?虽然这里调用了,在后来的也会因为(![self.pendingOperations.downloadsInProgress.allKeyscontainsObject:indexPathWithLogo])这个条件判断而不会加载到下载队列中去。但毕竟属于二次调用了。
本文参考了http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues。
如果你使用的是浏览器,那么这一切它都做好了。但如果你要更灵活的展现和处理这些数据,这需要开发一个应用。
1.实现过程
我建立一个简单的基于视图控制器的应用。新建的视图控制器类XYViewController。在该类中手工添加UITableView对象resultTableView,用于展现WEB Service中请求来的数据。WEB SERVICE使用SOAP协议交互。建一个数据请求类XYQueryHotel,使用它的delegate将数据以数组的形式回调回来。
在这个数据请求类中,使用异步请求数据,将收到的XML格式的数据使用NSXMLParser类进行分析。
在视图控制器类XYViewController请求数据过程中,不可避免地会有一个等待出现,但UI可以继续,因为是异步请求操作。这个上面可以设置一些用于杀时间的有趣味的小图片,避免枯燥的等待,提升UI友好度。
在数据请求类操作完成后,通过delegate方式返回了数据给视图控制器类XYViewController中一个属性resultHotels。视图控制器类XYViewController将该属性的数据展现在UITableView对象resultTableView中。
对于resultTableView,通过的datasource方法(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath,将能显示在屏幕上的cell设置好数据。
在resultTableView中,还有一个图片信息需要展现,这个需要通过resultHotels中图片URL去二次请求web service。
这个过程也需要异步去实现,包括请求到图片uiimage数据,请求到的数据的优化,请求到的数据展现等操作,反正不能影响到UI。这是整个实现过程中的关键点,也是难点。
这个请求操作是数据一开始加载时就发出。(UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath,这个方法中在cell填充时使用多线程发出请求。它的实现代码如下:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
static NSString *CellIdentifier =@"resultCell";
//初始化cell并指定其类型,也可自定义cell
hotelCell = (XYHotelCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(hotelCell == nil)
{
//将Custom.xib中的所有对象载入
NSArray *nib = [[NSBundle mainBundle]loadNibNamed:@"KaiFangTableViewCell" owner:nil options:nil];
//第一个对象就是CustomCell了
hotelCell = [nib objectAtIndex:0];
}
switch (indexPath.section) {
case 0://对应各自的分区
//修改CustomCell的控件
hotelInfo=[resultHotelsobjectAtIndex:indexPath.row];
hotelCell.name.text = [hotelInfoname];
hotelCell.addr.text = [hotelInfoaddr];
hotelCell.distance.text = [NSStringstringWithFormat:@"%@,%@",[hotelInfo lat],[hotelInfo lng]];
if(hotelInfo.hasLogoImage)
{
hotelCell.logo.image=hotelInfo.logo;
}
else
{
hotelCell.logo.image = [UIImageimageNamed:@"Placeholder.png"];
if (!resultTableView.dragging&& !resultTableView.decelerating) {
[selfstartOperationsForHotelLogo:hotelInfo atIndexPath:indexPath];
}
}
//返回CustomCell
return hotelCell;
break;
}
return hotelCell;//返回cell
}
数组resultHotels中保存类HotelInfo的对象的记录。类HotelInfo是一个单独定义的数据类,属于MVC中MODE范畴。
方法tableView:cellForRowAtIndexPath:,根据在屏幕上显示的cell行号,得到该数组中去动态取对应行的记录。然后将得到的记录信息填写到cell中,用于展现,并将得到的记录,通过一个方法 [self startOperationsForHotelLogo:hotelInfoatIndexPath:indexPath];去多线程请求展现图片。
这个方法 [self startOperationsForHotelLogo:hotelInfoatIndexPath:indexPath]中包括了图片从WEB SERVICE上下载操作,图片美化操作,图片展示到对应的CELL中的操作,并修改提交的记录中信息,再二次展现时如上下滚动时不需要再二次请求该方法了。
如果用户退出这个视图控制器类,那么该数组resultHotels如何缓存,怎不能再将WEB SERVICE交互再做一次把。
继续实现[selfstartOperationsForHotelLogo:hotelInfo atIndexPath:indexPath]方法,
-(void)startOperationsForHotelLogo:(HotelInfo *)hotel atIndexPath:(NSIndexPath*)indexPath {
if (!hotel.hasLogoImage) {
[selfstartImageDownloadingForHotelLogo:hotel atIndexPath:indexPath];
}
if (!record.isFiltered) {
[self startImageFiltrationForRecord:recordatIndexPath:indexPath];
}
}
根据传入的hotel对象中hasLogoImage来决定是否下载,再根据isFiltered来决定是否美化它。
继续实现(void)startImageDownloadingForHotelLogo:(HotelInfo*)hotel atIndexPath:(NSIndexPath *)indexPathWithLogo方法。
-(void)startImageDownloadingForHotelLogo:(HotelInfo *)hotelatIndexPath:(NSIndexPath *)indexPathWithLogo {
if (self.pendingOperations==nil)
{
self.pendingOperations=[[PendingOperationsalloc] init];
NSLog(@"pendingOperationsinitial");
}
if(![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPathWithLogo]){
HotelImageDownloader *imageDownloader = [[HotelImageDownloader alloc]initWithHotel:hotel atIndexPath:indexPathWithLogo delegate:self];
[self.pendingOperations.downloadsInProgress setObject:imageDownloaderforKey:indexPathWithLogo];
[self.pendingOperations.downloadQueueaddOperation:imageDownloader];
NSLog(@"operation count is%d",[self.pendingOperations.downloadQueue operationCount]);
}
}
首先,分配并初始化一个对象pendingOperations,用于保存操作队列的NSMutableDictionary对象和操作的NSOperationQueue对象。前者保存了哪些操作在操作队列中,然后多线程运行该操作。如果多线程操作取消了,那么也需要在操作队列中删除该操作。
接着,判断这个indexPath对应的记录在不在下载队列中,如果在,就不再二次操作了。如果不在,执行下载操作。
而这个下载操作的实现是一个非常关键的点。它需要多线程实现,并在完成后反馈到主线程中。多线程的实现方法有很多种,就我知道的有三种方式,分别是NSThread,NSOperation,Grand Central Dispatch (GCD)。
GCD的实现方法用来实现比较简单的逻辑,我不知道怎么调用delegate。
dispatch_queue_timageQueue=dispatch_queue_create("hotelInfo.logo.imageQueue", NULL);
dispatch_async(imageQueue, ^{
NSData *imageData = [NSDatadataWithContentsOfURL:[hotel logoURL]];
dispatch_async(dispatch_get_main_queue(),^{
[hotelInfo setLogo:[UIImageimageWithData:imageData]];
[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPathWithLogo]withRowAnimation:UITableViewRowAnimationNone]; });
});
NSOperation是在GCD基础上封装,使用更简单些。如果实现简单的逻辑,只需要用block;如果实现复杂的逻辑,也只需要以NSOperation为父类,重写main()方法即可。 NSOperation是一个抽象类。(我现在也不明白啥叫抽象类)
NSThread的实现方法,我没用过,不介绍了,自己gogole把。
在这里使用NSOperation的子类HotelImageDownloader来实现下载操作,再将这个HotelImageDownloader类定义对象加到NSOperationQueue中,实现后台下载操作。
将hotelInfo,indexPath和delegate传到该对象中去,这些是需要实现的操作的必备条件,并使用delegate回调到主线程,以更新UI。
HotelImageDownloader*imageDownloader = [[HotelImageDownloader alloc] initWithHotel:hotelatIndexPath:indexPathWithLogo delegate:self];
HotelImageDownloader的主要函数定义如下:
#pragmamark -
#pragmamark - Life Cycle
-(id)initWithHotel:(HotelInfo *)hotel atIndexPath:(NSIndexPath *)indexPathdelegate:(id<HotelImageDownloaderDelegate>)theDelegate
{
if (self = [super init]) {
// Set the properties.
self.delegate = theDelegate;
self.indexPathInTableView = indexPath;
self.hotelInfo = hotel;
}
return self;
}
#pragmamark -
#pragmamark - Downloading image
// 3:Regularly check for isCancelled, to make sure the operation terminates as soonas possible.
-(void)main {
// 4: Apple recommends using@autoreleasepool block instead of alloc and init NSAutoreleasePool, becauseblocks are more efficient. You might use NSAuoreleasePool instead and thatwould be fine.
if (self.isCancelled)
return;
NSData *imageData = [[NSData alloc]initWithContentsOfURL:self.hotelInfo.logoURL];
if (self.isCancelled) {
imageData = nil;
return;
}
if (imageData) {
UIImage *downloadedImage = [UIImageimageWithData:imageData];
self.hotelInfo.logo = downloadedImage;
}
else {
self.hotelInfo.failed = YES;
}
imageData = nil;
if (self.isCancelled)
return;
// NSLog(@"performSelectorOnMainThread");
// 5: Cast the operation to NSObject, andnotify the caller on the main thread.
[(NSObject *)self.delegateperformSelectorOnMainThread:@selector(logoImageDownloaderDidFinish:)withObject:self waitUntilDone:NO];
}
代码[(NSObject*)self.delegate performSelectorOnMainThread:@selector(logoImageDownloaderDidFinish:)withObject:self waitUntilDone:NO];就是实现到主线程的回调,将HotelImageDownloader类的对象imageDownloader通过参数项"withObject:self"传回去,self就是imageDownloader对象,它的属性有hotelInfo信息和indexPath信息。通过这些信息实现WEB
SERVICE的请求结果集hotelResults的更新和UITABLEVIEW对应的cell的更新(也就是更新uiimage)。
这个操作是使用delegate实现的,也有人用NSNotificationCenter实现。
它的大致实现过程是这样.
首先,在视图控制器类XYViewController中的viewDidLoad方法中增加一个通知。名称为@"hotelLogoDownloader.completed"。该通知在触发时,会调用一个方法"logoImageDownloaderDidFinish:"。这个方法就是委托中需要实现的方法。
[[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(logoImageDownloaderDidFinish:)name:@"hotelLogoDownloader.completed" object:nil];
其次,在HotelImageDownloader类中main()方法中,添加一个发送通知的代码,和这里调用delegate操作异曲同工。
NSDictionary*userInfo=[NSDictionary dictionaryWithObjectsAndKeys:[hotelobjectForKey:@"code"],@"code" ,nil];
[[NSNotificationCenterdefaultCenter] postNotificationName:@"hotelLogoDownloader.completed"object:self userInfo:userInfo];
通知提交操作的参数项"object:self"传回去,self就是imageDownloader对象,它的属性有hotelInfo信息和indexPath信息。
这样也实现了消息传递机制。
两个方法都是为了将消息传递到主线程,属于跨线程,跨方法的消息传递操作。
不管使用何种方式,在主线程的视图控制器类XYViewController中,都要实现方法"logoImageDownloaderDidFinish:"的定义。
-(void)logoImageDownloaderDidFinish:(HotelImageDownloader *)downloader {
NSLog(@"logoImageDownloaderDidFinishis executed");
NSIndexPath *indexPath =downloader.indexPathInTableView;
HotelInfo *theHotel = downloader.hotelInfo;
[hotelResultsreplaceObjectAtIndex:indexPath.row withObject:theHotel];
[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.pendingOperations.downloadsInProgressremoveObjectForKey:indexPath];
}
这个方法就是用来实现WEB SERVICE的请求结果集hotelResults的更新和UITABLEVIEW对应的cell的更新(也就是更新uiimage),并且在最后结束将下载队列字典中该键和值删除。
该逻辑的实现代码是最后三行。
代码[hotelResultsreplaceObjectAtIndex:indexPath.row withObject:theHotel];实现了根据行号更新数组hotelResults的对应行的记录。
代码[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];实现uitableview的datasource方法的调用。
这一方法会重新加载所指定indexPaths中的UITableViewCell实例,因为重新加载cell所以会请求这个UITableView实例的data source来获取新的cell;这个表会用动画效果让新的cell进入,并让旧的cell退出。会调用UITableViewDataSource协议中的所有方法来更新数据源,其中调用 (UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath
*)indexPath ,只会调用所需更新的行数,来获取新的cell。此时该cell的-(void)setSelected:(BOOL)selected animated:(BOOL)animated将被调用,所设置的selected为NO;
只是,不知道是只更新一个屏幕中正显示的cell还是会更新屏幕中正显示的所有的cell。
如果屏幕上将hotelResults的所有的记录一屏都显示完了,那么事情到这里就结果了。但是,很遗憾,这不是PC终端,没有那么大的屏幕。就是PC终端,也有一个lazy load功能,没那么啥一下子请求所有的数据,也是一屏幕满了,你下拉才会显示剩下的内容。这是基于性能考虑的。 在iphone终端上,这种需求更强烈。应用不可能将hotelResults所有记录都先写到UI内存中,而是屏幕显示多少行,显示那些行,就写入到屏幕内存中。
于是,新的问题就来了。
因为iphone屏幕会上下滚动,这是uitableview的最基本的功能。上下滚动操作过程中,屏幕上显示的hotelResults记录会变化。
如初始显示的是1-10行记录,下拉后显示的5-15行。 那么,前5行记录的logoImg信息,我们就不要再下载了,原来的下载操作队列需要取消,再增加11-15录的下载操作。如果下载了前行记录,再reloadRowsAtIndexPaths:withRowAnimation:操作时,那么显示操作就失败了,很明显没有办法更新屏幕了。
这个滚动操作,有uiscrollview的delegate实现,我们需要做的是将delegate的方法在主视图控制器类XYViewController中实现一下。
#pragmamark -
#pragmamark - UIScrollView delegate
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[self.pendingOperations.downloadQueuesetSuspended:YES];
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollViewwillDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self loadImagesForOnscreenCells];
[self.pendingOperations.downloadQueuesetSuspended:NO];
}
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 3: This delegate method tells you thattable view stopped scrolling, so you will do the same as in #2.
NSLog(@"scrollViewDidEndDecelerating");
[self loadImagesForOnscreenCells];
//[downloadLogoQueue setSuspended:NO];
[self.pendingOperations.downloadQueuesetSuspended:NO];
}
#pragmamark -
#pragmamark - Cancelling, suspending, resuming queues / operations
-(void)loadImagesForOnscreenCells {
NSSet *visibleRows = [NSSetsetWithArray:[resultTableView indexPathsForVisibleRows]];//现在展现在屏幕上的如6-15行
NSMutableSet *pendingOperations =[NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgressallKeys]];//正在下载队列的1-8行。
NSMutableSet *toBeCancelled =[pendingOperations mutableCopy];
NSMutableSet *toBeStarted = [visibleRowsmutableCopy];
[toBeStarted minusSet:pendingOperations];//现在展现在屏幕上的如6-15行,减去,正在下载队列的1-8行。得到需要加到下载队列的行。
[toBeCancelled minusSet:visibleRows];////正在下载队列的1-8行 ,减去,现在展现在屏幕上的如6-15行。得到需要取消下载队列的行。
for (NSIndexPath *anIndexPath intoBeCancelled) {
HotelImageDownloader *pendingDownload =[self.pendingOperations.downloadsInProgress objectForKey:anIndexPath];
[pendingDownload cancel];//就是调用[NSOperationcancel]; 取消该operation操作。
[self.pendingOperations.downloadsInProgressremoveObjectForKey:anIndexPath];
}
toBeCancelled = nil;
for (NSIndexPath *anIndexPath intoBeStarted) {
HotelInfo *hotelToProcess = [hotelInfosobjectAtIndex:anIndexPath.row];
[selfstartOperationsForHotelLogo:hotelToProcess atIndexPath:anIndexPath];//执行多线程下载,美化等用于图片展现的操作。
}
toBeStarted = nil;
}
方法[selfstartOperationsForHotelLogo:hotelToProcess atIndexPath:anIndexPath]也是在” (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath“方法中调用的。
视图滚动操作会刷新屏幕,也就是为调用”tableView:cellForRowAtIndexPath:“。那么,这里的bestarted调用是不是有点多余了,因为在tableView:cellForRowAtIndexPath:方法中,不是会自动调用"startOperationsForHotelLogo:atIndexPath:"方法吗?虽然这里调用了,在后来的也会因为(![self.pendingOperations.downloadsInProgress.allKeyscontainsObject:indexPathWithLogo])这个条件判断而不会加载到下载队列中去。但毕竟属于二次调用了。
2.总结
在这个逻辑实现过程中,涉及到众多知识点。其中delegate,uitableview,nsoperation,nsoperationqueue,nsxmlparser,NSNotificationCenter,也数组的深度复制,字典的处理等,本文参考了http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues。
相关文章推荐
- iOS后台如何保持socket长连接和数据传输
- 如何使用c标签将数据库数据用forEach依次展现出来
- [iOS]如何给Label或者TextView赋HTML数据
- 如何写一个使用Web Service的IOS应用
- iOS教程:如何使用Core Data – 预加载和引入数据
- iOS后台如何保持socket长连接和数据传输
- iOS中获取设备数据以及如何获取应用信息之UIDevice的用法
- SharePoint:DataView如何绑定Web Service返回的主从表数据集
- SharePoint:DataView如何绑定Web Service返回的主从表数据集
- 如何使用R进行数据展现?且看使用iris数据可视化实例
- 关于 Ajax 的 Java 对象序列化 及获得数据后如何展现出来
- SharePoint:DataView如何绑定Web Service返回的主从表数据集
- IOS开发教程 - 如何通过二进制数据上传图片
- SharePoint:DataView如何绑定Web Service返回的主从表数据集
- iOS系统如何实现网络数据抓包
- IOS Q&A 我应该如何在网络传输中保持数据的安全性?
- iOS后台如何保持socket长连接和数据传输
- ios iphone 如何序列化存储,保存数据。
- iOS开发点滴 - 如何通过Segue写代码传递数据从一个ViewController到另一个ViewController(Swift代码)
- [ios开发基础之 Core Data[2]]如何使用Core Data – 预加载和引入数据