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