CoreDataとCollectionViewでのメモリ管理と闘う。。。

今回は説明っていうより迷走愚痴ブログみたいな感じになるので、役に立つコードなんかはないと思います。

 

どうも。Reoです。9月に入ってからずっとRobinの改修作業をしています。

いつもはサボる日のが多いんじゃないのって感じなんですが、今月は本当に毎日ちゃんと作業をして、めっちゃ頑張ってます。

現在改修中のRobinちゃんは、毎日コマ撮りをしてコマドリ動画を作ろう!というものです。(詳しくは【iOSアプリ】毎日をコマドリする「Robin」AppStoreにて配信開始しました!【GIF作成】に書いてあります。)

 

半月ずっと同じ作業をしているというか、解決策をずーっと模索していました。

やっていることはメモリ管理。メモリリークがヤバイのです。

 

リリース当初のRobinは開くのすら結構重いアプリでした。

メモリ管理まで頭が回ってないっていうよりは、重いのはわかってるんだけど、とにかく今は出したい!区切りつけたい!ってことで結構不満足に思いつつのリリースでした。

アプリが重くなるのはいっぱい写真を撮るからで、「ユーザーがいっぱい写真を撮ってしまう前に、アップデートすればいいんや!」とも思っていました。

実際は精神的にも色々参っている時でしたし、チーム作業の方で手一杯なのもあって、全然とりかかれていなかったんですね。

 

Robinをリリースした後にGitを初めて導入したので、いつ作業したのかっていうのがはっきりわかっちゃうんですが、4月頭にリリースして、Git導入したのが5月末です。

完全にまるっと2ヶ月触ってないってことです。こりゃあかん。

 

そんで6月にちょっと作業をしていて、7月0コミットなんですが、いったい私は何をしていたんだ・・・。

そんなこんなで、ようやく8月中旬からガッツリRobinと向かい合ってるわけなのです。

でもリリースされているのがあまりにも重すぎて、6月作業後のやつと比べるともう全然起動速度が違うので、メモリ管理は完璧じゃないけれど今のよりマシでしょ・・・とアップデートしました。

そしてリジェクト->(【iOSアプリ】初めてのリジェクトをくらってしまった話)

 

前置きが長くなってしまいました。

とにかくメモリ使用量がやばくてやばくてやばいんです!!!

ちなみに、Git様のおかげで最初のリリースバージョンの状態にも戻ることができるので、このときのメモリどうだったんだろ・・・と思って試してみたら起動すらできませんでした。一応アプデしておいて正解だった・・・。

 

CoreDataを使ってデータを保存

RobinではCoreDataを用いて写真を保存しています。

どちらかというとRealm使ってるアプリのが多いんだろうか、当時はそんなもんの存在は知らなくて、むしろCoreData一択でした。そもそもライブラリ導入するのがあんまり好かんのです。(なんかよく失敗する)

 

一番最初は、まずロード画面で全てのデータを取り出して配列にぶちこんでました。本当に全てのデータを取り出してどうやらしていたんですね。

そしてこのRobinで厄介なのが、同じ写真をいろんな画面でいろんなサイズで表示している、ということなのです。

メイン画面を見開きするとこんな感じ

 

左がカレンダー画面で、右が全ての写真が観れるコレクション画面です。

さらにはこれらをタップした先の画面でもリサイズ。

他にも動画作成画面でもまた別の大きさにリサイズしているんです。

とにかく1つ別にリサイズしたものを保存しておくってするにしては、いろんなサイズがあって面倒くさい。

最初のバージョンではViewが表示されるたびに毎回配列回してリサイズしてってやっていて、枚数が多いとそこでメモリリークして終了でした。恐ろしいですね。

そして最近アップデートしたバージョンではそれをやめて、リサイズ処理を非同期でセルごとに行うようにしたんですね。これはとりあえず正解!でした。

 

とりあえず前よりは落ちないアプリになったんですが、メモリはスクロールするたびに相変わらず増えています。これをどうにかしないと本当にヤバイ。

 

NSFetchedResultsControllerとの出会い

色々調べまくって出会ったのがNSFetchedResultsControllerというもの。

これはUITableViewやUICollectionViewと合わせて使うとベリーグッドなもので、1つのデータに1つのセルが対応しているRobinにはピッタリなものでした。

今回使い方は割愛しますが、また別記事にするかもしれません。

色々と書くことはありますが、とりあえずデータをpredicateで絞って、それを1つずつ取り出すっていうのがすごく簡単にできます。

collectionView(cellForItemAt:)で書くのは

let object = FetchedResultsController.object(at: indexPath)

これだけで、あとは例えばobject.imageとかで取得ができちゃいます。

 

とりあえずこいつを導入。

 

そうすると・・・・!メモリが・・・・!

 

前と変わらんやんけ!!!!!!

ってことで導入したからってメモリリークしているのとは特に関係ありませんでした。残念。

 

collectionView(cellForItemAt:)で直接データを取り出してみた

Robinでは最初の画面でアルバム一覧が表示されます。

アルバムを選択すると、上記のカレンダーだったりコレクションだったりに遷移します。

 

最初のアルバム一覧ではアルバムの情報は全て欲しいけれど、写真は最新の一枚だけで欲しいのです。

うーむ、じゃあいっそセルごとに1枚だけ読み込んじゃうのが一番じゃない?と思って実行。

