Swiftの有名画像キャッシュライブラリを比較してみた

# 前置き
サーバーから受け取った画像のパスを、``String型`` → ``URL型``に変換して
``TableView``なり``CollectionView``に表示するっていうことは結構多々あると思います。

今まで自作アプリを作る時になんとなく実装していた(``AlamofireImage``を使っていた)のが、いざ仕事で実装しようと思った時に、ライブラリがいろいろあるので、オーソドックスなやつを一通り触ってみました、というのが今回のきっかけ。

(追記 2017/8/1)
PINRemoteImageを比較対象に追加しました。

# 比較対象
以下の6つを用意した。
1. URLSession(iosのデフォルト）
2. [AlamofireImage](https://github.com/Alamofire/AlamofireImage)
3. [Nuke](https://github.com/kean/Nuke)
4. [SDWebImage](https://github.com/rs/SDWebImage)
5. [Kingfisher](https://github.com/onevcat/Kingfisher)
6. [PINRemoteImage](https://github.com/pinterest/PINRemoteImage)


選定基準は

- **Pods/Carthageに対応**していること
- 最終更新が**３ヶ月以内**であること(2017/7/26時点)
- 知名度（githubにそれなりのスター数があるとか）

## 前提条件
今回はすべての比較を<font color="Red">**画像の取得機能**</font>の一点において比較します。
アップロード、ダウンロード等ではまた変わってくるかと思うのでご了承ください。

## 比較1 〈機能〉
### 目的
- 用途に合わせた使い方でライブラリを選びたい

### 結果

以下にまとめてみた。（共通している機能は一部省いています。違ったらご指摘お願いします。）

||①URLSession|②AlamofireImage|③Nuke|④SDWebImage|⑤Kingfisher|⑥PINRemoteImage|
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|Placeholder|-|○|△|○|○|○|
|Image<br>Transform|-|○|○|○|○|○|
|Animation|-|○|○|△|○|○|
|Prefetch|-|-|△|△|○|○|

Placeholder(プレースホルダー)： 画像読み込み中に出す画像の機能（インジケーターとはまた違います）
ImageTransform(画像加工): 取得した画像を表示するまでに編集できる機能（例 サイズ、まる角など）
Animation(アニメーション)： 画像出現の際にアニメーションをつける機能
Prefetch(プリフェッチ)： 表示する画像をあらかじめ読み込んでおく機能

### 考察

- ``SDWebImage``と``Kingfisher``の機能はだいたい同じ
- ``Nuke``は[Preheat](https://github.com/kean/Preheat)で拡張すること、``PINRemoteImage``はデフォルトで**プリフェッチ機能**が使える。
- ``AlamofireImage``は画像出現アニメーションが一番充実している。自分で作らなくてもデフォルトでいくつか用意されている。オプションで自作アニメーションも設定可能。(以下、用意されたアニメーション)

<table>
 <tr>
  <th>crossDissolve</th>
  <th>&nbsp;&nbsp;&nbsp;&nbsp;curlUp&nbsp;&nbsp;&nbsp;&nbsp;</th>
  <th>&nbsp;&nbsp;curlDown&nbsp;&nbsp;</th>
  <th>&nbsp;flipFromTop&nbsp;</th>
  <th>flipFromBottom</th>
  <th>&nbsp;flipFromLeft&nbsp;</th>
  <th>&nbsp;flipFromRight&nbsp;</th>
 </tr>
 <tr>
  <td>
   <img src="https://qiita-image-store.s3.amazonaws.com/0/175242/4e4cd385-8944-a10a-4c2d-396972caff92.gif" width="100">
  </td>
  <td>
   <img src="https://qiita-image-store.s3.amazonaws.com/0/175242/56a78534-a449-42b3-0c44-735faaa0dc52.gif" width="100">
  </td>
  <td>
   <img src="https://qiita-image-store.s3.amazonaws.com/0/175242/d09c2060-d04e-9883-02b9-f2dd7bc1c438.gif" width="100">
  </td>
  <td>
   <img src="https://qiita-image-store.s3.amazonaws.com/0/175242/f31c860b-418c-e90b-f57d-f78546cb5459.gif" width="100">
  </td>
  <td>
   <img src="https://qiita-image-store.s3.amazonaws.com/0/175242/bca757f5-2292-b5a9-5329-2a2a6b7c9631.gif" width="100">
  </td>
  <td>
   <img src="https://qiita-image-store.s3.amazonaws.com/0/175242/6505f004-0119-2d7a-7564-3bb6f87034a2.gif" width="100">
  </td>
  <td>
   <img src="https://qiita-image-store.s3.amazonaws.com/0/175242/981c083c-3b56-06aa-6546-132089c05622.gif" width="128">
  </td>
 </tr>
</table>


## 比較2 〈書き方〉
### 目的
「書きやすさ」=「導入のしやすさ」=「**可読性**」に繋がると考え比較してみた。（個人的な主観です）

### 内容
比較しやすいように、できるだけ画像の取得方法を同じように記述しました。
（``UIImageView``を拡張した``Extension``で書いた。（指摘あればお願いします））

※今回は画像が取得できる前提のためエラーハンドリングは何も行っていません。
 また書き方統一のためにエラーも１つに集約された形をとっています。（Requestを生成しないのもそのため）
 それぞれレスポンスの型が違うため、結果の判定部分の書き方が同じにならない点にご了承ください。


#### ①URLSession(iosのデフォルト）

```swift
extension UIImageView {

    func setImageByDefault(with url: URL) {

        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            // Success
            if error == nil, case .some(let result) = data, let image = UIImage(data: result) {
                self?.image = image

            // Failure
            } else {
                // error handling

            }
        }.resume()
    }

}
```
ただ通信を行っているだけなので、レスポンスを``UIImage(data:)``でUIImageに変換しないといけない。

#### ②AlamofireImage

```swift
import AlamofireImage

extension UIImageView {

    func setImageByAlamofire(with url: URL) {

        af_setImage(withURL: url) { [weak self] response in
            switch response.result {
            case .success(let image):
                self?.image = image
                
            case .failure(_):
                // error handling
                break
            }
        
        }
    }
}
```
他のライブラリと違い、レスポンスがタプルではなく``Success``/``Failure``で分岐できる。

#### ③Nuke

```swift
import Nuke

extension UIImageView {

    func setImageByNuke(with url: URL) {
        
        Nuke.Manager.shared.loadImage(with: url, into: self) { [weak self] response, isResponse in
            // Success
            if isResponse, case .success(let image) = response {
                self?.image = image

            // Failure
            } else {
                // error handling
            }
            
        }
    }
}
```
引数が``URL``型とは別に``into: UIImageView``を指定する必要があるのが特徴。
（ただし、上記のようにExtensionなりでラップしてしまえば関係ないっちゃない）


#### ④SDWebImage

```swift
import SDWebImage

extension UIImageView {

    func setImageBySDWebImage(with url: URL) {
        
        self.sd_setImage(with: url) { [weak self] image, error, _, _ in
            // Success
            if error == nil, let image = image {
                self?.image = image
                
            // Failure
            } else {
                // error handling
                
            }
        }
        
    }
}
```
ベースの書き方は``URLSession``と同じ。``UIImage``に変換する手間がない。

#### ⑤Kingfisher

```swift
import Kingfisher

extension UIImageView {

    func setImageByKingfisher(with url: URL) {

        self.kf.setImage(with: url) { [weak self] image, error, _, _ in
            // Success
            if error == nil, let image = image {
                self?.image = image
                
            // Failure
            } else {
                // error handling
            }
        }
        
    }
}
```
``SDWebImage``を元に書かれた言われてるだけあって、まんま同じに書けます笑
書き方の``.kf``っていうのがちょっと``Rx``っぽい。

#### ⑥PINRemoteImage

```swift
import PINRemoteImage
import PINCache

extension UIImageView {

    func setImageByPINRemoteImage(with url: URL) {
        
        self.pin_setImage(from: url) { [weak self] result in
            // Success
            if result.error == nil, let image = result.image {
                self?.image = image

            // Failure
            } else {
              // error handling
            }
            
        }
    }
}
```
戻り値は``PINRemoteImageManagerResult``の１つだが、結局中身を取り出して分岐する必要がある。
今回は、``error``の``nil``チェックをしていますが、公式リファレンスを見ると`` let image = result.image``しかしてないのでなくても良いかと。
別途、<font color="Red">PINCacheをimportしないとキャッシュが働きません</font>でした。

### 考察
- 書き方を寄せてるとはいえ、基本的にどのライブラリも同じように書けるとわかった。

- ``AlamofireImage``が``Result<T,Error>``型のレスポンスのため、``Success``/``Failure``で簡潔に書ける印象。
他の４つは、レスポンスがタプルで返され、エラーが存在するか（``nil``か）を先に判定する部分をワンクッション挟む必要があるため、若干冗長になってしまう印象。

（今回はプレースホルダーやリクエストなどのカスタマイズを一切行っていない書き方のため、偏っている可能性があるのはご了承ください。）


## 比較3 〈速度〉
### 目的
- **ライブラリの処理速度**を知りたい
- 特に**キャッシュの性能**で速度が変わるのかを知りたい

### 環境
- シュミレータ iOS10 iPhoneSE

### 検証内容
1. 画像を表示する際、初期表示の画像取得速度
2. 画像を表示する際、一度表示したものを再表示する時の画像取得速度

今回は``CollectionView``に表示する想定で行っています。

#### 補足
- ２に関しては一度画面をスクロールしきってすべての画像が表示されてから、再度戻す場合に表示される**画像のキャッシュ機能で、どの程度画像の取得速度が変わるのか**を測定する。

- 計測する部分は「画像取得から、成功した時のコールバック時」までの時間を計測しているので、``CollectionView``の動作には左右されないようになっている。

- 画像は事前に複数のURLセットした配列を用意している。

### 結果
計150枚分の画像取得を計測した
→初回表示では15枚表示されるようにコレクションを作成したので、それを10回行って平均をとるため

画像１枚の取得平均

|平均|①URLSession|②AlamofireImage|&nbsp;&nbsp;③Nuke&nbsp;&nbsp;|④SDWebImage|⑤Kingfisher|⑥PINRemoteImage|
|:--|:--:|:--:|:--:|:--:|:--:|:--:|
|初期表示|12.9 ms|<font color="blue">51.2 ms</font>|<font color="Red">0.975 ms</font>|23.7 ms|20.6 ms|36.0 ms|
|再表示|1.2 ms(1 ms)|<font color="Gold">0.273 ms</font>|<font color="Gold">0.130 ms</font>|<font color="Gold">0.146 ms</font>|<font color="Gold">0.180 ms</font>|<font color="Gold">0.134 ms</font>|

上記の結果から、初期表示に関しては、かなりばらつきが見られたが、再表示する際はキャッシュが働きかなり<font color="Red">**高速化されている**</font>ことがわかる。


#### 考察
- 結果として、**Nuke**が画像表示において一番早いことがわかった。
- 一見差があるように見えて、単位が``ms(1/1000秒)``なので、**ユーザーが不快に感じるような差は出ない**ように思える。ただ、画像取得と絡む形で、大量にデータがある場合や１画面で多くのAPIを叩いている場合は、その差が如実に出てくるかもしれないが、、。


# 後書き
**Nuke**が早い上にプリフェッチ機能もあるので、すばらしいなぁとは思いつつも、、、

現状、本家ライブラリにも記載はあるが、iOS10から``UITableViewDataSourcePrefetching``,``UICollectionViewDataSourcePrefetching``
が出たので、わざわざライブラリで実装する必要はないかもと思ったり。笑
導入のしやすさで行けば、**AlamofireImage**かと思います。

なんども記載していますが、今回は本当に一番簡単な方法で比較したので、素のライブラリを単純に比較しただけです。実際はリクエスト、キャッシュ部分のカスタマイズなど行ったりすることで、性能の差が出てくるんだろうなあ、と。

ご指摘あれば、ぜひ編集リクエストお願いいたします。m(_ _  )m

(追記 2017/8/1)
コメントで指摘され``PINRemoteImage``を調べてみましたが、機能も揃っており早い。
なにより**Pinterest**より提供されているのでかなり信頼度も高いかと！

# ライブラリのあれこれ
- AlamofireImage

[iOSの画像をキャッシュする方法とAlamofireImageの解説](http://qiita.com/Masaaki/items/231b1f6226c807a658bf)
[Swift製のURL画像取得ライブラリAlamofireImageを使って商品一覧画面を作成してみた](http://dev.classmethod.jp/smartphone/swift-alamofireimage/)

- Nuke

[What is… Nuke?](https://www.raywenderlich.com/118361/what-is-nuke) (コードは古い)
[初めてOSSを読んでみた(Nuke)](http://sssslide.com/speakerdeck.com/takattata/chu-meteosswodu-ndemita-nuke)

- SDWebImage

[SDWebImageでURLから画像を取得する](http://programming-beginner-memo.com/?p=164)
[SwiftでSDWebImageを使ってダンロードした画像を加工してUIImageViewに表示する](http://blog.ch3cooh.jp/entry/20160115/1452819720)

- Kingfisher

[SDWebImageライクなライブラリ、Kingfisherを使ってみる](http://llcc.hatenablog.com/entry/2016/12/26/231642)
[[Swift]KingFisherで画像URLから画像データをダウンロード](http://qiita.com/dondoko-susumu/items/a6f51ebef45e819cbf9d)

- PINRemoteImage

[SDWebImage と PINRemoteImage の速さを比較してみた](http://qiita.com/mironal/items/aab090a73bf6f14f0e50)

- その他

[[iOS 10] UICollectionViewにおけるセルのライフサイクルの変更とPre-Fetching APIによるスムーズなスクロールについて](http://dev.classmethod.jp/smartphone/ios-10-uicollectionview-pre-fetching/)