Androidのライフサイクルからアプリ設計を見直してみる


はじめに

Androidでクラッシュしにくいアプリを作るにはどうしたらよいか?
ということを考察したいと思います。
というのはこの辺の設計ノウハウに関しては
基本なはずで大事なのに残念なことにまとまっている書籍とかあまり見かけないため
先人達の知恵をお借りしつつ、私のAndroid開発経験上に基づいて備忘録として残しておきます。
ほんとは初級者向けに書きたかったのですが、実装経験やAndroid内部がある程度よくわかってないとわからないと思うので難易度高めになってしまいました。
間違ってるとか改善点があればコメント頂ければ幸いです。

この記事で紹介してるライブラリとかの導入方法はこちらも参考に
最近のAndroidネイティブ開発まとめ(2017年始版)

よくあるクラッシュ(アプリが落ちる)

実運用ではCrashlyticsでクラッシュログを取ることが多いと思います。
クラッシュログを取ると浮き彫りになってくるのは
以下のクラッシュが発生することがとても多いです。

1.NullPointerException
2.IllegalStateException

1.の原因として多いのは、単純にnullパラメータの変数がメソッドに渡されて落ちるというわけではなく、ライフサイクルですでに死んでいるはずのActivityやFragment内の通信処理などの非同期処理が動いていて、非同期処理実行時にすでに死んでいるActivityやFragmentのViewを操作しようとしてしまう場合(ActivityContextやViewがnull状態)によく発生します。
それ以外の単純なnullチェック忘れは@Nullable@NonNullアノテーションをつけるか
参考:脱ビギナー!Androidのnullな話

もしくはOptinalを導入してnullチェックを明示的にします。
参考:Android で Optional を使おう

2.の原因はFragmentのライフサイクルに起因します。
これは非同期処理などでFragmentの操作タイミングがonSaveInstanceStateメソッドよりも後になってしまった場合などに発生します。
この原因はとても複雑でそもそもFragmentを使うべきかという議論にもなると思うので後述します。(FragmentTransactionのcommitが非同期処理だということとActivityとFragmentでライフサイクルが2つあり、とても複雑だということ)

1.2.の原因の詳細は下記リンクにとてもわかりやすくまとまってます。(必読)
参考:絶対落ちないアプリの作り方

ライフサイクル考察

Activity、Fragmentのライフサイクルについて

ライフサイクルのメソッドの詳細は下記参考にしてください
参考:知らずに作って大丈夫?Androidの基本的なライフサイクルイベント31選
ライフサイクルのフローの詳細は下記リンクを参考にしてください
参考:Activity のライフサイクル再確認

まず、Androidのライフサイクルは次のようになっています。

Fragmentのライフサイクルは次のようになってます。
Fragmentに関してはFragmentTransactionクラスのaddメソッドを呼ぶか(動的生成)
fragmentタグで生成されるため(静的生成)、生成フローは2パターン存在します。

ActivityやFragmentはportraitモードをしてない、画面を回転するだけで死にまくります。(ActivityのonCreateから再度呼ばれる)
また厄介なのは、他のアプリをたくさん起動してメモリが足りなくなった場合等にもOS側でActivityがDestroyされます。

そのため、onSaveInstanceStateメソッドで保持したい変数を一時保存する必要があります。

Activity、Fragment、Viewの状態保持に関しては下記リンクを参考にしてください
状態保持に関してはIcepickというライブラリを用いた方が楽だと思います。

個人的にEditTextなどの入力中データ以外は
モデルデータクラス化してApplicationクラスのような死なない領域に保持したほうが無難だと思います。(後述)

Contextについて

Androidにおいて至る所でActivity、Fragment、Viewなどの引数として必要になってくるのがContextという神オブジェクトです。
神オブジェクトはメソッドや変数をたくさん持ってるオブジェクトで依存度が大きいオブジェクトです。
Androidでは2種類のContextが存在します。(正確には3種類なのですがBaseContextはほとんど使われないのでここでは除外します。)

・ApplicationContext
・ActivityContext

厄介なことにContextインタフェースでどちらのインスタンスかわかりずらい上、
大抵の場合、どちらを引数に渡しても動いてしまったりします。
ApplicationContextとActivityContextの違いに関しては
まず、styles.xmlのアプリテーマが参照されるか、AndroidManifest.xmlのActivityに設定したテーマが参照されるかという違いがあります。
次のリンクがわかりやすいです。
参考:Android:引数はthisか?getApplicationContextか?ActivityとApplicationの違い

ApplicationContextは基本的にアプリが死ぬまでnullにはなりませんが、
ActivityContextはActivityがライフサイクルで死んだ後はnullになります。
だからと言って、常にApplicationContextの方を参照してしまうと意図しないテーマがView(標準ダイアログなど)に割り当てられたり、メモリリークの原因となります。
そのため、ライフサイクルに逆らわず、ActivityContextを使った方が無難です。
参考:ApplicationContextは使ってはいけない

