iOS/AndroidのWebViewを活用したSPA(SinglePageApplication)でのゲーム開発

こんにちは！
「[CYBIRDエンジニア Advent Calendar 2016](http://qiita.com/advent-calendar/2016/cybird)」6日目担当の @megadreams14 です。


昨日は、PM業をこなしつつアプリエンジニアとして活躍している新卒4年目@sgtさんの
「[お問い合わせと目視でバグを潰してた俺がアプリクラッシュ検知ツールを使って効率の良いバグ潰しを始めた話](http://qiita.com/sgt/items/ef2b29d5ba5723addeb2)」でした。

クラッシュ情報の収集は不具合の早期発見修正につながるため、
機会損失の防止や開発効率の向上にもつながるので必須ツールですね。


さて、今回はiOS/AndroidのWebViewを活用したSPA(SinglePageApplication)での
ゲーム開発の中で得られた知見を簡単にご紹介をさせて頂きます。


# Webとネイティブの良い部分を取り入れたハイブリッド開発

現在自分が所属しているチームでは、Webの技術を中心としてこれまで開発を行っており、
AppStore/GooglePlayへの配信においても、
WebViewを利用した「ガワネイティブ」としてゲームの配信をしてきました。
（[ガワネイティブとは - IT用語辞典バイナリ](http://www.sophia-it.com/content/%E3%82%AC%E3%83%AF%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96)）

WebViewで動作するため、iOS/Android以外のプラットフォームへの展開もしやすく、
キャンペーンやデータの更新、開発スピード面での運用効率化をはかり、
サービス展開をすることが出来ています。



最近配信しているゲームでは、よりお客様に魅力を伝えるために画面をリッチにしたり
表現を豊かにする画面ではネイティブ(iOS/Android)で実装し、
キャンペーンの実施などの定常運用や更新頻度が高い画面では
WebViewを利用して開発を行っております。


また、WebViewの画面もネイティブに負けないようにSPA（SinglePageApplication）を用いて、
ネイティブとWebの両方の良い部分を取り入れた開発を行っています。


![図1_ネイティブとWebのハイブリッド構成.png](https://qiita-image-store.s3.amazonaws.com/0/17346/4295dd95-8d13-098d-9c26-53323d35f1b4.png)



# ハイブリッドアプリにおけるWebとネイティブのデータ管理

ハイブリッドアプリとして開発を進めていますが、
Webとネイティブでやりとりを行うためのインタフェース以外の部分は
基本的にはそれぞれの領域での処理を行い、
扱うデータの管理などもそれぞれで実施していました。


## ネイティブ側で扱うデータ管理
* 対象
   * 演出データやキャラ画像など表現としてクオリティを担保したいデータ
   * マスター関連のデータ（jsonファイルで管理）
* ダウンロードのタイミング
   * ダウンロード画面を用意し、ゲーム進行に必要なデータを起動時にダウンロード
* 管理方法
   * 「リソースデータ」として「いつ」「何を」「どこで」ダウンロードするのかをサーバ側で管理設定する

## Web側で扱うデータ管理
* 対象
   * サイト内のUI画像やキャンペーン画像、アニメーションファイルなど
   * マスター関連のデータ（jsonファイルで管理）
* ダウンロードのタイミング
   * 主に画面が描画されるときや対象のデータを利用するタイミングでダウンロード
      * よく利用するデータについては事前読み込みなどを実施
* 管理方法
   * 静的ファイル配信サーバにアップロードする


# Webの弱みでパフォーマンス劣化

Web側の開発では、Backboneを利用したSPAでの開発を行っており、
一度実行に必要なファイルを読み込めば、
ネイティブ画面のような画面遷移などを実現できています。

しかし、現状どれだけチューニングをしたとしても
「ネットワークを挟んだ通信」が前提とした作りのため、
画像やマスターデータ(jsonファイル)の取得がボトルネックとなり

* 「描画処理の遅延」
* 「ネットワーク環境に左右された処理速度」
* 「スマートフォンでの通信容量の肥大化」

といった問題につながり、「Webっぽさ」が残ったゲームになっている状況でした。


# Webの弱みをネイティブのWebViewで吸収

そこで、同じWebViewでゲーム開発をされている、
「FINAL FANTASY Record Keeper」などを参考にしつつ、
ネットワーク通信のボトルネックを解消するためにWebViewにテコ入れすることにしました。

* 参考
   * [「FINAL FANTASY Record Keeper の作り方 - slideshare](http://www.slideshare.net/dena_study/20141111-seminar-eisuke)」
      * WebViewを扱うときのデータキャッシュ周りを参考にし、すぐに導入出来そうな対応を進めました。


# 「あれば使う」「なければ取る、保存する」のシンプルなキャッシュ機構の導入

これまでもWebで扱うデータに関しては、

* HTTPレスポンスヘッダー
* WebViewの標準キャッシュ
* HTML5のApplicationCache

などを利用してボトルネックの解消に取り組んできましたが、

* iOS/AndroidでのWebViewの挙動の違い
* OSのバージョン違いに伴うキャッシュの扱いの差異

など、それぞれの不具合やOS毎に対応することは難しく、
また確実にキャッシュさせてネットワークのボトルネックを
確実に解消できるまでは出来ていませんでした。


## 実装にあたって

そのため、下記ポリシーでカスタムキャッシュの導入を行いました。

* ネイティブ領域のローカルにあればローカルのファイルを利用する
* 無ければサーバから取得し、そのデータをローカルに保存する
* 保存失敗などカスタムキャッシュでのエラーが出たときは、WebViewの正常な挙動の処理に戻す
* Web側はQueryStringのみを意識してキャッシュ制御を出来るようにする
   * ファイル名とQueryStringでキャッシュファイルを作成し、キャッシュ制御を行う


図にすると下記のような感じです。

![図2_WebViewを利用したカスタムキャッシュの処理の流れ.png](https://qiita-image-store.s3.amazonaws.com/0/17346/0d421464-08fe-cc36-e458-091256c9b920.png)


## 実装としてやったこと

* iOS
   * WebViewのNSURLCacheとNSCachedURLResponseを利用
      * WKWebViewではなくUIWebView
   * 手順
      1. リクエストをフックし、ローカルにあればそのデータを返す
      2. 無ければWebViewの標準処理に移しデータを取得
      3. レスポンスの処理をフックし取得したデータをローカルに保存
      4. 保存後WebViewの呼び出し元に処理を返す


* Android
   * AndroidのWebViewのshouldInterceptRequestを利用
      * JavaでHTTPクライアントを生成しデータを取得
      * InputStreamをWebResourceResponseとして返す

* サーバ
    * レスポンスヘッダーにハッシュ値を追加
        * サーバからいつでもキャッシュ破棄やデータ更新が出来るように設定

* Web
    * キャッシュさせたいデータにQueryStringを付ける
        * APIのレスポンスヘッダーのハッシュ値をQueryStringに付与
    * ハッシュ値の更新でカスタムキャッシュの破棄を実行


## 現状の課題

* レスポンスヘッダーの扱い
   * CORS(Cross-Origin Resource Sharing) 対策
      * 従来のWebのチューニング方法でドメイン分割などをしている場合など
      * カスタムキャッシュのレスポンスヘッダーの付与に注意
   * レスポンスヘッダーの付与は、Android 5.0以上しか対応できないらしい
      * そのため、Android 4以下はカスタムキャッシュを利用出来ていない

* データの整合性
    * キャッシュとしてローカルに保存したファイルが正常かどうかのチェックが出来ていない
        * バイナリデータとしてWebに返されるのでネイティブ側では現状チェックを出来ていない
    * 画像に関してはWeb側で下記のような処理で読み込みのエラー処理を念のため実施
        * エラー時はキャッシュから取得しない形で再取得させる

        ```
        var img = new Image();
        img.onerror = function(){};
        img.onload = function(){};
        img.src = url;
        ```


本当はWebで扱うデータをネイティブで扱うデータ同様にリソースデータとして扱い、
WebViewからの実行は既にダウンロードしているローカルファイルを全て参照するようにすれば、
ネットワークを挟む処理はAPI通信のみとなるのですが、
環境を整備するのに時間がかかりすぎるため、
今回はWeb領域、ネイティブ領域、サーバ領域それぞれの領域が少しずつ頑張れば実装できる
上記のような仕組みで実現しました。



# 導入した結果

導入前は、キャリア回線で通信制限に入ったとき（1ヶ月の通信上限を上回ったときの）にゲームが起動すら出来なかったのですが、
導入後では速度制限時の「128kbps」ネットワーク環境でも動作するようになりました。
（※ 初回のみWebViewで一通り画面遷移を繰り返してデータを取得しておく必要はありますが...）

また、導入前には1ヶ月あたり3.3GB程度かかっていた通信容量が0.5GBまで削減され、
画面描画や遷移の高速化など、ネットワーク通信がボトルネックとなっていた箇所においても大幅な改善が見られ、
まだまだWebの可能性を追求することが出来ると感じた仕組みになりました。


## 最後に

最近はゲーム業界においてもネイティブ化がかなり進んでおり、
スマートフォンで配信するゲームのクオリティも上がってきています。

それらと並行してWebの技術も進歩しWebの可能性としても広がっていますが、
スマートフォン向けアプリとして配信するにはOS依存や
iOS/AndroidのWebViewと向き合っていく必要があり、苦労の連続かと思います。

ネイティブの弱みはWebでカバーし、Webの弱みをネイティブでカバーできれば
まだまだWebの可能性は広がると思いますので、
個人的にはWebの領域でまだチャレンジしていきたいなと感じています。


-----

「[CYBIRDエンジニア Advent Calendar 2016](http://qiita.com/advent-calendar/2016/cybird)」 明日は、弊社AWS費用コストカッターで有名な @gucchon さんです。
過去のアドベントカレンダーでは「[コストを下げろと言われたら~AWSでまずやること~](http://qiita.com/gucchon/items/33731a18e5b457f2bd34)」でバズった彼ですが、
今年は何を書かれるのか楽しみですね。