JSON 再び


この記事は Appcelrator Titanium Advent Calendar 2015 の 21 日目の記事です。

今年の夏、私は Titanium のある挙動に悩まされていました。よく Titanium でハマるのは Android であると言われ、私もイベントで LT / 登壇することがあるとそのように言ってきたのですが、今回は iOS で発生した問題と、その解決法についての共有です。

JSON をどう扱いますか?

言わずもがな、 Titanium は JavaScript を使ってネイティブ UI を持つモバイルアプリケーション開発を行う環境です。そして JSON は JavaScript Object Notation が正式名称であるように、 JavaScript 生まれのメッセージフォーマット。つまりは Titanium と JSON の相性は抜群で、

var json = JSON.parse(json_text);
var text = JSON.stringify(json);

たったこれだけでシリアライズ・デシリアライズが行えます。 JSON をレスポンスフォーマットとした Web API が非常に多く公開されている今、クライアントアプリケーションを作るために Titanium を選ぶのは、理にかなった選択肢と言えます。

64bit

iPhone 5s 以降 64bit CPU が搭載された iOS デバイスが登場し、今では完全に 64bit に移行済みとなった iOS プラットフォームですが、今年の 2 月以降、 Apple からお達しで、 64bit バイナリ (ARM64 バイナリ) が同梱されていないアプリの提出ができなくなりました。

Titanium では Titanium SDK の他、使用しているネイティブモジュールも 64bit 対応を行う必要があり、 SDK は 3.5.0.GA から 64bit に対応しました。 64bit になったからと API に変更があるわけではなく、前述の JSON API もそのまま動作する はず でした。

メモリーリーク

とある案件で事件は起きました。 64bit 環境の iOS デバイスで大きめの JSON (1MB) を JSON.parse を使ってデシリアライズすると、途端にメモリリークを引き起こすのです。急激にメモリを食いつぶしてしまい、頻繁にクラッシュを引き起こしていました。 Instruments を使って確認してみると上のような状態でした。

Titanium SDK を 64bit 非対応の世代のものに戻してみると、この問題は発生しませんでした。 JSON.parse のタイミングを変えてみたり、 JSON オブジェクトを使い終わったら null を入れてみたりするなど試みましたが、この問題は解決しませんでした。ちなみにこの案件では Titanium SDK 4.0.0.GA を使用しており、タイミング的に 4.1.0.GA や 5.0.0.GA が登場していたのですが、これらに切り替えても問題は解決しませんでした。

Titanium 向けの JavaScript Core は GitHub で公開されていますが、 Titanium SDK のバージョンが違っても使われる JavaScript Core のバージョンが同じであるため、 Titanium SDK を変えても挙動の改善が見られなかったと推測しています。

モジュールの力

Titanium を本格的に仕事で使用してモバイルアプリケーションを作り始めると、様々な場面で "モジュール" を活用し始めます。ほとんどの場合は著名な iOS / Android 向けのライブラリを Titanium からも使えるようにする、 "ブリッジ API" を作ったり、ちょっとした処理をネイティブに委譲するためのものを作り、使うことになるでしょう。 Titanium 自体が備えている機能を代替することは滅多にありません。

滅多にないはずでしたが、今回の問題に対応するためには、 JSON を処理するという Titanium 自体の機能を代替する必要がありました。 JSON のシリアライズ・デシリアライズを行うモジュールの構築です。

実は 2 年前に実験と LT のネタにするために JSON モジュールを作っていました。このモジュールでは iOS 5 以降に標準搭載されるようになった NSJSONSerialization とサードパーティライブラリである JSONKit / SBJson を使っていましたが、今回は安定した動作が期待できる NSJSONSerialization ベースで新しく開発することにしました。

さらに、 Titanium SDK の iOS 向けコードを grep してみると、 TiUtils.m に興味深い記述が見つかります。 Titanium SDK 4.0.0.GA のコードを見てみましょう。なんと、 Titanium SDK の内部で NSJSONSerialization を使ってシリアライズ・デシリアライズを行うためのコードが実装されています。これを活用しない手はありません。

そんなわけで、モジュールを作りました。今回は案件対応のための複合的なモジュールから JSON 処理だけを行う部分を分離させ、 GitHub に配置しました

コンパイル済みのモジュールのバイナリは GitHub Releases に登録してあります

var module = require('com.r384ta.ti.module.tinativejson');
var json = module.parse('{"key": "value"}');
var text = module.stringify(json);

このモジュールを使った効果は絶大で、 Instruments 上で 600MB 程度のメモリ使用量まで膨れあがっていたものが、 1/10 の 60MB 程度のメモリ使用量に抑えられることになりました。案件では 1MB 程度の JSON 以外でも効果はあり、全体的に省メモリで JSON の処理を行える結果が得られました。

最後に

今回の例は、大きな JSON を使った場合に発生する特殊な例とは思いますが、原因が特定できるまでかなりの時間を費やした問題でした。最初はまさか JSON を処理するという基本的な API に問題があるとは考えられず、全く見当違いの場所を探ってしまっていたのですが、 Instruments を使うことで問題を目の当たりにして、こういうこともあるのだな……と。

もしも大きな JSON を取り扱うような Titanium iOS プロジェクトを実践されている方がいて、同じような問題に直面したらモジュールをご活用ください。メモリ使用量に関しても、これまた別途モジュールを組み立てるか Instruments で監視をしないと発覚しづらいため、一度確認されることをお勧めいたします。実は……なんてこともあるかもしれません。