ただし、getContextメソッドで取得されるActivityContextは使わないほうが無難です。
理由はライフサイクル終了後でも生き残る非同期処理内だと、いつActivityやFragmentが死ぬか不定でnullが返されるかもしれないからです。
また、ライフサイクル外の非同期処理でActivityContextが参照されるようなケースがあるというのはそもそも破綻しているので設計を見直した方がよいでしょう。

次のように弱参照で参照するとライフサイクル終了時にGCが検知して
mContextを消してくれるのでよいです。

Activity.java
private WeakReference<Context> mContext;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mContext = new WeakReference<Context>(this);
}

まとめると・・・
原則1:Activity(Fragment)ではApplicationContextではなくActivityContextを使う(ライフサイクルに逆らわらない)
原則2:ActivityContextをActivity(Fragment)より長命などこかに渡す際(非同期処理等)は設計を見直す。
原則3:ライフサイクル内でActivityContextを変数代入し、参照する際は弱参照にしないとメモリリークの原因になる。

メモリリークに関して

Androidのメモリリークの原因に関しては下記参考にしてください
(基本的に循環参照させるような場合やActivityのstatic変数をインスタンス化させるとメモリリークしやすいです。)
Eight Ways Your Android App Can Leak Memory

メモリリークの検知はLeakCanaryを導入するとよいです。

onCreateなどでインスタンス化した変数(強参照)は
onDestroyメソッドでnull代入すると良いです。

private Object obj;

@Override
public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     obj = new Object();
}

@Override
private void onDestroy(){
   obj = null;
   super.onDestroy();
}

強参照と弱参照の詳細に関しては次のリンクを参照してください。
参考:Android のメモリ管理 #4 SoftReference と WeakReference を活用する

特に画像や大きいサイズのファイル読み込みに関しては、すぐに解放しないとメモリを切迫します。
対策は下記を参考にしてください
OutMemoryErrorと戦う

モデル、通信をライフサイクルと分離する

次のリンクがものすごく参考になります。(必読)
参考:iOS/Androidアプリエンジニアが理解すべき「Model」の振る舞い

スライドの要点は次のようになっています。

  • モデルのデータはActivity、Fragmentから分離する(Activity、Fragmentはすぐ死ぬので基本死なない、Applicationクラスで保持)
  • Activity、Fragment、Viewにはモデルのデータ変更があった際に通知で変更を伝える(通信処理などの非同期処理はActivity、Fragment、Viewに直接持たせてはいけない)

この設計にするためには次のようにします。
1.モデルクラスをライフサイクルで死なないApplicationクラスに保持
2.通信処理の呼び出しは基本死なないApplicationクラスやモデルクラス内で保持
3.モデルデータの更新部、取得部はsyncronizedキーワードなどで同期を取る
4.EventBusなどのObserver構造でモデルデータ変更時に通知を生きてるActivity、Fragmentに一斉送信し、Viewを更新する

Fragmentを使うべきか、Activity+CustomViewで代用するのか?

Activity、Fragmentを両方使うとライフサイクルが2つ存在することになります。
しかも、Activityが死ぬのは不定なため、デバッグの困難性に拍車をかけます。
そこでFragmentを使わずにCustomViewで代用できるのでは?という設計案が出ています。
ただし、FragmentライクなCustomViewの自作実装はとても困難で
正直、どの場合でも通用するベストプラクティスは現状無いため、ここでは紹介程度にとどめておきます。

参考:Fragments vs. CustomViews に一つの結論を出してみた

Fragmentを捨てて、Activity+CustomViewで代用するパターン

Dagger2やRetrofit、LeakCanaryなどの有名ライブラリを生み出しているSquare社がFragmentを捨てるという選択肢に出ています。
参考:【翻訳】Android Fragmentへの反対声明

このパターンで作る場合、下記に関して実装する必要があるため、実装難易度は極めて高いです。

  • CustomViewの状態保存の機構(onSaveInstanceState、onRestoreInstanceState)
  • CustomViewの画面遷移アニメーション
  • CustomViewのバックスタックの仕組み

そのため、SquareではFlowやMortorライブラリを開発して実装しています。
実装に関しては下記が詳しいです。
AndroidアプリのSquare風MVP仕立て 〜Dagger 2をそえて〜

Fragmentも使って頑張るパターン

下記対策はFragmentのIllegalStateExceptionの原因が処理順番の問題なのであれば、処理順番をずらしてしまえばいいのではという割とごり押しな(ある意味正当な?)手法です。

参考:Fragment使用時のIllegalStateException回避

FragmentライクなCustomView自作はしんどいので
Activity, Fragment, CustomView の使い分けすればいいじゃないかという設計は下記参考になります。

参考:Activity, Fragment, CustomView の使い分け - マッチョなActivityにさよならする方法 -