はじめに
最近約1ヶ月間、RettyのiOSアプリパフォーマンスの改善について集中的に作業しました。 本記事では、
- 今回の改善の必要性について
- どのような改善をしたのか
- どのような効果があったのか
上記について2章にわたってご紹介しようと思います。
画像のキャッシュを管理するマネージャーの導入背景
Rettyアプリでは数多くの投稿写真を表示しているので、写真に対してキャッシュメモリーを多く使っています。
それによってメモリーが多く消費されてしまう可能性が高いのですが、 これはパフォーマンス的に遅くなる上、メモリー不足問題につながってアプリがクラッシュする問題がありました。
よって、周期的にキャッシュを削除する機能が必要でした。 そのため、下記のようなキャッシュマネージャーを作ろうとしました。
- Default キャッシュだけで管理するのではなく、1いくつかの特殊な場合の画面についてはキャッシュを別々に管理する
- キャッシュに対して設定と特定のキャッシュを削除するなどのコントロールを行う
- Singletonパターンで管理する
Image Cache Manager
iOSでは非同期写真のライブラリーとしてKingfisherを利用しています。
Kingfisherでは、すでにキャッシュ機能がある程度入っているので、簡単にマネージャーを作ることができました。
そのため、下記のコードでキャッシュマネージャーを構成しています。
ImageCacheManagerのコード
import Foundation import Kingfisher typealias ImageCacheType = ImageCacheManager.ImageCacheType final class ImageCacheManager { static let shared = ImageCacheManager() enum ImageCacheType: String { case restaurantCache = "restaurant-top-cache.retty.me" case reportCache = "report-detail-cache.retty.me" case defaultCache func getCache() -> ImageCache { switch self { case .restaurantCache: return ImageCacheManager.shared.restaurantCache case .reportCache: return ImageCacheManager.shared.reportCache case .defaultCache: return ImageCacheManager.shared.defaultCache } } } private let restaurantCache: ImageCache private let reportCache: ImageCache private let defaultCache = ImageCache.default init() { // 100 mb memory, 60 sec to expiration let megabyte: Int = 1024 * 1024 restaurantCache = ImageCache(name: ImageCacheType.restaurantCache.rawValue) restaurantCache.memoryStorage.config.totalCostLimit = 100 * megabyte restaurantCache.memoryStorage.config.countLimit = 100 restaurantCache.memoryStorage.config.cleanInterval = 60 restaurantCache.memoryStorage.config.expiration = .seconds(60) restaurantCache.diskStorage.config.sizeLimit = UInt(300 * megabyte) reportCache = ImageCache(name: ImageCacheType.reportCache.rawValue) reportCache.memoryStorage.config.totalCostLimit = 100 * megabyte reportCache.memoryStorage.config.countLimit = 100 reportCache.memoryStorage.config.cleanInterval = 60 reportCache.memoryStorage.config.expiration = .seconds(60) reportCache.diskStorage.config.sizeLimit = UInt(300 * megabyte) defaultCache.memoryStorage.config.totalCostLimit = 300 * megabyte defaultCache.memoryStorage.config.countLimit = 100 defaultCache.memoryStorage.config.cleanInterval = 30 defaultCache.memoryStorage.config.expiration = .seconds(60) defaultCache.diskStorage.config.sizeLimit = UInt(500 * megabyte) } func clearCache(_ types: [ImageCacheType]) { for type in types { switch type { case .restaurantCache: restaurantCache.clearMemoryCache() case .reportCache: reportCache.clearMemoryCache() case .defaultCache: defaultCache.clearMemoryCache() } } } func removeExpiredCache() { restaurantCache.memoryStorage.removeExpired() reportCache.memoryStorage.removeExpired() defaultCache.memoryStorage.removeExpired() } }
それぞれのCacheタイプを利用している画面の例
画像に対するキャッシュ保存設定
例-UIKitの画像設定コード
public func rt_setImage( withURL url: URL?, placeholderImage: UIImage? = nil, processors: [ImageProcessor] = [], fade: TimeInterval = 0.4, targetCache: ImageCache? = nil, completion: ((UIImage?) -> Void)? = nil ) { image = placeholderImage guard let url = url else { return } let cache: ImageCache if let targetCache = targetCache { cache = targetCache } else { cache = ImageCacheType.defaultCache.getCache() } let kf = KF.url(url) .targetCache(cache) .diskCacheExpiration(.seconds(60)) .memoryCacheExpiration(.seconds(60)) .onSuccess { result in completion?(result.image) } .placeholder(placeholderImage) .fade(duration: fade) processors.forEach { _ = kf.setProcessor($0) } kf.set(to: self) }
例-SwiftUIの画像設定コード
KFImage(URL(string: "")) .targetCache(ImageCacheType.restaurantCache.getCache())
上記のようにパラメータでImageCacheTypeを伝えると、ダウンロードする際にそのキャッシュに保存されるように指定しています。
キャッシュ削除
例-キャッシュ削除コード
ImageCacheManager.shared.clearCache([.restaurantCache, .reportCache]) ImageCacheManager.shared.removeExpiredCache()
これにより画面遷移を行なったりメモリーに対する初期化が必要な場合によっては上記のようなコードで満了したキャッシュを削除したり、これ以上キャッシュを必要としてないキャッシュタイプに対して削除を行っています。
今後改善するもの
1. SwiftUI
KFImageは内部にImageBinding Instanceを持っていますが、これにはKFCrossPlatformImageと言うImageTypeが入っています。
問題は、これはUITableViewやUICollectionViewのセルが消えてもMemory Freeにならないです。 したがって、今後はSwiftUIではAsyncImageを利用しようと思います。 ただこれはキャッシュ機能は入っていないので、直接キャッシュ機能を作る必要があります。
2. Disk Cache
現在全てのキャッシュについてはメモリーに保存しているため、2自分のデータ(例:自分の投稿写真)などについては非効率的に管理しています。 したがって、自分の写真データについてはメモリー上ではなく、ディスクに保存するようにして、ディスクから写真を持ってくるように改善しようと思っています。
3. まだCache Typeが追加されていない他の画面についてもImage Cache Typeを追加する
まだ初期段階なので、いくつかの画面に対してのみ開発されています。 したがって、対応が必要な画面については、追加的にメモリー管理ができるようCache Typeを追加していく予定です。
効果
Before
After
予想通り、画面が使わなくなった時や、画像のキャッシュが必要なくなった場合にメモリーリリースされることがわかりました。
あとは設定したメモリーサイズよりキャッシュを保存しないことも確認できました👍
結果、ユーザーさんがより安定したアプリを使うことができるようになりました!🚀
- このテスト結果は数百件の写真がある店舗で写真を全部表示し、お店の画面を閉じたときのメモリー情報です
- Before / Afterテスト結果はImageCacheManagerをOff / Onした時の挙動で、表示したお店と画像は同じです。
- 画面のメモリーリークはありません
おわりに
今回の記事ではパフォーマンス改善のためのSTEP第一として画像キャッシュマネージャーについてご紹介しました。
結果としては、各画面で使用しているイメージキャッシュメモリーに対して適切に管理ができるようになり、 よりアプリが効率的に動作できるようになりました。
次の章ではDiffableDataSourceを導入して、リスト画面でのパフォーマンス改善を行った話や、 具体的に1ヶ月間でどれぐらい改善ができたのかについて話をしようと思います。
ぜひ、お楽しみにしてください!
アプリ開発チームのメンバーとMeetyでお話しできます!本記事でRettyのアプリ開発チームについて興味が湧きましたら、ぜひMeetyで気軽にお話ししましょう!