学习带图片的列表
官方 LazyTableImages demo http://download.csdn.net/detail/jlyidianyuan/5726749
分析源码是学习的好方法。
源码结构如上,不能运行,加红框内容。
项目结构
挨个看源文件
/* Copyright (C) 2017 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Application delegate for the LazyTableImages sample. It also downloads in the background the "Top Paid iPhone Apps" RSS feed using NSURLSession/NSURLSessionDataTask. */ #import "LazyTableAppDelegate.h" #import "RootViewController.h" #import "ParseOperation.h" #import "AppRecord.h" // the http URL used for fetching the top iOS paid apps on the App Store static NSString *const TopPaidAppsFeed = @"http://phobos.apple.com/WebObjects/MZStoreServices.woa/ws/RSS/toppaidapplications/limit=75/xml"; @interface LazyTableAppDelegate () // the queue to run our "ParseOperation" NSOperationQueue解析队q列,类似java线程池 @property (nonatomic, strong) NSOperationQueue *queue; // the NSOperation driving the parsing of the RSS feed 解析类操作 @property (nonatomic, strong) ParseOperation *parser; @end #pragma mark - @implementation LazyTableAppDelegate // The app delegate must implement the window @property // from UIApplicationDelegate @protocol to use a main storyboard file. // @synthesize window; // ------------------------------------------------------------------------------- // application:didFinishLaunchingWithOptions: // ------------------------------------------------------------------------------- - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //实例化联网请求 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:TopPaidAppsFeed]]; // create an session data task to obtain and the XML feed // 使用9.0以后的联网操作类,之前可能使用NSURLConnection NSURLSessionDataTask *sessionTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // 获取联网状态代码 200 ,300,400,500等 // in case we want to know the response status code //NSInteger HTTPStatusCode = [(NSHTTPURLResponse *)response statusCode]; if (error != nil)//如果有错误 { [[NSOperationQueue mainQueue] addOperationWithBlock: ^{ [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; if ([error code] == NSURLErrorAppTransportSecurityRequiresSecureConnection) { // if you get error NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022), // then your Info.plist has not been properly configured to match the target server. //错误码含意查询:https://www.meiwen.com.cn/subject/jjjdnttx.html //在工程的 info.plist 文件中添加 https允许 abort(); } else { //其它错误交给handleError处理 [self handleError:error]; } }]; } else { //没错误往下走 // create the queue to run our ParseOperation 初始化解析队列 self.queue = [[NSOperationQueue alloc] init]; // create an ParseOperation (NSOperation subclass) to parse the RSS feed data so that the UI is not blocked 初始化解析操作类 _parser = [[ParseOperation alloc] initWithData:data]; //__weak 弱引用,这里使用弱引用,防止线程引用对象造成内存泄漏。要回收LazyTableAppDelegate?真如此,程序已经结束。没必要了。 __weak LazyTableAppDelegate *weakSelf = self; //添加解析错误时回调的block ,block 类似java interface 或者理解为内部类。好比java OnClickLister. self.parser.errorHandler = ^(NSError *parseError) { //dispatch_async GCD 方式在主线程上操作的方法。相关学习:线程如何操作主线程的3种方法. dispatch_async(dispatch_get_main_queue(), ^{ [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; //扔给本类的handleError方法处理。 [weakSelf handleError:parseError]; }); }; // referencing parser from within its completionBlock would create a retain cycle __weak ParseOperation *weakParser = self.parser; //解析完成返回处理 self.parser.completionBlock = ^(void) { // The completion block may execute on any thread. Because operations // involving the UI are about to be performed, make sure they execute on the main thread. //结果需要在主线程更新 dispatch_async(dispatch_get_main_queue(), ^{ [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; if (weakParser.appRecordList != nil) { //如果返回的解析集合不为空 RootViewController *rootViewController = (RootViewController *)[(UINavigationController *)weakSelf.window.rootViewController topViewController]; //RootViewController : UITableViewController 解析数据给到tableview rootViewController.entries = weakParser.appRecordList; // tell our table view to reload its data, now that parsing has completed 更新tableview [rootViewController.tableView reloadData]; } }); // we are finished with the queue and our ParseOperation weakSelf.queue = nil; }; [self.queue addOperation:self.parser]; // this will start the "ParseOperation" } }]; [sessionTask resume]; // show in the status bar that network activity is starting [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; return YES; } // ------------------------------------------------------------------------------- // handleError:error // Reports any error with an alert which was received from connection or loading failures. // ------------------------------------------------------------------------------- - (void)handleError:(NSError *)error { NSString *errorMessage = [error localizedDescription]; // alert user that our current record was deleted, and then we leave this view controller // UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Cannot Show Top Paid Apps", @"") message:errorMessage preferredStyle:UIAlertControllerStyleActionSheet]; UIAlertAction *OKAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // dissmissal of alert completed }]; [alert addAction:OKAction]; [self.window.rootViewController presentViewController:alert animated:YES completion:nil]; } @end
/* Copyright (C) 2017 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Controller for the main table view of the LazyTable sample. This table view controller works off the AppDelege's data model. produce a three-stage lazy load: 1. No data (i.e. an empty table) 2. Text-only data from the model's RSS feed 3. Images loaded over the network asynchronously This process allows for asynchronous loading of the table to keep the UI responsive. Stage 3 is managed by the AppRecord corresponding to each row/cell. Images are scaled to the desired height. If rapid scrolling is in progress, downloads do not begin until scrolling has ended. */ #import "RootViewController.h" #import "AppRecord.h" #import "IconDownloader.h" #define kCustomRowCount 7 static NSString *CellIdentifier = @"LazyTableCell"; static NSString *PlaceholderCellIdentifier = @"PlaceholderCell"; #pragma mark - @interface RootViewController ()// the set of IconDownloader objects for each app @property (nonatomic, strong) NSMutableDictionary *imageDownloadsInProgress; @end #pragma mark - @implementation RootViewController // ------------------------------------------------------------------------------- // viewDidLoad // ------------------------------------------------------------------------------- - (void)viewDidLoad { [super viewDidLoad]; _imageDownloadsInProgress = [NSMutableDictionary dictionary]; } // ------------------------------------------------------------------------------- // terminateAllDownloads // ------------------------------------------------------------------------------- - (void)terminateAllDownloads { //停止下载 // terminate all pending download connections NSArray *allDownloads = [self.imageDownloadsInProgress allValues]; //数组的makeObjectsPerformSelector:SEL方法来减少自己写循环代码.让数组中每个对象都执行 cancelDownload 方法 [allDownloads makeObjectsPerformSelector:@selector(cancelDownload)]; //从字典移除记录。 [self.imageDownloadsInProgress removeAllObjects]; } // ------------------------------------------------------------------------------- // dealloc // If this view controller is going away, we need to cancel all outstanding downloads. // ------------------------------------------------------------------------------- - (void)dealloc { // terminate all pending download connections [self terminateAllDownloads]; } // ------------------------------------------------------------------------------- // didReceiveMemoryWarning // ------------------------------------------------------------------------------- - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // terminate all pending download connections [self terminateAllDownloads]; } #pragma mark - UITableViewDataSource // ------------------------------------------------------------------------------- // tableView:numberOfRowsInSection: // Customize the number of rows in the table view. // ------------------------------------------------------------------------------- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSUInteger count = self.entries.count; // if there's no data yet, return enough rows to fill the screen if (count == 0) { return kCustomRowCount; } return count; } // ------------------------------------------------------------------------------- // tableView:cellForRowAtIndexPath: // ------------------------------------------------------------------------------- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = nil; NSUInteger nodeCount = self.entries.count; if (nodeCount == 0 && indexPath.row == 0) { // add a placeholder cell while waiting on table data 在storyboard中定义的加载中... cell = [tableView dequeueReusableCellWithIdentifier:PlaceholderCellIdentifier forIndexPath:indexPath]; } else { cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; // Leave cells empty if there's no data yet if (nodeCount > 0) { // Set up the cell representing the app AppRecord *appRecord = (self.entries)[indexPath.row]; cell.textLabel.text = appRecord.appName; cell.detailTextLabel.text = appRecord.artist; // Only load cached images; defer new downloads until scrolling ends //这里注释得很明白,只加载cached(已缓存的)图片。 //【defer】 英[dɪˈfɜː(r)] ,美[dɪˈfɜːr] ,v.推迟; 延缓; 展期; if (!appRecord.appIcon)//如果为空 { if (self.tableView.dragging == NO && self.tableView.decelerating == NO) { //如果没有拖动,也没在惯性滑动。开始下载。 [self startIconDownload:appRecord forIndexPath:indexPath]; } // if a download is deferred or in progress, return a placeholder image //如果正在下载中给一个默认图。 cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"]; } else { // (self.entries)[indexPath.row]; 直接使用 cell.imageView.image = appRecord.appIcon; } } } return cell; } #pragma mark - Table cell image support // ------------------------------------------------------------------------------- // startIconDownload:forIndexPath: 第N个Cell的下载图片资源 // ------------------------------------------------------------------------------- - (void)startIconDownload:(AppRecord *)appRecord forIndexPath:(NSIndexPath *)indexPath { IconDownloader *iconDownloader = (self.imageDownloadsInProgress)[indexPath]; if (iconDownloader == nil) { iconDownloader = [[IconDownloader alloc] init]; iconDownloader.appRecord = appRecord; [iconDownloader setCompletionHandler:^{ //图片加载完成时回调本段代码 UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; // Display the newly loaded image cell.imageView.image = appRecord.appIcon; // Remove the IconDownloader from the in progress list. // This will result in it being deallocated. 从字典中把下载对象移除。之前是有记录要下载的。 [self.imageDownloadsInProgress removeObjectForKey:indexPath]; }]; //在开始下载后,把下载对象记录到字典管理。 (self.imageDownloadsInProgress)[indexPath] = iconDownloader; [iconDownloader startDownload]; } } // ------------------------------------------------------------------------------- // loadImagesForOnscreenRows // This method is used in case the user scrolled into a set of cells that don't // have their app icons yet. // ------------------------------------------------------------------------------- - (void)loadImagesForOnscreenRows { if (self.entries.count > 0) { //获取tableview 当前可见的编号。indexPathsForVisibleRows NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows]; for (NSIndexPath *indexPath in visiblePaths) { AppRecord *appRecord = (self.entries)[indexPath.row]; if (!appRecord.appIcon) // Avoid the app icon download if the app already has an icon // 如果没有图片 开始下载图片 { [self startIconDownload:appRecord forIndexPath:indexPath]; } } } } #pragma mark - UIScrollViewDelegate // ------------------------------------------------------------------------------- // scrollViewDidEndDragging:willDecelerate: // Load images for all onscreen rows when scrolling is finished. // tableview 滚动结束事件回调。停下来时,加载当前屏的图片。 // ------------------------------------------------------------------------------- - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (!decelerate) { [self loadImagesForOnscreenRows]; } } // ------------------------------------------------------------------------------- // scrollViewDidEndDecelerating:scrollView // When scrolling stops, proceed to load the app icons that are on screen. // ------------------------------------------------------------------------------- - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { //惯性滑动结束时,开始加载图片,同上。 [self loadImagesForOnscreenRows]; } @end
/* Copyright (C) 2017 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Helper object for managing the downloading of a particular app's icon. It uses NSURLSession/NSURLSessionDataTask to download the app's icon in the background if it does not yet exist and works in conjunction with the RootViewController to manage which apps need their icon. */ #import "IconDownloader.h" #import "AppRecord.h" #define kAppIconSize 48 @interface IconDownloader () @property (nonatomic, strong) NSURLSessionDataTask *sessionTask; @end #pragma mark - @implementation IconDownloader // ------------------------------------------------------------------------------- // startDownload // ------------------------------------------------------------------------------- - (void)startDownload { NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.appRecord.imageURLString]]; // create an session data task to obtain and download the app icon _sessionTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // 这里使用NSURLSessionDataTask 进行数据加载。@NSURLSession @NSURLSessionDataTask 属一个知识体系,可以系统学习。 //_sessionTask 声明为了属性,目的是可以进行控制。 // in case we want to know the response status code //NSInteger HTTPStatusCode = [(NSHTTPURLResponse *)response statusCode]; if (error != nil) { if ([error code] == NSURLErrorAppTransportSecurityRequiresSecureConnection) { // if you get error NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022), // then your Info.plist has not been properly configured to match the target server. // abort(); } } //以上代码好像是规范格式。 [[NSOperationQueue mainQueue] addOperationWithBlock: ^{ // Set appIcon and clear temporary data/image UIImage *image = [[UIImage alloc] initWithData:data]; if (image.size.width != kAppIconSize || image.size.height != kAppIconSize) { //对图片进行裁剪操作。 CGSize itemSize = CGSizeMake(kAppIconSize, kAppIconSize); UIGraphicsBeginImageContextWithOptions(itemSize, NO, 0.0f); CGRect imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height); [image drawInRect:imageRect]; self.appRecord.appIcon = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } else { self.appRecord.appIcon = image; } //不如何image给到了appRecord数据模型。 // call our completion handler to tell our client that our icon is ready for display //通知加载完成 if (self.completionHandler != nil) { self.completionHandler(); } }]; }]; //开启task [self.sessionTask resume]; } // ------------------------------------------------------------------------------- // cancelDownload // ------------------------------------------------------------------------------- - (void)cancelDownload { [self.sessionTask cancel]; _sessionTask = nil; } @end
代码比较简单
1.appdelegate 去下载数据并解析。并更新tableview
//RootViewController : UITableViewController 解析数据给到tableview
rootViewController.entries = weakParser.appRecordList;
2.使用 model给tableview展示。
@property (nonatomic, strong) NSString *appName;
@property (nonatomic, strong) UIImage *appIcon;
@property (nonatomic, strong) NSString *artist;
@property (nonatomic, strong) NSString *imageURLString;
@property (nonatomic, strong) NSString *appURLString;
appIcon开始时为空,当展示在屏幕时,起线程去加载。并记录在
self.imageDownloadsInProgress
第一个下载对应一个线程对象。
3.这个代码 UIImage 会不数增加,数量够多,内存肯定溢出。
所以需要图片的加载策略。图片的三级缓存可系统独立学习。