前述
ARCは偉大。メモリ管理をほとんど気にすることなくプログラミングが出来るようになった。ーーそう思っていた時期が僕にもありました。
何が問題か
ARCのおかげで、基本循環参照以外はコンパイラーが自動で使わなくなったオブジェクトをリリースしてくれるようになりましたが、ただしオブジェクトのリリースとメモリの解放はまた別問題です。まずこれを常に念頭においておかないといずれはメモリの圧迫に悩まされます。実際問題として特別に指定しない限り、OSは基本メモリが足りない時だけゴミを捨てているっぽいです。
例えば以下のようなコードが有るとします:
for i in 0 ..< 100000 {
let image = UIImage(named: "image.jpg")!
}
上記のコードを動かしてみればわかりますが、実際メモリの使用量はずーーっと上がりっぱなしです。え?もう image 使ってないのに?と、僕のようなド新人プログラマーなら驚くかもしれませんがはい本当にそうです。↓
(上記のコードを動かした時のメモリ使用量グラフ)
ちなみにこれを無限ループにしてみるとわかりますが、確かに一応OSはメモリ解放してはくれます。そのタイミングは本当にメモリが圧迫されてて足りない時だけです。しかも要らないもの全部開放してるわけではなく、適当です。
(上記コードで for i in 0 ..< 100000 を while true に直した時のメモリ使用量グラフ)
このグラフでわかると思いますが、だいたいメモリが本当に足りなくなったらメモリを開放しています。ただし量は適当です。そしてこれは最終形態ではなく、これを放置するとそのうちアプリが落ちます。しかもランタイムエラーではなく、いきなりスポンッと落ちて、「Message from debugger: Terminated due to memory error」という謎のエラーメッセージを喰らいます。
Terminated due to memory error の正体
この問題を解くためには、Xcode のメモリグラフだけでは同しようもないので、Instruments を使う必要があります。が、それの使い方を解説する記事ではありませんので、ざっくり言いますと、要するにそれは本当にメモリが開放されたわけではないっぽいです。それで実メモリがどんどん圧迫されていき、メモリ解放が間に合わず最終的にメモリリークと同じような症状でアプリが落ちます。
これなら一体どうすれば…どうすれば…どうすればいいの?
Objective-Cの時代は「@autoreleasepool{}」というやつがあって、あれは何かというと、使い終わったら即捨てて欲しいものを「@autoreleasepool」という容器の中に入れれば、OSは自動で処理してくれるという仕組みです。ああこれで可能性を感じてきましたね。というわけでこの「@autoreleasepool」を Swift で使いたい!じゃあどうすればいいかというと、実は Objective-C とほぼ同じで、前の「@」マークを取り除くだけです。というわけで、上記のコードを以下のように改造すれば、メモリ圧迫の問題も解消します!
while true {
autoreleasepool {
let image = UIImage(named: "image.jpg")!
}
}
これでもう一回アプリを実行してみましょう。あら不思議、今度はちゃんとメモリが必要以上に確保されることなく、スポンッと落ちることがなくなり、メモリ使用量グラフもこのように綺麗になりました↓
これで万事解決!と思うんじゃん?
実はこのこの autoreleasepool、大体のオブジェクトには問題ないのですが、一部のオブジェクトには効果がないのです。その代表的な一例は「UIView」(もちろん UIView のサブクラスも含む)です。例えば上記のコードを更に以下のように改造してみましょう:
while true {
autoreleasepool {
let image = UIImage(named: "image.jpg")!
let imageView = UIImageView(image: image)
}
}
これでもう一回メモリ使用量を確認してみましょう。するとまた、最初のグラフと同じように右肩上がりのメモリ使用量になってしまいます、最終的にはスポンッと落ちてしまいます↓
結論
現段階では少なくとも筆者は UIView オブジェクトのメモリ解放の解決法を見つけませんでした。上記のコードで試しに imageView.removeFromSuperview() とかいうロジック的にワケガワカラナイ呪文も唱えても見ましたけど案の定全くもって意味ありませんでした。ちなみに筆者の場合はそもそもなぜこういうことが起きたかというと、一度使った UIView オブジェクトを初期値に戻すのが面倒と思って全部古いやつを捨ててまた一から新しく作り直してるわけですよ。そしたら通常時ではなんの問題もなかったのですが早送りすると CPU がフル回転でメモリ解放する余裕もなくメモリがどんどん圧迫していきこのザマです。やっぱ大人しくちゃんと全部初期値に戻しましょう。
というわけで、UIView 系のオブジェクトは、むやみに作り過ぎないこと。そして Terminated due to memory error とかいう謎のエラーメッセージを喰らったら、おそらく何処かでメモリを食い尽くしているので、大量にオブジェクトを生成しているところを探しましょう。