2015年11月23日月曜日

いまさら、だけど Activity の遷移と終了について

ツールでもゲームでも複数の土台を持ちたい場合がありますよね。 土台というと分かりにくいけれども、例えば単純なゲームであるインベーダーやテトリスなら、 タイトル画面とメイン画面だけ用意すればいいですよね? でも、 Wiz(3D迷宮RPG) なんかだとタイトル画面、街滞在中の画面、3Dダンジョンの画面、戦闘画面、といくつか必要になりそうです。 Android でアプリを作る際には、 画面のデザインが異なる場合は、そのデザインパターンの数だけアクティビティーを作ります。 これが面倒なら、一つのアクティビティーで複数のレイアウトを使い回すこともできます。

アプリを複数のアクティビティーで分割管理することにはいくつかのメリットがあります。 まず、プログラムの構成がアカデミックな感じになります。 インデックスといいますか、大分類>小分類みたいな感じで、大量に発生してしまう Class(特定の役割を担ったプログラムの塊)を、 上手に仕分けることができます。 上手に整理すれば、どこに何があるのか分かりやすいし、機能を変更・追加する場合にしたって最小限の労力で作業を終わらせることができます。 整理がきっちりされている方が、バグや不具合が発生しにくいという側面だってあります。 それに、実際に使う部分だけのリソース(画像ファイルとか)を読み込ませることで、 マシーンの負担を軽減させることだってできる。 メリットはたくさんあります。

で、結局のところ、ある程度以上プログラムの規模がでっかくなりそうなときは、 素直にアクティビティーを複数作って、作業を分業化する方が効率的です。 複数のアクティビティーがあるということは、 アクティビティーの遷移と呼ばれる行為が必要となります。 これは、要するにアクティビティーAからアクティビティーBにジャンプするという行為です。 これによってAは後ろに下がり、Bが表に出てくるということになります。 これは、Android でプログラミングする際の超基本なんですが、ちょっとちょっと今さらながら、理解を深める必要があると思っている次第です。

finish() で現在、進行中の Activity を終わらせる。そう思っていた時期がオレにもありました。 と、いうか今まで当然のようにコレを行っていた。 が、しかし、Activity の終了を明示的に行わない方がいいらしい。 どういうことか? つまり、Activity の遷移だけ指示して、あとの終了過程はお任せしてしまうのである。

Activity には3つの状態がある。
実行中:表に出てきていてアクティブな状態
一時停止:表に出ていないが、一部が見えているケースもある
停止:バックグラウンドであり、見えない。システムによって終了させられやすい
一時停止と停止のときは、まだ完全には死に絶えておらず、変数なども破棄されていない。 メモリの余裕がなくなってきた場合は、停止状態のアクティビティーが優先的に廃棄される。 一時停止状態も場合によっては強制停止される。 つまり、メモリが不足してきた際に、自動的にアクティビティーを強制終了させるという 便利システムをアンドロイドさんが搭載しているので、終了過程は端末にお任せすればよいということみたい。 現に携帯は、ブラウザ、電話、メール、Playストア、YouTube、ゲームA、ゲームB、ゲームC、電卓、メモ帳…みたいな感じでいろいろなアプリを 同時並行的に使いますからね。こういう乱立状態の中で上手にメモリをやりくりしようという Google さんの知恵がシステムに反映されているようです。

で、Android のプログラミングをやってみようという入門書に必ず書かれているライフサイクルというものがある。 特定のタイミングで自動的に呼び出されるメソッドがあるから、そこで初期化とか終了処理とかするといいぜ、という奴である。 一発目の初回に起動させた時は、 onCreate()→onResume()の順で呼び出される。 終了するときは、onPasuse()→onStop()の順で呼び出されるし、 再開時は、onResume()のみが呼び出される。 アクティビティーを強制終了させない限りは、onCreate() の部分はショートカットされるようだ。 また、本当に強制終了されたときは、onDestroy()が呼び出され、 この状態からアプリを開いたらご丁寧に onCreate() からスタートするみたいだ。

onCreate() に記述する内容は最低限にしておかないと、 onCreate() の中身がスルーされてしまうかもしれない。 onResume() に記述しておけば、確実に実行されるだろう。 onCreate() や onResume() で変数の初期化を行うと、再開時に微秒なことが起こるので気を付けないといけない。 このあたりは、よくよく注意してプログラミングしないとバグを引き起こす元となりますよ。

再開時は、最後に開いていたアクティビティーから復活するのが基本です。 例えば、MainActivity→FieldActivity→BattleActivity という具合に遷移していって、 最後のバトルアクティビティーをやっている最中にホームボタンで中断したとしましょう。 このとき、BattleActivity から再開しようとするが、ここが finish() されていたら、一つ前の FieldActivity から再開されてしまう。 とにかく、 finish() を使ってアクティビティーを強制終了させると逆に混乱を産んでしまうので使わない方がよいだろう。

