Twitter Image Pipeline (TIP)
laiso さんが 2017年におけるObjective-Cコミュニティの動向 で Twitter が iOS の Twitter クライアントで使っている画像ダウンロード・キャッシュライブラリを公開したと紹介されていたのでちょっと使ってみました。
- Twitter による紹介: Introducing Twitter Image Pipeline iOS framework for open source
- TIP 以前の実装がいかにヒドかったか書かれていて面白いです。
 
- GitHub: twitter/ios-twitter-image-pipeline: Twitter Image Pipeline is a robust and performant image loading and caching framework for iOS clients
特徴
- Twitter クライアントで使っている!
- Objective-C で作られている。
- 独立した複数のキャッシュを持てる。
- Twitterクライアントは複数のアカウントをサポートするが、ユーザー毎に独立したキャッシュを持ち、ログアウトするときにはそのアカウントのキャッシュのみクリアできる。
 
- キャッシュは 3 階層に分かれている。
- 1 次キャッシュ: メモリにロード済みで、特定の解像度にスケーリング済み。同期アクセス。
- 2 次キャッシュ: メモリにロード済みだが、スケーリングはされていない。デコードも済んでいるとは限らない。1 次キャッシュにミスした場合に非同期アクセスする。
- 3 次キャッシュ: 永続化するためディスクに保存されている。2 次キャッシュにミスした場合に非同期アクセスする。
 
- キャッシュポリシは LRU/LLU
- キャッシュミスしてネットワークから画像をダウンロードする前に別の方法で画像を探すためのプラグインを組み込むことができる。
- Twitter クライアントの場合、TIP 導入前の古いバージョンのクライアントのキャッシュ機構が保存した画像が大量にある場合がある。ネットワークからダウンロードする前にそこから探すためこの仕組みを使っている。
 
- スレッドセーフ
- 同じ画像が複数の解像度の利用できる状況に対応する。
- キャッシュにある画像より小さい画像を要求された場合はネットワークアクセスせずに単純に縮小して返す。
- 逆の場合は高解像度の画像のダウンロードが終わるまで低解像度の画像をプレビュー画像として利用できる。
 
- ネットワークアクセスは隠蔽されており、同じ画像を複数回ダウンロードしたりはしない。
- ダウンロードに失敗したりキャンセルした場合、次回のダウンロードは続きから行う。
- プログレッシブ JPEG に対応し、ダウンローが完了前に表示できる。
- アニメーション GIF, GIF, プログレッシブJPEG, JPEG, JPEG-2000, WebP と ImageIO1 が対応するコーデックに対応する。
- デフォルトでは NSURLSession を使うが、ネットワーク層は抽象化されていて、任意のネットワーク層を追加できる。
 
- キャッシュに画像を保存できる。投稿する画像をキャッシュに保存しておけば、タイムラインに自分が投稿した画像を表示するためにわざわざダウンロードせずにすむ。
- 内部で何が起きているのか詳しくわかるようになっている。
- 画像が欠けたことを検出できる。
- TIP の UIImageView を使えば、画像の詳細情報をオーバーレイ表示できる。
- キャッシュに何が入っているのか、それはダウンロード完了しているのかなどの詳細がわかる。
 
インストール
CocoaPods に対応しているが公式のリポジトリだと Swift から使えないクラスがあった2ので直接 GitHub のリポジトリからとって来ました。
target 'TIP_Lesson1'
use_frameworks!
pod 'TwitterImagePipeline', :git => 'https://github.com/twitter/ios-twitter-image-pipeline.git', :branch => 'master'
画像を表示する
画像をダウンロードして UIImageView に表示する最低限のコードはこうなりそうでした。
class ViewController: UIViewController, TIPImageFetchDelegate {
    @IBOutlet weak var imageView: UIImageView!
    var pipeline = TIPImagePipeline(identifier: "TIP_Lesson1")!
    override func viewDidLoad() {
        super.viewDidLoad()
        TIPGlobalConfiguration.sharedInstance().logger = Logger()
        let url = URL(string: "https://upload.wikimedia.org/wikipedia/en/5/55/Bsd_daemon.jpg")!
        let request = ImageRequest(imageURL: url)
        let operation = pipeline.operation(with: request, context: nil, delegate: self)
        pipeline.fetchImage(with: operation)
    }
    func tip_imageFetchOperation(_ op: TIPImageFetchOperation, didLoadFinalImage finalResult: TIPImageFetchResult) {
        imageView.image = finalResult.imageContainer.image
    }
}
class ImageRequest: NSObject, TIPImageFetchRequest {
    public var imageURL: URL
    init(imageURL: URL) {
        self.imageURL = imageURL
    }
}
class Logger: NSObject, TIPLogger {
    func tip_log(with level: TIPLogLevel, file: String, function: String, line: Int32, message: String) {
        let map: [TIPLogLevel: String] = [
            .emergency:     "EMG",
            .alert:         "ALT",
            .critical:      "CRT",
            .error:         "ERR",
            .warning:       "WRN",
            .notice:        "NTC",
            .information:   "INT",
            .debug:         "DBG"
        ]
        print("[\(map[level]!)] \(message)")
    }
}
ログを出す必要がなければ TIPGlobalConfiguration.sharedInstance().logger = Logger() と Logger クラスもいらないです。
プロジェクトはこちらへ置いておきます: https://github.com/yamoridon/TIP_Lesson1
- 
iOS 標準の画像コーデックライブラリっぽい。Image I/O Programming Guide ↩ 
- 
ログを出力する場合、TIPLogger というプロトコルを継承したクラスを作ることになっているが、Swift では継承できないとエラーが出た。 ↩