UITableView介绍 之 网络图片数据加载
UITableView网络图片数据加载
通常我们看到的tableView几乎都是图文混排只展示文字的特别少,tableView下载图片和文字的过程还是比较复杂的,下面请听我一一道来。
搭建简单的web后台
要从web获取数据这里简单用Node.js搭建一个后,有兴趣的可以跟着做一下,只是为了演示,所以后台的业务逻辑也比较简单,废话不多说直接上代码:
const PORT = 8000;
const http = require('http');
const url = require('url');
const fs = require('fs');
const mime = require('./mime').types;
const path = require('path');
const dataJson = require('./dataJson').dataJson;
const querystring = require('querystring');
// 开启一个简单web服务器
var server = http.createServer((req, resp) => {
var murl = url.parse(req.url);
var pathName = murl.pathname;
if (pathName.indexOf('webServer')) {
// 进行简单的路由
route(murl, resp);
} else {
resp.end(errorMessage());
}
}).listen(PORT);
console.log('server running ....');
function route (murl, resp) {
var pathName = murl.pathname;
// 返回数据处理
if (pathName == '/webServer' || pathName == '/webServer/') {
reJsonMessage(resp);
} else { // 返回图片
var pArr = pathName.split('/webServer');
var realPath = 'assert' + pArr[pArr.length - 1];
fs.exists(realPath, (exists) => {
if (exists) {
readFile(realPath, resp);
} else {
resp.end(errorMessage());
}
});
}
}
// 根据不同图片的mime选择性返回
function readFile(filePath, resp) {
fs.readFile(filePath, (err, data) => {
if (err) {
resp.end(errorMessage());
} else {
var ext = path.extname(filePath);
ext = ext ? ext.slice(1) : 'unknown';
var contentType = mime[ext] || 'text/plain';
resp.writeHead(200, {'Content-Type': contentType});
resp.end(data);
}
});
}
function errorMessage () {
var redata = {};
redata.flag = 'fail';
redata.value = 'access error!';
return JSON.stringify(redata);
}
function reJsonMessage (resp) {
resp.writeHead(200, {'Content-Type': mime.json + ';charset=utf-8'});
var redata = {};
redata.flag = 'success';
redata.value = dataJson;
resp.end(JSON.stringify(redata), 'utf8');
}
完整代码的下载路径 使用方法很简单在控制台运行 node index.js 即可,前提是你的机器已安装最新版 node,注意:dataJson.js中的ip改成和你机器相同,访问服务器的地址:http://localhost:8000/webServer 即可拿到返回的json数据格式如下:
{
"flag": "success",
"value": [
{
"detail": "高德是中国领先的数字地图内容、导航和位置服务解决方案提供商。",
"title": "高德地图",
"url": "http://192.168.31.167:8000/webServer/autonavi_minimap_72px_1127483_easyicon.net.png"
},
{
"detail": "百度是全球最大的中文搜索引擎、最大的中文网站。",
"title": "百度",
"url": "http://192.168.31.167:8000/webServer/bdmobile_android_app_72px_1127470_easyicon.net.png"
},
.....
]
}
到此准备工作已经完成,下面开始撸ios代码。
加载网络图片
简单粗暴的获取数据
//
// ViewController.m
// UITableViewDemo
//
// Created by code_xq on 16/3/3.
// Copyright © 2016年 code_xq. All rights reserved.
//
#import "ViewController.h"
#import "AFNetworking.h"
@interface DataModel : NSObject
/**描述信息*/
@property (nonatomic, copy) NSString *detail;
/**图片地址*/
@property (nonatomic, copy) NSString *url;
/**标题*/
@property (nonatomic, copy) NSString *title;
/**数据模型转换方法*/
+ (instancetype)initWithDict:(NSDictionary *)dict;
@end
@implementation DataModel
+ (instancetype)initWithDict:(NSDictionary *)dict {
DataModel *dm = [[self alloc] init];
[dm setValuesForKeysWithDictionary:dict];
return dm;
}
@end
@interface ViewController ()<UITableViewDataSource, UITableViewDelegate>
/**数据源*/
@property (nonatomic, strong) NSMutableArray *dataSource;
/**table引用*/
@property (nonatomic, weak) UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:tableView];
tableView.dataSource = self;
tableView.delegate = self;
self.tableView = tableView;
// 请求数据
[self getDataFromWeb];
}
/**
* 请求网络数据
*/
- (void)getDataFromWeb {
// 为了防止block中产生循环引用
__weak typeof (self)weakSelf = self;
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"http:192.168.31.167:8000/webServer"
parameters:nil progress:nil
success:^(NSURLSessionTask *task, id responseObject) {
NSDictionary *dict = responseObject;
if([@"success" isEqualToString:dict[@"flag"]]) {
NSArray *temp = dict[@"value"];
for (NSDictionary *d in temp) {
DataModel *dm = [DataModel initWithDict:d];
[weakSelf.dataSource addObject:dm];
}
[weakSelf.tableView reloadData];
}
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(@"Error: %@", error);
}];
}
#pragma mark - 代理方法<UITableViewDataSource>
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataSource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 定义static变量
static NSString *const ID = @"cell";
// 先从缓存中找,如果没有再创建
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}
DataModel *dm = self.dataSource[indexPath.row];
cell.textLabel.text = dm.title;
cell.detailTextLabel.text = dm.detail;
// 图片下载处理
[self downLoadCellPic:cell data:dm];
return cell;
}
/**
* 下载图片
*/
- (void)downLoadCellPic:(UITableViewCell *)cell data:(DataModel *)dm {
NSURL *url = [NSURL URLWithString:dm.url];
NSData *imgData = [NSData dataWithContentsOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:imgData];
cell.imageView.image = image;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
return 80;
}
/**
* 数据源懒加载
*/
- (NSMutableArray *)dataSource {
if (_dataSource == nil) {
_dataSource = [NSMutableArray array];
}
return _dataSource;
}
@end
下载图片的代码一眼开上去还行,但是它至少存在这么几个问题:在主线程中下载图片、每次滑动都会下载图片
在子线程中下载图片
上面的下载图片的方法修改为下面的代码:
/**
* 下载图片
*/
- (void)downLoadCellPic:(UITableViewCell *)cell data:(DataModel *)dm {
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:dm.url];
NSData *imgData = [NSData dataWithContentsOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:imgData];
// 跟新界面操作放在主线程中
dispatch_sync(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
}];
[self.queue addOperation:operation];
}
这里我采用了ios中的queue来异步下载图片,但是上面提到的图片重复下载多次的问题依然存在而且在滑动的过程中会创建很多线程严重影响性能
图片仅下载一次
NSBlockOperation *operation = self.queueDicts[dm.url];
if (!operation) {
operation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:dm.url];
NSData *imgData = [NSData dataWithContentsOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:imgData];
// 跟新界面操作放在主线程中
dispatch_sync(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
}];
[self.queue addOperation:operation];
// 用字典缓存下载队列
[self.queueDicts setObject:operation forKey:dm.url];
}
用一个字典作为缓存url作为key来缓存队列,这样可以解决一个url被多次下载的情况,但是这样做又产生了一些其他问题,由于cell是重复利用的所以你往下滑一段后,再滑回去发现原来的每行对应的图片变了,这是因为图片只能下载一次cell重复利用了下面的图片。还有一个问题就是如果有图片下载失败那就没办法解决了,为了解决问题代码还需改进
dispatch_sync(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
// 移除操作
[self.queueDicts removeObjectForKey:dm.url];
});
在下载完成后将缓存的操作移除,但这样做又回到了前面一个url被请求多次的情况,那么代码还需要继续改进,我的思路是再做一个图片缓存
/**
* 下载图片
*/
- (void)downLoadCellPic:(UITableViewCell *)cell data:(DataModel *)dm {
UIImage *img = self.picDicts[dm.url];
if (img) {
cell.imageView.image = img;
} else {
// 添加默认背景图片,避免刚加载时没有预留图片位置
cell.imageView.image = [UIImage imageNamed:@"background_icon"];
NSBlockOperation *operation = self.queueDicts[dm.url];
if (!operation) {
operation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:dm.url];
NSData *imgData = [NSData dataWithContentsOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:imgData];
// 跟新界面操作放在主线程中
dispatch_sync(dispatch_get_main_queue(), ^{
// 将图片放入缓存
if (image) {
[self.picDicts setObject:image forKey:dm.url];
}
cell.imageView.image = image;
// 移除操作
[self.queueDicts removeObjectForKey:dm.url];
});
}];
[self.queue addOperation:operation];
// 用字典缓存下载队列
[self.queueDicts setObject:operation forKey:dm.url];
}
}
}
经过上面的改造基本上可以解决一个url被下载多次的问题,目前测试上面代码一切正常。网络极差的情况下没有测试过。
通过三方框架进行图片加载
上面的代码基本讲清楚了图片加载的这个过程,但是在某些细节方面可能还不那么完美,但是不用担心大名鼎鼎的SDWebImage github地址 已经为我们封装好了图片下载的整个过程,而且使用非常简单:
#import "UIImageView+WebCache.h"
....
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 定义static变量
static NSString *const ID = @"cell";
// 先从缓存中找,如果没有再创建
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}
DataModel *dm = self.dataSource[indexPath.row];
cell.textLabel.text = dm.title;
cell.detailTextLabel.text = dm.detail;
// 图片下载处理
// [self downLoadCellPic:cell data:dm];
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:dm.url]
placeholderImage:[UIImage imageNamed:@"background_icon"]];
return cell;
}
引入头文件,将原来下载的图片的方法改为上面的形式即可,一个方法就搞定,少了很多繁琐的过程,到此为止UITableView加载图片就讲完了。