また、バックキー押したときの対応だが、一つ前のアクティビティーに戻るものの、 変数の初期化に失敗してチグハグなことになりがちなので、特にゲームではバックキーそのものを無効化してしまうのが最も簡単な解決策となるでしょう。

セーブと一時保存

ここまで書いているのは、あくまで一時保存からの再開です。 一時保存なので、保存の信用性はそれほど高くありません。 スマホの使い方によって個人差が著しくあるでしょうが、1ヵ月の間に1、2回程度はデータが消失するリスクがあります。 クリアまでに1時間以上かかるようなゲームであれば、セーブ機能を付けるしかないでしょう。 特に今の時代ともなれば、オートセーブが必須でしょう。

ただし、何でもかんでもオートセーブを小刻みにすればよいわけではありません。 例えばRPGで、絶対に勝てない状態から再スタートしたならば、プレイヤーは対策がないまま全滅を繰り返すしかありません。 将棋でいう「詰みの状態に入るより前」から再開できないと、やり直しようがないのです。 もし、20時間もプレイしたところでこの手の「セーブハマリ」に直面したら、プレイヤーの落胆は深く苦しいものになるでしょう。 哀しみは怒りへと変わり、「二度とやるかボケッ!」と吐き捨てて、アンイストールするにちがいありません。

あらゆる変数が、どのタイミングで初期化され、どのタイミングで更新され、どのタイミングで保存されるのか? と そこまで考えた上で設計するのが理想的なのだろうが、そんな細かいことできるわけねぇっ、と思わずにはいられません。

メモリーとヒープ

元々「男男女ダンジョン物語」の頃は、Activity の finish() をやりまくっていた。 これによって、オートセーブが実施される拠点にいるとき以外(ダンジョン探索中、会話イベント中、戦闘中)に 中断してしまうと、また拠点からやり直しというコトになっていた。 一回の旅路が5~10分程度で終わるものだから、それでもいいやと考えていたが、 いざ、中断復帰システム(アクティビティーを強制終了しないやり方)に切り替えたら、中断→再開がサクッとできるのはスゴイし便利だと思った。

finish() しまくるやり方は、プログラム技術が未熟なため、そうしていた面が大きいが、 メモリの解放という意味合いも無いわけではなかった。 メモリが不足してきた際に、自動的にアクティビティーを強制終了させると先ほど書きましたが、 これって「Aのアプリを実行中に、Bアプリの(そんなにいらない)アクティビティーを終了させる」ということではありません。 いや、そういうケースもあるのかもしれませんが、 「Aのアプリを実行中に、メモリが不足したのでAのアプリが強制終了する」ということが起こりうるのです。 いわゆる "out of memory" ですね。 これは、一般ユーザーの方も多くが経験をしていることでしょう。 一般ユーザーはエラーで落ちた場合、何が原因なのかまでは把握することはできませんが、 間違いなく「アプリがバグる原因BEST3」に入っている因子です。

今回、「アタックダンジョン MMF」では臨機応変に中断→再開ができる仕様を目指して、アクティビティーを殺さない「不殺」主義を貫きました。 しかし、開発が進展してテストでプレイする量が長くなってくると、、、早くも「アウトオブメモリー」による強制終了が出たあぁ! 困ったぁ。 largeHeap という技を使えば解決できるのですが、メモリの使用量を自力で抑える努力も必要だとのことで、なるほど、なるほど。 秘奥義「羅味曾父(らあじひいぷ)」を使わずに、現場の技術で何とかなるまいか? こんな開発半ばで秘奥義に頼るようでは、今後必ず追いつめられる…。
で…。
結局、アクティビティーが遷移するタイミングで用済みのアクティビティーをキッチリ終了させてやった方がメモリは解放できそうです。 実際にテストしたところ、finish() を多様するほど「アウトオブメモリー」を回避できるという結果が得られました。 というわけで「不殺」の誓いを早くも破る方向で、アクティビティーを殺しにかかります。

「ちょっと待てよ!」と、心の中から反論が飛んできます。 臨機応変に中断→再開ができる仕様を目指してたんじゃないのかと? そう、そうなんだけど…。 ただ、これは意外と簡単に解決できまして。 要するに onPause() 内で、特定の条件を満たした場合だけ finish() するように if で分岐させます。 特定の条件とは、正常なルーチンの流れでアクティビティーが遷移するときです。 だから、ホームキーを押して強制的に中断した場合などは onPause() が呼び出されても、finish() は実行されません。 あくまで、次のアクティビティーに切り替わったときだけ、finish() させるという構造です。 これによって「アウトオブメモリー」を回避しつつ、中断→再開も自由自在という無敵仕様が完成しました。 やればできるじゃん。

0 件のコメント:

コメントを投稿