AlbumっていうモデルとImageというモデルがあり、それが1対多の関係になっているのですが、Albumから直接Imageを1つ取り出すのが恐らく普通なのかな?と思うんですが、今回そうでなく、collectionView(cellForItemAt:)でフェッチして取り出すという方法をやってみました。

いざ・・・

あれっメモリリークしてない!!!!

 

えっなんで?ってなりました。

Album.images[0].image という風にImageモデルにアクセスしてやるとのいったい何が違うんや・・・

とりあえずはアルバム画面ではメモリリーク全くしなくなりました。

30のアルバム作ってあるんですが、どんだけスクロール早くしても大丈夫。やったぜ。

ここまでで1週間は使った。

 

そして原因に気づく・・・・

リサイズ処理でメモリリーク!

アルバム画面のメモリリークを潰し別のビューもこの方法で行こうぐへへと思っていたのですが、何故か上手くいかない。特にコレクション画面が一番メモリリークしてヤバイ。

NSFetchedResultsControllerを使ってセクションも簡単に作れる〜って思ってたんですが、どうにもカックカクだし上手くいかない。

アルバム画面ではなんで上手くいったのかを色々と考えてみてようやく原因に気づきました。っていうかその前に絶対ここだーってのは気づいてたけどはっきりとした原因がようやくわかりました。

 

画像リサイズするときにメモリリークしてるんですね。

もちろんメモリ管理といえばのautoreleasepoolなんかも書いてみてたんですが、なんの役にも立っていない。。。

 

調べていてよく見た話なんですが、ゆっくりスクロールするとメモリはちゃんと解放されていてリークすることはないけれど、速くスクロールするとすぐにメモリリークしちゃう!っていうまさにこれなんですね。

そのときの解決法にautoreleasepoolが出てくるんですが、私のところでは全く役に立ってくれない。

つまりは解放が間に合ってないわけです。

そんでアルバム画面での話に戻すと、おそらくcellでImageをフェッチするより本当はalbum.images[0].imageとして取り出した方がはやいんですね。

でもその処理がはやいせいでリサイズに回ってくるのがはやく、解放が間に合わない。

そこをフェッチでワンクッション置いてやると上手く解放されてくれるようになったってことだと思います。

 

そして解決へ・・・?

少し時間を待ってあげることで、うまくメモリリークを防ぐことができていたのがわかったのですが、他画面でもそれをしようとするとなかなか上手くいきませんでした。

 

結局のところ「Cellごとに1つデータを読み込んであげる」っていうのがなんだかんだで一番メモリリークしない方法でした。

もやもやはします。

 

そしてログでエラーを見つける・・・

CoreDataではDispatchQueueが使えない・・・!?

な、ナンダッテーーー!はやくいってよ!!!!

スレッドセーフじゃないからなんやららしいので、エラーが出てました。なのでしょうがなくDispatchQueueをmanagedObjectContext.performに変えてやりました。

 

するとですね、なんかカクカクになりました。ナンデヤ〜〜〜〜〜〜〜^

 

ということでまだまだ迷走中なのです。

 

とりあえず大量画像を表示する時は、cell上で1つずつデータを取り出すのが良いのかな?ってことです。

fetchLimit = 1にして、fetchOffsetを上手く使って取り出して・・・ってやるのが今の所のベストです。

 

毎度リサイズ処理をするせいではあると思うので、別で保存したほうがいいのかなーとも思います。マイグレーションがなかなか厄介なのでちょっと考えものなんですけどね。

fetchLimit1だと時間早すぎて解放間に合わないのでわざわざ4にしてた時期もありました。大分あほっぽいけど。

 

ちなみに非同期処理にしていたのを同期処理でやってみたところ、カックカクだけどメモリリークは全くしませんでした。

個人的にメモリリークといえば循環参照!なので、その辺がなさそうでホッとしています。

 

リサイズ処理でメモリリークさえしなければ・・・・!

しかもその解放できなかったやつは他のビューにいっても解放できないのでまずいのです。

カクカクをなくすとメモリリークし、メモリリークをなくすとカクカクになる・・・。

どっちも上手いことやるのはなかなか難しいです。

 

とりあえずは完璧ではないけれど、それでも以前の10分の1くらいのメモリ使用量にはなりました。前どんだけひどかったんだよって感じですね。

700MBくらいあったのが50~100MBくらいで収まるようになったのです。

でも同期処理で全部ちゃんと解放されているのをみると20MBくらいなのでそこまでどうにか持っていってからアプデしたいです。

 

結構心折れてるので完璧を目指さない可能性もありますけどね〜。でもやれるだけはやります。。。今月中にはアップデートしますので、そこまででできたレベルですね。

あんまりRobinばっかりやってるわけにもいかないし、しんどいし、とにかくこの山早く超えたいです。

 

ではでは。

 

 

 

 

2018/04/13追記
結局それぞれのビュー用にサムネイル保存しています。

んで、最初にまとめてではなくcollectionView(_:cellForItemAt:)内にてデータをセルごとに呼び出すことにしました。

これでだいぶ改善されたっぽい。けど実はまだちょっと不安定ではあります。
また少し戦わないとなー。

これ!っていう確定した解決策がまだない。
Swift ,

Comments...

コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS
more