JSON 再び

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

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

## JSON をどう扱いますか？

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

```javascript
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 もそのまま動作する **はず** でした。

### メモリーリーク

![memory_leak.png](https://qiita-image-store.s3.amazonaws.com/0/5036/e1240041-abbc-013a-26a3-c5dbbc0728af.png)

とある案件で事件は起きました。 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 で公開](https://github.com/appcelerator/tijscore)されていますが、 Titanium SDK のバージョンが違っても使われる JavaScript Core のバージョンが同じであるため、 Titanium SDK を変えても挙動の改善が見られなかったと推測しています。

### モジュールの力

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

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

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

さらに、 Titanium SDK の iOS 向けコードを grep してみると、 `TiUtils.m` に興味深い記述が見つかります。 Titanium SDK 4.0.0.GA の[コードを見てみましょう](https://github.com/appcelerator/titanium_mobile/blob/4_0_0_GA/iphone/Classes/TiUtils.m#L1885-L1921)。なんと、 Titanium SDK の内部で NSJSONSerialization を使ってシリアライズ・デシリアライズを行うためのコードが実装されています。これを活用しない手はありません。

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

コンパイル済みのモジュールのバイナリは [GitHub Releases に登録してあります](https://github.com/ryugoo/TiNativeJson/releases)。

```javascript
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 で監視をしないと発覚しづらいため、一度確認されることをお勧めいたします。実は……なんてこともあるかもしれません。