[Swift]在不依赖三方库的情况下如何异步下载和缓存图片?

在可滚动视图(如UITableView)中异步加载大量图片是一个很常见的任务。 然而,在图片正在下载的同时又要保持应用程序流畅滚动,可能有点挑战。

许多开发人员依靠像Alamofire和SDWebImage这样的库来避免背景图像加载最终的麻烦和缓存管理的麻烦。 但是,如果您想自己用纯代码而不是依赖第三方库, 那该怎么写呢?

嗯,一下就是我所要讲的:

在这篇文章中,您将使用您的技能构建一个流畅的应用程序,从iTunes API中提取游戏标题和图标的列表。 一路上,您将了解GCD(Grand Central Dispatch)和NSCache是如何协同工作以管理网络图片并生成一个整洁的缓存管理系统。

好,让我们开始吧:

打开Xcode,从菜单中选择“File \ New \ Project”,选择Single View应用程序模板并将项目命名为“ImagesDownloadAndCache”。


创建UI

您即将构建的应用程序包含单个界面,带有刷新控件的表视图,以加载内容。

Xcode storyboard文件带有一个默认的UIViewController,让我们删除它并替换为一个UITableViewController,默认情况下UITableViewController有一个刷新控件属性。

从项目导航器视图中选择Main.storyboard,将UITableViewController对象从对象库拖动到画布。 接下来,选择初始的View Controller,然后从键盘中点击delete。

注意:选择一个iPhone模型,这里我选择了iPhone 6s模型。


从项目导航器视图中选择ViewController.swift并更改类声明,使其成为一个UITableViewController子类。

class ViewController: UITableViewController {

切换回storyboard,选中TableViewController,然后在Identity inspector 中将类设置为ViewController。


然后我们设置好 identifier


界面搭建好了,我们可以开始编码了

首先,我们做好属性声明

var refreshCtrl: UIRefreshControl!
var tableData:[AnyObject]!
var task: URLSessionDownloadTask!
var session: URLSession!
var cache:NSCache<AnyObject, AnyObject>!

数据将使用 URLSessionDownloadTask 类下载,这就解释了为什么你声明了上面的 task 和 session 属性。 此外,tableData 属性将用作表视图数据源,缓存变量是缓存字典的参考,APP将在下载图片之前使用缓存字典请求缓存中的图片(如果存在)。

接下来,找到viewDidLoad方法,并在super.viewDidLoad调用之后执行以下代码:

session = URLSession.shared
task = URLSessionDownloadTask()
        
self.refreshCtrl = UIRefreshControl()
self.refreshCtrl.addTarget(self, action: #selector(ViewController.refreshTableView), for: .valueChanged)
self.refreshControl = self.refreshCtrl
        
self.tableData = []
self.cache = NSCache()

上面的代码将初始化 session、task,tableData,以及 cache 缓存对象。 还添加了一个刷新的调用方法,以便在每次提取表视图时运行。 让我们继续实现“refreshTableView”选择器。

func refreshTableView(){
        
let url:URL! = URL(string: "https://itunes.apple.com/search?term=flappy&entity=software")
task = session.downloadTask(with: url, completionHandler: { (location: URL?, response: URLResponse?, error: Error?) -> Void in
            
       if location != nil{
            let data:Data! = try? Data(contentsOf: location!)
            do{
                let dic = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as AnyObject
                self.tableData = dic.value(forKey : "results") as? [AnyObject]
                DispatchQueue.main.async(execute: { () -> Void in
                    self.tableView.reloadData()
                    self.refreshControl?.endRefreshing()
                })
            }catch{
                print("something went wrong, try again")
            }
       }
})
  task.resume()
}

基本上,上述方法将异步请求iTunes搜索API返回其标题或描述。 一旦接收到数据,tableData 数据源数组就被记录填充,并且表视图会从主线程重新加载。

然后,实现两个强制性数据源协议方法来填充表视图行中下载的数据,特别是图片数据。

将以下代码复制到类中:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tableData.count
}    
    
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
  // 1
  let cell = tableView.dequeueReusableCell(withIdentifier: "GameCell", for: indexPath)
  let dictionary = self.tableData[(indexPath as NSIndexPath).row] as! [String:AnyObject]
  cell.textLabel!.text = dictionary["trackName"] as? String
  cell.imageView?.image = UIImage(named: "placeholder")
          
  if (self.cache.object(forKey: (indexPath as NSIndexPath).row as AnyObject) != nil){
      // 2
      // Use cache
      print("Cached image used, no need to download it")
      cell.imageView?.image = self.cache.object(forKey: (indexPath as NSIndexPath).row as AnyObject) as? UIImage
  } else {
      // 3
      let artworkUrl = dictionary["artworkUrl100"] as! String
      let url:URL! = URL(string: artworkUrl)
      task = session.downloadTask(with: url, completionHandler: { (location, response, error) -> Void in
          if let data = try? Data(contentsOf: url){
              // 4
              DispatchQueue.main.async(execute: { () -> Void in
                  // 5
                  // Before we assign the image, check whether the current cell is visible
                  if let updateCell = tableView.cellForRow(at: indexPath) {
                        let img:UIImage! = UIImage(data: data)
                        updateCell.imageView?.image = img
                        self.cache.setObject(img, forKey: (indexPath as NSIndexPath).row as AnyObject)
                  }
              })
          }
      })
      task.resume()
  }
  return cell
}

刚刚实现的应用程序的主要部分,让我分解上面的代码,以便更好地了解:

1:这里,tableView 将使 cell 出列以便重用。 如果没有分配 cell,则 tableVeiw 将分配,调整大小并返回一个新的 cell。 接下来,在字典对象中提取数据源数组中的当前缓存记录。 游戏标题被设置为 cell 的文本标签,并且 cell 被临时分配给占位图片,同时等待其下载。

2:cache 是一个类似集合的容器,非常类似于 NSDictionary 实例。 这里你使用它作为 UIImage 对象的集合,其中关键是行索引(这是非常重要的,以便跟踪对应于每个 cell 的正确的缓存图片)。 所以基本上,你首先检查是否有当前图片的缓存副本。 如果副本已经存在,则将其加载到 cell 中。
3:如果没有给定的缓存副本,那么就会从服务器去异步下载它。
4:假设图片下载成功,将切换到主线程,以更新视图。 这很重要,因为所有 UI 任务都应该在主线程中执行,而不是在后台线程中执行。
5:这是棘手的部分,在更新图片之前检查 cell 是否在屏幕上可见。 否则,图片将在滚动时在每个 cell 上重复使用。 如果相关 cell 是可见的,那么只需将图片分配给 cell,并将其添加到缓存中以供以后使用。

此外,禁用 APP 的ATS(应用程序传输安全),以便能够执行网络操作。 要禁用ATS,请从项目导航器视图中选择Info.plist文件,并将其更改为以下内容:


更多开源,尽在 https://github.com/CNKCQ

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,623评论 4 59
  • Swift版本点击这里欢迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh阅读 24,875评论 7 249
  • 最近重读《红楼梦》的同时在看老师推荐的《大连接》,一本关于社会网络是如何形成的以及对人类现实行为的影响,最主要的观...
    笑响点亮了四面wind阅读 307评论 0 1
  • 1955年4月18日凌晨1点15分,76岁的爱因斯坦在普林斯顿一家医院里停止了呼吸。不发讣告,没有仪仗,没有花圈,...
    艾薇塔阅读 1,112评论 2 10
  • 周晨(大发化纤有限公司) 276期,感谢1组成员 【日精进打卡第14天】 【知~学习】 《六项精进》读1遍 共60...
    周晨i阅读 517评论 0 0