React Nativeアプリのメモリリークを追いかける

モバイルエンジニアの@hotchemiです。

今回は少し前にReact Nativeアプリの開発中にメモリリークを調査、解決した体験が学びが多かったので調査の顛末を共有できればと思います。

概要

今回問題となったのは、上記の課題・宿題を管理する画面の開発でした。

QA中に発覚した問題としては、「アプリを操作していると特定の端末(主にiPhone5)でアプリが落ちたり、その他の端末でも次第に筐体が熱くなる事がある」というもので、表示するデータはそれなりにあるものの画像や動画を表示する画面ではないのでさすがにおかしいだろうという事と、プロセス起動から時間が経つに従い事象が発生・深刻化していくという特性を考慮しメモリのリークが発生しているという仮説を元に調査を開始しました。

モニタリング

推測するな、計測せよ」の格言通り、まずはメモリの使用量を計測する所から始めてみます。

Native

React Nativeのコードは勿論JavaScriptですが、メモリの使用量自体は既存のiOS/Androidの開発ツールセットを使い回す事ができます。

今回は、XCodeDebug Navigatorを使いメモリの利用量を計測します。アプリを起動したまま放置していると次第グラフが右肩上がりになっていき、数分で2.28GBを計測しました。これはまずい…。

screen_shot_2018-10-01_at_5 50 16_pm

次に、Instrumentsを利用して更に具体的な手がかりを見つけられないか確認していきます。

80bytesのmallocが短期間に大量に呼ばれている事が分かりますが、JavaScriptの具体的なコードに繋がる情報はこれ以上は得られなさそうです。ただ、何らかの条件下においてオブジェクトが大量に生成されているのではないか、というざっくりした仮説を立てる事はできそうです。

screen shot 2018-12-05 at 21 30 22

JavaScript

Nativeのレイヤーではこれ以上の手がかりは無さそうなので今度はJavaScriptのレイヤーで調査してみます。

React NativeはChrome DevToolsを利用したデバッグができるので、MemoryタブからHeap Snapshotsを取ってどんなオブジェクトが生成されているのか確認してみます。

注意点としては、React Nativeはrelease buildではWebKitJavaScriptエンジンとして用いるのですが、debug buildで動いている実行エンジンはV8(Chrome DevToolsを利用する為)なのでheap dumpが同じになる保証はありません。正確に調査したい場合はSafari Developer Toolsを利用してデバッグした方がベターです。

何はともあれ、ヒープダンプを眺めてみます。Shallow SizeやObject Countでソートしてみていくと、先程Instrumentsで見かけた80bytesのオブジェクト群らしきものを見つけました。

screen shot 2018-12-06 at 2 25 03

これはいかにも怪しいので、しばらく時間を置いてsnapshotを取り直してdiffを確認してみます。

screen shot 2018-12-06 at 2 28 47

Objects Countが51から117に増加していました!どうやらアニメーション周りで問題が起きていそうです。

原因

上記の調査を元にアニメーションをしている箇所を調査していた結果、placeholderのアニメーションを実現する為に利用していたrn-placeholderというライブラリに原因がある事が分かったのでPull Requestを送りました(ちょっとサボってしまって最終的にはこちらで取り込まれました。v1.3.0でリリースされています)。

少し解説すると、Animated.startメソッドのcallbackに渡ってくるfinishedというbooleanフラグがtrueだった場合時のみアニメーションを再度開始するように修正をしました。この条件文が抜けていた為に、キャンセル・停止済のアニメーションに対しても再度startメソッドが呼び出され結果として必要ない大量のAnimatedValueオブジェクトが作られメモリ領域を圧迫してしまっていたという事でした。詳細が気になる方は以下の公式ドキュメントをご覧ください。

https://facebook.github.io/react-native/docs/animated#working-with-animations

ちなみに、このエントリを書く過程で調査してみたところReact Nativeでメモリリークの原因になりがちなのはAnimation以外にも以下の項目があるそうです。

  • Component内でのaddListenerの解除し忘れ
  • setIntervalのclearし忘れ
  • Closure(arrow function)による外部変数の参照によるキャプチャ

おわりに

「推測するな、計測せよ」に従いまず定量的な観点からデータを計測する事で、ランダムに調べがちなパフォーマンスやメモリ関連の調査の方向性をロジカルに決定できるのではないかと思います。

複数の言語のboundaryを超えたデバッグは簡単ではないですが、解決できるとその分嬉しいですね。

現場からは以上です。