「写真いっぱい撮ってるけど、一生振り返る暇ないな…」
という課題意識から、100枚/秒で写真を閲覧できる「Somato」というiOSアプリをつくった。
1,000枚の写真であれば10秒で閲覧できる。
まぁ正直100枚/秒はインパクト重視で謳っているところはあるが、意外と20枚/秒ぐらいで高速表示していても不思議と「懐かしいなぁ」と十分に思い出に浸れる。無料なので興味のある人はぜひダウンロードして試してみてほしい。
(M1 Macでも利用可能。MacのApp Storeで"Somato"で検索してください)
本記事ではこのアプリの実装について書く。
概論
実は、Somatoは技術的に尖った部分は特にない。
ざっくりいうと、Photos(PhotoKit)で画像を取得し、UICollectionViewで表示しているだけ。
最初のプロトは1時間もかからずできた。特に工夫もなく、素直な実装をやってみたら、「あれ、100分の1秒以下で表示できてるぞ...!」っていう。(約10年前に同様のコンセプトでアプリをつくったときはデバイス性能の限界もありこうはいかなかった)
しかし、やはり当然ながら、プロダクトとしてちゃんと出すとなるとそう甘くはなかった...
メモリの問題
まず最初にさくっとつくったプロトでは、僕がiPhoneに入れている約500枚の画像を、最初に(表示開始前に)全部UIImageとして読み込んでいた。
3列表示に十分なサイズ(iPhone 12 miniで375x375
)で取得すればいいので、それでもメモリ使用量は大したことはない。
しかしこれが、iPhoneに1万枚以上の画像が入っているとなると話は違う。実際に一緒にやったデザイナーさんのiPhoneには16,000枚の写真・ビデオが入っていた。どんなにサイズが小さくとも、1万枚の画像をUIImageとしてメモリに置いておくことはできない。
さらに、1列表示だとより高い解像度の画像データが必要となる。
かといって、「大量の写真を高速に振り返ることができる」というのがアプリのコンセプトなので、「1,000枚までのアルバムが対象です」なんていう制限を設けるわけいにはいかない。1万枚だろうと100万枚だろうと、メモリを食わずに高速表示できる必要がある。
マルチスレッドの問題
上記の使用メモリの制約から、一定枚数だけをメモリにキャッシュして表示しつつ、古いキャッシュを捨てつつ、バックグラウンドスレッドで先読みする、という処理を1/100秒ペースで実行する必要がある。
で、画面描画のメインスレッドの他に、メモリキャッシュを管理するスレッド、ローカルやiCloudから画像を取得してくるスレッドetc.のマルチスレッド処理が必要になってくる。
キャッシュはアクセスを一本化するためにSerial Queueで、画像取得は並列に処理したいのでConcurrent Queueで、という点も注意が必要。
動画のフレーム抽出処理の問題
コンセプトとしては「大量にたまった写真を高速に閲覧する」というものなので、同じくたまっている動画も高速にふりかえりたい。Somatoでは動画からn秒おきにフレームを抽出して、それを1枚の写真と同様に高速に流す、という実装を行っている。(これがなかなかにエモい)
この動画処理がさらに鬼門で、フレームを抜き出す処理は動画のデコードが入るので1/100秒表示しながらだといくら先読みしていても間に合わない。
なのでいったんフレーム抽出したものをファイルキャッシュして、そこから必要な分だけメモリに読み込みつつ捨てつつ、みたいなことをする必要がある。
iCloudの問題
iCloudがさらにさらに鬼門だった。問題が大きく分けて2つ、
-
iCloudからの読み込み速度の問題
-
PhotoKitのInternal Errorの問題
があった。
1の方は、ネットワーク経由でのダウンロードとなる場合はローカルからの読み込みと比較して当然ながら相当時間がかかるので、 PHImageManager.default().requestImage〜
する際に
let options = PHImageRequestOptions()
options.deliveryMode = .fastFormat
でいったんサムネレベルの解像度の低い画像を取得してお茶を濁しつつiCloudから必要サイズの画像を取得し、ファイルキャッシュする、というのを100枚/秒で再生しつつバックグラウンドスレッドで行っている。(実は一度iCloudから取得しておけばPhotoKit/Photos自体がローカルキャッシュしてくれるようだが、そちらはいつキャッシュが解放されるのかが不明)
しかしそれよりもややこしく、そしてサードパーティデベロッパーにとってどうしようもない問題は2だ。これはシステムのiCloud同期の状態によって引き起こされる。試しに「設定」アプリの「写真」 > 「iCloud写真」をオフにし、5分ぐらい待ってから(データが削除されてから)、またオンにして、標準の写真アプリを見てほしい。iCloudの共有アルバムその他の写真がなかなか出てこないと思う。
iCloud同期を有効化した直後、あるいは標準の写真アプリで長いこと当該iCloudアルバム(写真)を閲覧していない状態だと、OS側でまだサムネ解像度の画像取得すらできておらず、このときにPhotosフレームワークでその写真にアクセスしようとすると、2 に書いたInternal Errorを引き起こしてしまう。
[PhotoKit] Error: Unknown internal error
これについては問題がニッチすぎるので対処法はローカルメモに留めておく。
まとめ
写真を高速表示するビューアアプリの実装について書いた。
500枚ぐらいのデータで「作ってみたらサクッとできた」んだけど、16,000枚ぐらいのケースや動画サポート、iCloudサポート等を考慮しはじめたら意外と大変だった、という話。
画像を1/100秒で表示したいというケースはあまりないかもしれないが、
- 画像をPhotosで高速に取得する
- それらを省メモリで行う(画像枚数にメモリ使用量を比例させない)
- 動画から高速にフレーム抽出する
こういった実装自体は実はわりと汎用的に役立つのではと思う。