生まれた設計パターン
TechBooster
著
Android
実践プログラミング ∼現場で生まれた設計パターン∼を手に取っていただき、あ りがとうございます。本書はAndroid
開発における設計上の課題をいかに解決してきたか、 という現場の知恵と工夫をあつめています。仕様を検討し、設計し、コードで実践するまでの 考え方が詰まった一冊です。Android
における設計パターンの一例として、参考になれば幸い です。(
発行代表@mhidaka)
本書の内容について
•
アプリ開発で出会った困ったコード15
選•
設計ガイドラインの思想と効果的な実装• Gradle
プロジェクト設定の逆引きリファレンス•
気になる周辺技術Docker
の今と未来を解説TechBooster
とは
TechBooster
はAndroid
をはじめとしたモバイルのための技術サークル*1です。オープン ソースへの貢献や社会還元を目的にサイトでモバイル技術を解説しています。お問い合わせ先
本書に関するお問い合わせhttps://plus.google.com/+TechboosterOrg/
はじめに
1
本書の内容について. . . .
1
TechBooster
とは. . . .
1
お問い合わせ先. . . .
1
第1
章 駄目コードとその対策6
1.1
Activity
間のデータのやりとりがstatic
変数で行われている. . . .
6
1.2
無名クラスのOnClickListener
がそこら中で生成されている. . . .
9
1.3
Handler
がそこら中で生成されている. . . .
13
1.4
ポーリングでしか状態を知ることができないスレッドがある. . . .
16
1.5
try-catch
が乱暴にException
で受けている. . . .
21
1.6
Thread
が回収できなくなって仕方なくkillProcess
している. . . .
22
1.7
static
フィールドに状態をもつオブジェクトが入っている. . . .
25
1.8
内部の処理が微妙に異なるAsyncTask
が無数にある. . . .
28
1.9
Preference
への書き込みに個々でedit
メソッドを呼んでいる. . . .
33
1.10
Application
にService
の実体を突っ込んでいる. . . .
38
1.11
OutOfMemoryError
避けが悪影響だけ出している. . . .
42
1.12
カスタムApplication
が本来あるべき振る舞いをしない. . . .
50
1.13
xml
を使わずにコードでView
を延々と生成している. . . .
51
1.14
synchronized
がそこら中にある. . . .
52
1.15
new
を嫌ってローカル変数をフィールドに追いやってる. . . .
52
1.16
おわりに. . . .
53
第
2
章Android100
機種サポートしたいけど10
機種しか試験するお金がありませ んでした54
2.1
はじめに. . . .
54
2.2
ケーススタディ. . . .
55
第3
章7
つのケース60
3.1
縦向き(portrait
)固定でいい. . . .
61
3.2
minSdkVersion
はなるべく高くしよう. . . .
61
3.3
DialogFragment
は素人は諦めよう. . . .
64
3.4
PreferenceActivity
は古い使い方でいい. . . .
65
3.5
ぼっちにContentProvider
は要らない. . . .
68
3.6
「タブレット向け」ではなく「大きな画面向け」で. . . .
69
3.7
下タブはタブじゃないと思いこもう. . . .
70
3.8
そうは言っても、原則には従おう. . . .
73
第4
章UCD
に沿ったアプリ設計の実践 ∼PlayStore
で☆1
をつけられないために∼74
4.1
序論. . . .
74
4.2
UCD
という考え方. . . .
75
4.3
UCD
の実践. . . .
76
4.4
スマートフォンアプリ設計での実践. . . .
79
4.5
結論. . . .
81
第5
章Android Gradle
プロジェクト逆引きリファレンス84
5.1
はじめに. . . .
84
5.2
applicationId
を変更したい. . . .
86
5.3
minSdkVersion
をbuild.gradle
から指定したい. . . .
87
5.4
targetSdkVersion
をbuild.gradle
から指定したい. . . .
88
5.5
versionCode
を算出するロジックを組み込みたい. . . .
88
5.6
一部の機能の異なるapk
を作りたい. . . .
89
5.7
Product Flavor
を追加したい. . . .
91
5.8
Build Type
を追加したい. . . .
92
5.10
メソッド数上限を回避したい. . . .
95
5.11
ProGuard
を有効化したい. . . .
97
5.12
使用していないリソースを削除してapk
を小さくしたい(1
). . . .
98
5.13
使用していないリソースを削除してapk
を小さくしたい(2
). . . .
99
5.14
RenderScript Support Library
を使いたい. . . 101
5.15
ABI
ごとに別のapk
を作りたい. . . 102
5.16
density
ごとに別のapk
を作りたい. . . 104
5.17
ビルドタイプ/
フレーバー固有の依存ライブラリを追加したい. . . 106
5.18
ビルドバリアント固有の依存ライブラリを追加したい. . . 106
5.19
推移的依存関係を管理したい(依存ライブラリの除外). . . 107
5.20
ライブラリプロジェクトをプロジェクトルートの外に置きたい. . . 108
5.21
ライブラリプロジェクトをデバッグビルド連動させたい. . . 109
5.22
Build Type
ごとに異なる値をAndroidManifest
に書きたい. . . 111
5.23
BuildConfig
に定数フィールドを追加したい. . . 113
5.24
リリース証明書の設定を外出ししたい. . . 115
5.25
デバッグ証明書をリポジトリに入れて共有したい. . . 117
5.26
Android Wear
アプリを追加したい. . . 117
5.27
すべてのビルドバリアントに対して処理を書きたい. . . 119
5.28
lint
の設定を記述したい. . . 119
5.29
assets
内の特定のファイルを除外したい. . . 121
5.30
コンパイル済みのjni
ライブラリ(.so
)を取り込みたい. . . 122
5.31
NDK
で*.c
や*.cpp
ファイルをコンパイルしたい. . . 122
5.32
依存ライブラリの重複ファイルを除外/
一つだけ取り込みたい. . . 124
5.33
apk
ファイルのファイル名を変えたい. . . 125
5.34
IDE
からのビルドを判別したい. . . 126
5.35
ビルドサーバでのビルドを高速化したい. . . 126
5.36
Gradle
ラッパーのバージョンを更新したい. . . 127
5.37
ProGuard
のマッピングファイルのパスを取得したい. . . 129
5.38
おわりに. . . 130
第6
章Docker
が切り開くソフトウェア開発の未来131
6.1
仮想化. . . 131
6.2
VM
とコンテナに基づくアプローチの違い. . . 132
6.3
コンテナのパフォーマンス. . . 134
6.4
Docker
が採用されるのはなぜか. . . 134
6.5
Docker
コンテナを用いた一例. . . 135
6.6
VM
とDocker
コンテナの住み分け. . . 136
6.7
Docker
を活かすソフトウェア設計. . . 137
6.8
マイクロサービスを阻む技術上の課題. . . 140
6.9
マイクロサービスを阻む管理上の課題. . . 141
6.10
コンテナ管理サービスの夜明け. . . 142
6.11
クラウド事業者のコンテナ戦略. . . 144
6.12
Docker
プラットフォームの未来. . . 146
6.13
Docker
後の世界. . . 149
6.14
最後に. . . 151
著者紹介152
駄目コードとその対策
Android
アプリケーション開発ではいろいろな要素を取り扱います。View
のイベントの扱 い方やマルチスレッド、Handler
では少々小難しいコードの書き方が必要になります。それ らのコードの書き方を押さえず、そのままAndroid
アプリケーションとして実装するとコー ドが読み難くなったり解りづらくなったりします。ここではそれらのコードの書き方で、どの ような問題が出て、どうすれば改善するのかを紹介します。 なお、この章で挙げている駄目な例はフィクションではありません。1.1
Activity
間のデータのやりとりが
static
変数で行われて
いる
1つの画面で完結するアプリケーションは少なく、通常、アプリケーションは複数の画面を 持ちます。1つの画面から別の画面へ遷移するとき、いくつかのパラメーターを引渡します。 パラメーターの中身は入力内容であったり、ユーザーの情報であったり、アプリ固有の状態で あったり多岐にわたります。Android
では1つの画面はActivity
によって構成され、パラメー タはIntent
で与えるのが慣習です。 駄目なコードの例として、画面間のパラメーターをstatic
変数で渡している例を見てみま しょう。以降の例ではパラメータにリスト1.1
のデータクラスを使用します。 リスト1.1: UserModel.javapublic class UserModel implements Serializable { public enum Role {
PROGRAMMER, DESIGNNER, MANAGER }
private long id;
private String username; private Role role;
/* GetterとSetterは省略 */ }
このデータクラスをリスト
1.2
のFirstDameActivity
からリスト1.3
のSecondDameAc-tivity
への遷移する場合を見てみましょう。リスト1.2: 悪い例FirstDameActivity.java
public class FirstDameActivity extends Activity implements View.OnClickListener {
private UserModel mUserModel; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button1).setOnClickListener(this); // mUserModelを作る /* 省略 */ } @Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
// パラメータを設定
SecondDameActivity.sUserModel = mUserModel;
Intent intent = new Intent(this, SecondDameActivity.class); startActivity(intent);
} } }
リスト1.3: 悪い例SecondDameActivity.java
public class SecondDameActivity extends Activity { public static UserModel sUserModel;
private UserModel mUserModel; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // static 変数からパラメータを受け取る mUserModel = sUserModel; } } リスト
1.2
のFirstDameActivity#onClick
メソッド内でパラメータとしてmUserModel
をSecondDameActivity.sUserModel
に代入しています。一見このコードは動作するように見 えますが、実際動作します。しかしいくつかの問題の原因を抱えています。Android
のプロセスはメモリ不足などの理由で破棄されるため、破棄されたタイミングでSecondDameActivity.sUserModel
がクリアされ、再度起動した際にsUserModel
から値が 取れないという現象を起こします。またSecondDameActivity
を2回目に起動するときにsUserModel
の設定を忘れると古いsUserModel
がそのまま使われるという挙動も起こり ます。 これらの他にも問題があるので、正しくAndroid
の手法に書き換えましょう。書き換えた コードがリスト1.4
とリスト1.5
になります。 リスト1.4: 良い例FirstRefineActivity.javapublic class FirstRefineActivity extends Activity implements View.OnClickListener {
private UserModel mUserModel; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button1).setOnClickListener(this); // mUserModelを作る /* 省略 */ } @Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
Intent intent = new Intent(this, SecondRefineActivity.class); // パラメータを設定
intent.putExtra(SecondRefineActivity.EXTRA_USER_MODEL, mUserModel); startActivity(intent); } } } リスト1.5: 良い例SecondRefineActivity.java
public class SecondRefineActivity extends Activity {
public static final String EXTRA_USER_MODEL = "user_model"; private UserModel mUserModel;
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// Intent からパラメータを受け取る
mUserModel = (UserModel) getIntent()
.getSerializableExtra(EXTRA_USER_MODEL); } } リスト
1.4
でIntent#putExtra
メソッドでIntent
の追加情報としてパラメータを渡すよう にしています。そしてリスト1.5
でその追加情報を取り出しています。Intent#putExtra
メソッドでIntent
の追加情報としてパラメータを渡す方法は面倒に見える こともあります。しかしその面倒なことの代わりに、不意なプロセスの破棄に対応したり、再 起動時の対応をフレームワークが行なってくれるようになります。特にこのケースは開発でActivity
を作る際に一番最初に触るところでありながら、トラブルが発生するのは開発の終盤 であるため、最初からフレームワークに合わせた形で実装するようにしましょう。1.2
無名クラスの
OnClickListener
がそこら中で生成されて
いる
ボ タ ン の よ う なView
が ク リ ッ ク さ れ た と き の イ ベ ン ト を 扱 う に は 、皆 さ ん お 馴 染 み のOnClickListener
を 使 用 し ま す 。OnClickListener
の イ ン ス タ ン ス をView#setOnClickListener
メソッドで設定していると思いますが、OnClickListener
のイ ンスタンス自体はどのようにして生成しているでしょうか。リスト
1.6
はActivity#onCreate
内にbutton1
とbutton2
について、それらの押されたと きの処理をすべて書き込んだ場合のコードです。リスト1.6: 悪い例ManyOnClickListenerDameActivity.java
public class ManyOnClickListenerDameActivity extends Activity { private EditText mEditText1;
private EditText mEditText2; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_many_handlers); // button1の押されたときの処理を設定する findViewById(R.id.button1).setOnClickListener( new View.OnClickListener() { @Override
public void onClick(View v) {
mEditText1.setText("button1 clicked!"); } }); // button2の押されたときの処理を設定する findViewById(R.id.button2).setOnClickListener( new View.OnClickListener() { @Override
public void onClick(View v) {
mEditText2.setText("button2 clicked!"); }
});
mEditText1 = (EditText) findViewById(R.id.editText1); mEditText2 = (EditText) findViewById(R.id.editText2); }
}
リスト
1.6
のActivity#onCreate
内で行われていることを整理してみましょう。1. OnClickListener
をbutton1
とbutton2
に設定する2. OnClickListener
のインスタンスを生成するリスト
1.6
の中では、これら3つのことをActivity#onCreate
メソッドの中ですべて行なっ ています。しかしActivity#onCreate
メソッドは本来Activity
の初期化や生成についての処 理を行なうものであり、これら3つがすべて入っているため読みにくくなっていないでしょう か。特に3.
の処理の定義は行数が増えやすく、onCreate
内に書くとメソッドの記述が長く なり、コードが読みにくくなります。リスト
1.7
では、OnClickListener
をAcitivity
にimplements
することで、2.
のインスタ ンスの生成を不要にしています。また3.
の処理の定義をActivity#onClick
メソッドに分離さ せています。リスト1.7: 良い例ManyOnClickListenerRefine1Activity.java
public class ManyOnClickListenerRefine1Activity extends Activity implements View.OnClickListener {
private EditText mEditText1; private EditText mEditText2; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_many_handlers); // OnClickListenerの設定
findViewById(R.id.button1).setOnClickListener(this); findViewById(R.id.button2).setOnClickListener(this); mEditText1 = (EditText) findViewById(R.id.editText1); mEditText2 = (EditText) findViewById(R.id.editText2); }
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
mEditText1.setText("button1 clicked!"); } else if (v.getId() == R.id.button1) {
mEditText2.setText("button2 clicked!"); } }} リスト
1.7
のようにボタンが押されたときの処理を分離させることで、Activity#onCreate
をがスッキリしました。Activity#onCreate
内では画面のView
要素の初期化をすることが多 く、画面のView
が増えればその分だけ大きくなることが多いです。このためView
が押されたときの処理を記載していくと、どんどん大きくなってしまい、可読性が低下します。これを 避けるにはインスタンスの生成、設定、処理の定義などやることが複数ある場合は、それらの コードを可能な限り別の場所にまとめて書くようにし、メソッドが肥大化しないようにしま しょう。
インスタンスの生成について
OnClickListener
をActivity
にimplements
するのが嫌な場 合、メンバ変数にしてしまう手がありますのでこちらも紹介しておきます(リスト1.8
)。リスト1.8: 良い例ManyOnClickListenerRefine2Activity.java
public class ManyOnClickListenerRefine2Activity extends Activity { private EditText mEditText1;
private EditText mEditText2; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_many_handlers);
findViewById(R.id.button1).setOnClickListener(mOnClickListener); findViewById(R.id.button2).setOnClickListener(mOnClickListener); mEditText1 = (EditText) findViewById(R.id.editText1);
mEditText2 = (EditText) findViewById(R.id.editText2); }
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (v.getId() == R.id.button1) {
mEditText1.setText("button1 clicked!"); } else if (v.getId() == R.id.button1) {
mEditText2.setText("button2 clicked!"); }
} }; }
リスト
1.8
はOnClickListener
にActivity
にimplements
する代わりに、メンバ変数とし てOnClickListener
を生成して保持しています。もしActivity
の規模が大きくなりActivity
ける手があります。 ところで
onClick
メソッドの中に直接処理を書くことについてはどうでしょう。厳密にい えば、あくまでonClick
メソッドはイベントのハンドリングだけにすべきかもしれません。し かし数行で終わる簡単な処理の場合は直接onClick
メソッド内に書いたほうがわかりやすい かも知れませんね。この辺りは処理の長さに合わせてケースバイケースで判断するようにし ましょう。1.3
Handler
がそこら中で生成されている
Handler
クラスはAndroid
のスレッド間の処理のやりとりの肝となるクラスです。別ス レッドから処理や結果を引き渡すときや、一定時間後に処理を実行したいときなどに使用しま す。便利なクラスではあるのですが、Handler
を闇雲に生成すると、コードが煩雑になり可読 性が低下します。 ボタンが押されたとき、一定時間後に処理を行なう処理を例に説明します。リスト1.9
はbutton1
とbutton2
がクリックされると、5000ms
後にそれぞれeditText1
とeditText2
に文 字列を出力する処理の実装です。リスト1.9: 悪い例ManyHandlersDameActivity.java
public class ManyHandlersDameActivity extends Activity implements View.OnClickListener {
private EditText editText1; private EditText editText2; @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
// その場でHandlerを生成して5000ms後に処理を実行させる new Handler().postDelayed(new Runnable() {
@Override
public void run() {
editText1.setText("button1 clicked!"); }
}, 5000);
// その場でHandlerを生成して5000ms後に処理を実行させる new Handler().postDelayed(new Runnable() {
@Override
public void run() {
editText1.setText("button2 clicked!"); } }, 5000); } } } 見てのとおり、その場で
Handler
を生成し、postDelayed
で5000ms
後に処理が実行すべ き処理であるRunnable
を渡しています。これは乱暴ですね。Handler
は丁寧に扱うように しましょう。 リスト1.10
ではHandler
の生成回数を抑えるため、クラス変数(static
変数)として生成 しています。 リスト1.10: 良い例 ManyHandlersRefine1Activity.javapublic class ManyHandlersRefine1Activity extends Activity implements View.OnClickListener {
private EditText editText1; private EditText editText2;
private static Handler sHandler = new Handler(); @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
sHandler.postDelayed(new Runnable() { @Override
public void run() {
editText1.setText("button1 clicked!"); }
}, 5000);
} else if (v.getId() == R.id.button2) { sHandler.postDelayed(new Runnable() {
public void run() { editText1.setText("button2 clicked!"); } }, 5000); } } } 少しマシになりました。このままでも特に問題は無いのですが、
Runnable
のインスタンス をボタンを押すたびに生成しているのが気になるかも知れません。これはメッセージを送る ようにすることで、リスト1.11
のような書き方にすることができます。 リスト1.11: 良い例 ManyHandlersRefine2Activity.javapublic class ManyHandlersRefine2Activity extends Activity implements View.OnClickListener {
private static final int EVENT_BUTTON1_CLICKED = 1; private static final int EVENT_BUTTON2_CLICKED = 2; private static Handler sHandler = new Handler() {
@Override
public void handleMessage(Message msg) { super.handleMessage(msg);
ManyHandlersRefine2Activity activity
= (ManyHandlersRefine2Activity) msg.obj; if (msg.what == EVENT_BUTTON1_CLICKED) {
activity.editText1.setText("button1 clicked!"); } else if (msg.what == EVENT_BUTTON2_CLICKED) {
activity.editText2.setText("button2 clicked!"); }
} };
private EditText editText1; private EditText editText2; @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
Message msg =
sHandler.obtainMessage(EVENT_BUTTON1_CLICKED, this); sHandler.sendMessageDelayed(msg, 5000);
} else if (v.getId() == R.id.button2) {
// メッセージを作成してsHandlerに渡し、処理自体はsHandlerに任せる Message msg = sHandler.obtainMessage(EVENT_BUTTON2_CLICKED, this); sHandler.sendMessageDelayed(msg, 5000); } } } リスト
1.11
ではリスト1.10
と異なり、sHandler
へはメッセージを送るだけで、editText1
やeditText2
への処理自体はsHandler
に記述しています。メッセージを送るだけなので毎回Runnable
を生成する必要がなくなっています。Handler
クラスは便利な反面、UI
スレッドと強い関係を持つため、迂闊に生成するとメモ リリークの原因になるなど、危険な側面もあります。メモリリークは開発中には気づきにく く、リリース間際になって発覚して大変なことになりやすいので注意が必要です。闇雲に生成 したりせず、コードの書き方をあらかじめ整理して開発を進めるようにしましょう。1.4
ポーリングでしか状態を知ることができないスレッドが
ある
長い処理はUI
スレッドで行うべきではないのはよく知られています。しかし別スレッドで 処理を行うとき、処理が終わったことをどのようにしてUI
スレッドに通知しているでしょ うか。 たとえばbutton1
を押したとき、長い処理の結果をeditText1
に表示する処理を考えてみま しょう。リスト1.12
では、別に生成したThread
で処理を行い、処理が終わるとtaskFinished
に終わったことを書き込み、taskResult
には処理の結果を書き込んでいます。そしてUI
ス レッドは1000ms
ごとにtaskFinished
がtrue
になることを確認し、true
になると結果をeditText
に表示しています。public class PollingDameActivity extends Activity implements View.OnClickListener {
private int taskResult; // 値の受け渡しのための変数
private boolean taskFinished; // 処理が終わったか判定するための変数 private EditText editText1;
private EditText editText2;
private static Handler sHandler = new Handler(); @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
taskFinished = false;
// 別スレッドで行なう処理の本体 new Thread() {
@Override
public void run() { super.run();
int result = 0;
{ // 長い処理のつもり
for (int i = 0; i <= 100; i++) { result += i; } } // 処理が終わったことを変数に書き込む taskResult = result; taskFinished = true; } }.start();
Runnable runnable = new Runnable() { @Override
public void run() {
// 処理が終わっているか確認、まだなら1秒後に再確認する if (taskFinished) { editText1.setText(String.valueOf(taskResult)); } else { sHandler.postDelayed(this, 1000); } }
}; sHandler.postDelayed(runnable, 1000); } } } このコードの駄目なところを整理してみましょう。
1.
別スレッドの処理の完了と結果を保持するためだけにtaskFinished
とtaskResult
と いうメンバ変数が追加されている2. 1000ms
ごとにUI
スレッドが処理の完了を確認しているが早く終わっても必ず1000ms
待ってしまう3.
仮に待ち時間を100ms
や10ms
すると、今度は確認回数が増えるため、CPU
使用率 が増加してしまう これらの他にもThread
をnew
で生成して実行しているため、後からハンドリングできな くなっている問題もあります。このコードの駄目なところを単純に修正するとリスト1.13
よ うなコードになります。 リスト1.13: 良い例 PollingRefine1Activity.javapublic class PollingRefine1Activity extends Activity implements View.OnClickListener {
private EditText editText1; private EditText editText2;
/** UIスレッドに処理を移管させるためのHandler */ private static Handler sHandler = new Handler(); @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
// 別スレッドで行なう処理の本体 new Thread() {
public void run() { super.run();
int result = 0;
{ // 長い処理のつもり
for (int i = 0; i <= 100; i++) { result += i;
} }
// UIスレッドで行なうべき処理を作る final int finalResult = result;
Runnable runnable = new Runnable() { @Override
public void run() {
// 結果をeditText1に出力する editText1.setText(String.valueOf(finalResult)); } }; sHandler .post(runnable); // sHandler経由でUIスレッドで実行 // runOnUiThread(runnable); // 代わりにこのメソッドも使える } }.start(); } } } この処理では
editText1
への出力処理をsHandler
を経由してUI
スレッドに実行させてい ます。ここではsHandler.post
としていますが、Activity#runOnUiThread
メソッドを使うこ とでもUI
スレッドに処理を実行させることができます。これにより先に挙げた駄目なところ が改善されました。 実はこの処理、Android
の標準ライブラリであるAsyncTask
クラスを使用することで、 もっと読みやすくすることができます。AsyncTask
クラスを使用するように置き換えたもの がリスト1.14
になります。 リスト1.14: 良い例 PollingRefine2Activity.javapublic class PollingRefine2Activity extends Activity implements View.OnClickListener {
private EditText editText1; private EditText editText2;
@Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
AsyncTask<Void,Void,Integer> task
= new AsyncTask<Void, Void, Integer>() { @Override
protected Integer doInBackground(Void... params) { // バックグラウンドで行われる
int result = 0;
{ // 長い処理のつもり
for (int i = 0; i <= 100; i++) { result += i; } } return result; } @Override
protected void onPostExecute(Integer result) { // UIスレッドで行われる editText1.setText(String.valueOf(result)); } }; task.execute(); } } }
AsyncTask
のdoInBackground
メソッドは別スレッドで実行されますが、onPostExecute
は
UI
スレッド上で実行される仕様になっています。このためdoInBackground
に時間の掛 かる処理を書き、onPostExecute
にUI
スレッドで行なうべき結果の出力処理を書くことで、 どれが別スレッドでどれがUI
スレッドで実行されるかを明確にすることができます。 リスト1.12
のように、taskFinished
とtaskResult
といった変数を用いる場合、別スレッ ドで行なうべき処理が増えた際にこれらの変数も一緒に増えてしまい、ソースの可読性の低下 や誤って別の変数をみるなどしてバグの原因となってしまいます。 別スレッドの処理結果をUI
スレッドに引き渡さなければならないときは、AsyncTask
を使うようにしましょう。
1.5
try-catch
が乱暴に
Exception
で受けている
Java
では例外の処理を取り扱うのにException
クラスを使用します。たとえばInte-ger#parseInt
メソッドでString
型の値をint
型へ変換するとき、String
が数値として認識で きない場合、NumberFormatException
がスローされます。また通信処理の多くでは、通信 の失敗時にIOException
がスローされます。リスト
1.15
の例はEditText
から受け取った文字列を数値に変換する場合の駄目なパター ンです。リスト1.15: 悪い例 ExceptionDameActivity.java
EditText editText1 = (EditText) findViewById(R.id.editText1); int intValue = -1; try { intValue = Integer.parseInt(editText1.getText().toString()); } catch (Exception e) { } if (intValue == -1) {
Log.d("Hoge", "Bad input."); // ここでエラーの場合の処理を行なう } else { // ここで正常な場合の処理を行なう } この書き方の駄目な箇所を整理しましょう。
1. try-catch
をException
で受けている2. -1
が入力された場合、エラーとして扱われる このコードの書き方では、1.
のとおりException
で受けているため、editText1
が存在せずnull
になるような場合でもエラーとして扱われてします。また、ユーザーが"-1"
を入力したと き、変数intValue
が-1
か否かでエラーを判定しているため、エラーとして判定されてしまい ます。 これらの問題を改善した実装がリスト1.16
になります。editText1
が存在せずnull
になる 場合は正しくNullPointerException
になりますし、ユーザーが"-1"
を入力したときも誤ってエラーと判定されることもありません。前述の問題が解決した上に、コード量が少なくなって いることがわかります。
リスト1.16: 良い例 ExceptionRefineActivity.java
EditText editText1 = (EditText) findViewById(R.id.editText1); try {
int intValue = Integer.parseInt(editText1.getText().toString()); // ここで正常な場合の処理を行なう
} catch (NumberFormatException e) { Log.d("Hoge", "Bad input.");
// ここでエラーの場合の処理を行なう } ここで紹介した例はどちらかというと
Android
というよりJava
のお作法の問題になりま す。Android
の標準フレームワークもJava
の作法に則って作られているため、例外処理につ いても同じように作法を守って実装することでフレームワークがよいようにしてくれます。 特に例外処理は乱暴にException
でキャッチしたりすると他のバグと混ざって解決が困難な ことになるので、日頃から作法を守ったコードを書くようにしましょう。1.6
Thread
が回収できなくなって仕方なく
killProcess
して
いる
時間の掛かる処理はUI
スレッドで実行するのは好ましくないため、AsyncTask
やThread
で別スレッドにします。また、ものによってはバックグラウンドで動き続けることを前提に するものもあると思います。アプリの動作中にバックグラウンドで動き続けるものがあると き、どうやってそれを止めているでしょうか。動き続けるからいいやと止めることを考えてな かったり、プロセスをkill
することで強引に止めていないでしょうか。 たとえばサービスが一つのスレッドを持っている場合を考えてみましょう。リスト1.17
は バックグラウンドで動き続けるスレッドを一つ持っています。Service#onCreate
メソッド 内で直接new
で生成し、その場でstart
メソッドを読んで実行しています。 リスト1.17: 悪い例 KillProcessDameService.javapublic class KillProcessDameService extends Service { @Override
public IBinder onBind(Intent intent) { /* 省略 */
}
@Override
public void onCreate() { super.onCreate();
new Thread() { @Override
public void run() { super.run(); while(true) { // do some thing } } }.start(); } @Override
public void onDestroy() { super.onDestroy(); Process.killProcess(Process.myPid()); } } こう なる と プロ グラ ム から 止め る こと は 最 早 でき ませ ん 。唯一 残 さ れ た手 段は
Ser-vice#onDestroy
に書かれているProcess#killProcess
です。サービスの終了時にこのメ ソッドによってプロセスをkill
することでスレッドを停止させています。どのみちサービスを 終了するのはアプリケーションの終了と同じだから問題が無いように思われるかも知れませ んが、次のような副作用があります。1.
サービスの終了がプロセスがkill
を意味するため、アプリケーションの設計上サービス の停止と再開ができなくなる2. JUnit
の実行時にService#onDestroy
が呼ばれると、その場でJUnit
が停止してしま うため、事実上JUnit
が使いものにならなくなる3. Process.killProcess
を呼ばないようにしてサービスを再起動すると、古いスレッドが 残ったままになるこのようなことにならないように、この手のスレッドはサービスの生存期間に内包するべき です。このためスレッドを作成する際は、何がスレッドの開始と停止の責務を負い、どの期間 に動作するかを意識するようにしましょう。この例ではサービスが開始と停止の責務を負い、 サービスの生存期間中に動作するようにすべきです。
リスト
1.18
ではService#onCreate
内でThread
を作成していますが、Thread#interrupt()
を呼ぶと停止するようになっています。
リスト1.18: 良い例 KillProcessRefine1Service.java
public class KillProcessRefine1Service extends Service { private Thread mThread;
@Override
public IBinder onBind(Intent intent) { /* 省略 */
}
@Override
public void onCreate() { super.onCreate();
mThread = new Thread() { @Override
public void run() { super.run(); while(!Thread.currentThread().isInterrupted()) { // do some thing } } }; mThread.start(); } @Override
public void onDestroy() { super.onDestroy(); mThread.interrupt(); } } この例では停止するために
Thread#interrupt
メソッドとThread#isInterrupted
メソッド を使用していますが、volatile
なboolean
の共有変数を使うなど、他にも方法はいくつかあり ます。 停止については1つ注意点があります。リスト1.18
のService#onDestroy
内ではスレッドに停止を促しているだけで実際の停止までは待っていません。このため短時間でサービス が停止して再起動した際に、瞬間的にスレッドが2個できることが起こりえます。もしスレッ ドが瞬間的にでも2個あるとアプリケーションとして好ましく無い場合はリスト
1.19
のよう に、Thread#join
メソッドを使って停止を待つようにしましょう。リスト1.19: 良い例 KillProcessRefine2Service.java
public class KillProcessRefine2Service extends Service { private Thread mThread;
/* 省略 */ @Override
public void onDestroy() { super.onDestroy(); mThread.interrupt(); // 必要なら止まるのを確認するまでonDestroy を抜けないようにする try { mThread.join(); } catch (InterruptedException e) { // 必要ならハンドリングする } } } 別スレッドで処理を行なうことは非常に便利で、特に
Bluetooth
やADK
で外部デバイスを 制御する場合は必須の要素です。しかし便利故に取り扱いを誤ると逃げ出して捕まえられな くなります。そうならないようにどのタイミングで開始し、停止するか設計レベルで明確にし て扱うようにしましょう。1.7
static
フィールドに状態をもつオブジェクトが入っている
ハードウェアや特殊なサーバーと通信するとき、それらを管理するオブジェクトをどのよう に持っているのでしょうか。恐らく直接的にはActivity
やService
がそれらを管理している と思いますが、それらの関係をうまく管理できているでしょうか。もしそれらをstatic
フィー ルドに格納するときはActivity
やService
のライフサイクルと巧くやらないと思わぬ不具合 に行き当たります。 リスト1.20
の例ではService
の開始時に専用の通信スレッドであるHogeThread
を作り、それを
static
フィールドであるsThread
に保持させています。リスト1.20: 悪い例 StaticFieldDameService.java
public class StaticFieldDameService extends Service { private static Thread sThread;
static class HogeThread extends Thread { @Override
public void run() { super.run(); while(!isInterrupted()) { // 何か通信する処理 } } } @Override
public IBinder onBind(Intent intent) { /* 省略 */
}
@Override
public void onCreate() { super.onCreate();
// 専用の通信スレッドをここで生成する sThread = new HogeThread();
sThread.start(); } }
static
フィールドであるため、Service
が破棄された後も明示的に破棄しなければ残り続け ます。このためService
が不意に再起動するとHogeThread
を新たに作成され、元々あったHogeThread
は終了されること無く、HogeThread
が2個生成された状態に陥ります。 これらの問題を回避するには、まずonCreate
で生成したものはonDestroy
内で廃棄するよ うにします。そうすると最早static
フィールドである必要が無くなりますので、HogeThread
を保持する変数を通常のフィールドに変更します。修正したコードはリスト1.21
になり ます。 リスト1.21: 良い例 StaticFieldRefineService.javapublic class StaticFieldRefineService extends Service { private Thread mThread;
static class HogeThread extends Thread { @Override
public void run() { super.run(); while(!isInterrupted()) { // 何か通信する処理 } } } @Override
public IBinder onBind(Intent intent) { /* 省略 */
}
@Override
public void onCreate() { super.onCreate();
// 専用の通信スレッドをここで生成する mThread = new HogeThread();
mThread.start(); }
@Override
public void onDestroy() { super.onDestroy(); // ここで停止して廃棄する mThread.interrupt(); mThread = null; } } ハードウェアや特殊なサーバーと通信するとしても、それらの通信が
Service
のライフサ イクルより長くなる必要はまずありません。いくら長い時間の管理が必要でもサービスのラ イフサイクル内に収めるべきです。そうすれば厳重な管理が必要なstatic
フィールドを使う こと無く、見通しのよいコードにすることができます。static
フィールドに状態をもつオブジェクトを保持させなければならなくなった場合は一度クル内に収めるようにしましょう。
1.8
内部の処理が微妙に異なる
AsyncTask
が無数にある
UI
スレッドでは行なうべきではない時間がかかる処理や通信処理を作るとき、別タスクで 実行させるためにAsyncTask
を使用します。AsyncTask
は便利な反面いろいろなコードの 書き方ができるため、可読性が低下しないよう注意が必要です。 リスト1.22
では、時間の掛かる処理のつもりとして、button1
が押されたときに0
∼100
まで足すタスクを実行して結果をmEditText1
に出力する処理と、button2
が押されたときに101
∼200
まで足すタスクを実行して結果をmEditText2
に出力する処理の2つを定義してい ます。処理自体は軽い処理ですがここには時間がかかる処理が入っていると思ってください。 リスト1.22
は冗長な例です。 リスト1.22: 悪い例 ManyAsyncTaskDameActivity.javapublic class ManyAsyncTaskDameActivity extends Activity implements View.OnClickListener {
private EditText mEditText1; private EditText mEditText2; @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
AsyncTask<Void,Void,Integer> task
= new AsyncTask<Void, Void, Integer>() { @Override
protected Integer doInBackground(Void... params) { int result = 0;
{ // 時間の掛かる処理のつもり
for (int i = 0; i <= 100; i++) { result += i; } } return result; } @Override
protected void onPostExecute(Integer integer) { super.onPostExecute(integer); mEditText1.setText(String.valueOf(integer)); } }; task.execute();
} else if (v.getId() == R.id.button2) { AsyncTask<Void,Void,Integer> task
= new AsyncTask<Void, Void, Integer>() { @Override
protected Integer doInBackground(Void... params) { int result = 0;
{ // 時間の掛かる処理のつもり
for (int i = 101; i <= 200; i++) { result += i; } } return result; } @Override
protected void onPostExecute(Integer integer) { super.onPostExecute(integer); mEditText2.setText(String.valueOf(integer)); } }; task.execute(); } } } リスト
1.22
の2つのAsyncTask
について、まずAsyncTask#doInBackground
メソッド 中を整理して見ましょう。次のような箇所に問題があります。1.
2つのAsyncTask#doInBackground
の動きの違いは繰り返しの回数だけである。2.
2つのAsyncTask#onPostExecute
の動きの違いは出力先のEditText
が異なるだけで ある。3.
おまけ:長い処理を行なうときはユーザーに処理中であることを見せるべき。 まず1.
のAsyncTask#doInBackground
メソッドの中身を共通化してみましょう。リスト1.23
のSumAsyncTask
のように書き換えることができます。それぞれ0-100
、101-200
をAsyncTask#onPostExecute
の引数で渡すようになっています。リスト1.23: 良い例 ManyAsyncTaskRefine1Activity.java
public class ManyAsyncTaskRefine1Activity extends Activity implements View.OnClickListener {
private EditText mEditText1; private EditText mEditText2;
static class SumAsyncTask extends AsyncTask<Integer, Void, Integer> { @Override
protected Integer doInBackground(Integer... params) { int result = 0;
{ // 時間の掛かる処理のつもり
for (int i = params[0]; i <= params[1]; i++) { result += i; } } return result; } } @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
SumAsyncTask task = new SumAsyncTask() { @Override
protected void onPostExecute(Integer integer) { super.onPostExecute(integer);
mEditText1.setText(String.valueOf(integer)); }
};
task.execute(0, 100);
} else if (v.getId() == R.id.button2) { SumAsyncTask task = new SumAsyncTask() {
@Override
protected void onPostExecute(Integer integer) { super.onPostExecute(integer);
mEditText2.setText(String.valueOf(integer)); }
task.execute(101, 200); } } } 次に
2.
と3.
を改善します。AsyncTask
が少々長めになるためSumAsyncTask
をリスト1.24
として分離します。このSumAsyncTask
の特徴としては、ISumAsyncTaskListener
インターフェースと、mUserData
メンバ変数があることです。mUserData
メンバ変数はGenerics
での指定になっており、好きな型を指定できるようになっています。 リスト1.24: SumAsyncTask.javapublic class SumAsyncTask<T> extends AsyncTask<Integer, Void, Integer> { public interface ISumAsyncTaskListener<T> {
public void onPreExecuteSumAsyncTask(T userData); public void onCancelSumAsyncTask(T userData);
public void onPostExecuteSumAsyncTask(int result, T userData); }
private ISumAsyncTaskListener<T> mListener; private T mUserData;
public SumAsyncTask(ISumAsyncTaskListener listener, T userData) { this.mListener = listener;
this.mUserData = userData; }
@Override
protected void onPreExecute() { super.onPreExecute();
mListener.onPreExecuteSumAsyncTask(mUserData); }
@Override
protected Integer doInBackground(Integer... params) { int result = 0;
for (int i=params[0];i<=params[1];i++) { result += i;
}
return result; }
@Override
super.onCancelled();
mListener.onCancelSumAsyncTask(mUserData); }
@Override
protected void onPostExecute(Integer result) {
mListener.onPostExecuteSumAsyncTask(result, mUserData); }
}
SumAsyncTask
を使ったものがリスト1.25
となります。だいぶスッキリした感じになり ました。SumAsyncTask
のGenerics
にはEditText
型を指定しています。リスト1.25: 良い例 ManyAsyncTaskRefine2Activity.java
public class ManyAsyncTaskRefine2Activity extends Activity implements View.OnClickListener,
SumAsyncTask.ISumAsyncTaskListener<EditText> { private EditText mEditText1;
private EditText mEditText2; SumAsyncTask<EditText> mTask1; SumAsyncTask<EditText> mTask2;
private ProgressDialog mProgressDialog; @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
mTask1 = new SumAsyncTask<EditText>(this, mEditText1); mTask1.execute(0, 100);
} else if (v.getId() == R.id.button2) {
mTask2 = new SumAsyncTask<EditText>(this, mEditText2); mTask2.execute(101, 200);
} }
@Override
public void onPreExecuteSumAsyncTask(EditText userData) { mProgressDialog = new ProgressDialog(this);
mProgressDialog.show(); }
@Override
public void onCancelSumAsyncTask(EditText userData) { mProgressDialog.dismiss();
mProgressDialog = null; }
@Override
public void onPostExecuteSumAsyncTask(int result, EditText userData) { userData.setText(String.valueOf(result)); mProgressDialog.dismiss(); mProgressDialog = null; } } リスト
1.25
とリスト1.23
の大きな違いは2つあります。 1つめはAsyncTask
を匿名クラスで拡張し、AsyncTask#onPostExecute
メソッドで実行 していたのを、onPostExecuteSumAsyncTask
メソッド内で行なっていることです。このメ ソッドの第二引数はGenerics
により出力先のEditText
が渡されるようになっています。こ れにより前の例ではそれぞれ個別に定義していた処理を1つにまとめることができています。 2 つ め はonPostExecuteSumAsyncTask
と 同 じ よ う に 定 義 さ れ たonPreExecute-SumAsyncTask
とonCancelSumAsyncTask
です。これらのメソッドはSumAsyncTask
内から呼ばれるように作られており、ここでプログレスダイアログを出す処理を完結させてい ます。 リスト
1.22
とリスト1.25
を見比べてみてどちらのコードがスッキリしているでしょう か。別タスクを使用する処理では、別タスクの処理とUI
スレッドの処理を分離させるため、 その継ぎ目となる処理が煩雑となりがちです。AsyncTask
やそのリスナーとなるメソッドをInterface
で上手に切りだすと、1箇所に複数の目的のコードが混在することを避けることが できます。もし書いているコードが煩雑になっている場合はInterface
やメソッドの分割を行 い、読みやすくなるようにしましょう。1.9
Preference
への書き込みに個々で
edit
メソッドを呼んで
いる
設定値や小さな値を保存するのにSharedPreference
を使います。SharedPreference
はString
型以外のデータもKey-Value
で値を保持できるのでちょっとした値を保存するのに便利です。しかし便利ゆえにいくつかの弱点があります。
1. String
型でキーを指定しなければならず、しばしばキーを間違えてバグの原因になる2.
誤って違うデータ型の値を入れると、取り出し時にClassCastException
がスローさ れる これらの弱点は非常に厄介です。どのように厄介かというと、間違えてもコンパイルエラー にならず、実行時の例外やおかしな挙動を引き起こすからです。 リスト1.26
を例に見てみましょう。このコードではボタンが2個あり、押されるとそのボ タン名をSharedPreferences
に保存します。そしてActivity#onCreate
の中でeditText1
に そのボタン名を設定する処理になっています。動作には問題はありませんが前述の問題があ り、開発を進めていく上での不安となる点が残っています。リスト1.26: 悪い例 SharedPreferencesDameActivity.java
public class SharedPreferencesDameActivity extends Activity implements View.OnClickListener {
private SharedPreferences mPref; private EditText editText1;
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_many_handlers); findViewById(R.id.button1).setOnClickListener(this); findViewById(R.id.button2).setOnClickListener(this); editText1 = (EditText) findViewById(R.id.editText1);
mPref = PreferenceManager.getDefaultSharedPreferences(this); String lastButton = mPref.getString("lastButton", "");
editText1.setText(lastButton); }
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
mPref.edit()
.putString("lastButton", "button1") .apply();
} else if (v.getId() == R.id.button2) { mPref.edit()
.putString("lastButton", "button2") .apply(); } } }
SharedPreferences
に保存する際のキーは"lastButton"
となっていますので、これを定数 化してみましょう。定数化したものはリスト1.27
となります。 リスト1.27: 良い例 SharedPreferencesRefine1Activity.javapublic class SharedPreferencesRefine1Activity extends Activity implements View.OnClickListener {
private SharedPreferences mPref; private EditText editText1;
private String KEY_LAST_BUTTON = "lastButton"; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_many_handlers); findViewById(R.id.button1).setOnClickListener(this); findViewById(R.id.button2).setOnClickListener(this); editText1 = (EditText) findViewById(R.id.editText1);
mPref = PreferenceManager.getDefaultSharedPreferences(this); String lastButton = mPref.getString(KEY_LAST_BUTTON, ""); editText1.setText(lastButton);
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
mPref.edit()
.putString(KEY_LAST_BUTTON, "button1") .apply();
} else if (v.getId() == R.id.button2) { mPref.edit()
.putString(KEY_LAST_BUTTON, "button2") .apply();
} }
} 見やすくなったでしょうか。そして前述の問題点は解決できているでしょうか。キー の
"lastButton"
が定数化されていますが、実は解決できていません。なぜならキーが複数ある 場合は、別のキーと取り違える可能性がまだ残っているからです。 こ の よ う な 場 合 、こ れ ら の 問 題 を 回 避 す る に は リ ス ト1.28
の よ う なSharedPref-erences
の ラ ッ パ ー ク ラ ス を 作 る と 回 避 で き ま す 。こ の ラ ッ パ ー ク ラ ス で は キ ー をKEY_LAST_BUTTON
の定数にしていますがprivate
になっているので、メソッドの呼 び元はキーを意識しなくてよくなっており、キーを取り違える心配もありません。またアクセ スする処理をgetLastButton
メソッドとputLastButton
メソッドにまとめているので、誤っ て別のデータ型を入れることはありません。すなわちラッパークラスに前述の問題点を閉じ 込めているのです。 リスト1.28: MySharedPreferences.javapublic class MySharedPreferences {
private static final String KEY_LAST_BUTTON = "lastButton"; private SharedPreferences mPref;
private SharedPreferences.Editor mEditor;
public MySharedPreferences(SharedPreferences mPref) { this.mPref = mPref;
}
public MySharedPreferences edit() { mEditor = mPref.edit();
return this; }
public void apply() { mEditor.apply(); mEditor = null; }
public String getLastButton() {
return mPref.getString(KEY_LAST_BUTTON, ""); }
public MySharedPreferences putLastButton(String value) { mEditor.putString(KEY_LAST_BUTTON, value);
} }
このラッパークラスを扱うように直したものがリスト
1.29
となります。MySharedPrefer-ences
を使うようにしたことで、前述の問題がいずれも解決していることがわかります。リスト1.29: 良い例 SharedPreferencesRefine2Activity.java
public class SharedPreferencesRefine2Activity extends Activity implements View.OnClickListener {
private MySharedPreferences mPref; private EditText editText1;
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_many_handlers); findViewById(R.id.button1).setOnClickListener(this); findViewById(R.id.button2).setOnClickListener(this); editText1 = (EditText) findViewById(R.id.editText1); mPref = new MySharedPreferences(
PreferenceManager.getDefaultSharedPreferences(this)); String lastButton = mPref.getLastButton();
editText1.setText(lastButton); }
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
mPref.edit()
.putLastButton("button1") .apply();
} else if (v.getId() == R.id.button2) { mPref.edit() .putLastButton("button2") .apply(); } } }
動けばいいという意味ではラッパークラス作らず、そのまま書いてしまっても違いはありま せん。しかしコード後から見たときや修正するときに、キーやデータ型のように手で合わせな いといけないものがそのままになっていると、誤って記述した際に不具合の原因となってしま います。特にこれらのキーやデータ型は誤って記述してしまうとコンパイルエラーにもなら ないため非常に厄介です。これら避けるため、ラッパークラスを作成して問題点の責務をラッ パークラスに持たせ、扱う側が無駄に悩まなくていいようにしましょう。ラッパークラスは単 にラッピングしているのではなく、そういった責務も封じ込めているのです。
1.10
Application
に
Service
の実体を突っ込んでいる
Android
アプリケーションではバックグラウンドで処理を行いたいときにService
を使用 します。Service
の生存期間はActivity
に比べて長めで、ものによってはプロセスの開始と同 時に起動してずっと動作するものもあります。ずっと動作するからとApplication
に保持させ たりすると、Service
のライフサイクルから逸脱し、面倒な不具合に繋がることがあります。リスト
1.30
はonCreate
メソッド内でApplication
に自身を保持させ、onDestroy
メソッ ド内で解除するような乱暴な実装の例です。このService
はhoge
メソッドという文字列返 すだけのメソッドが実装されています。リスト1.30: 悪い例 ResidentDameService.java
public class ResidentDameService extends Service { private IResidentDameService.Stub mBinder =
new IResidentDameService.Stub() { };
@Override
public IBinder onBind(Intent intent) { return mBinder;
}
@Override
public void onCreate() { super.onCreate();
DameApplication app = (DameApplication) getApplication(); app.setDameService(this);
@Override
public void onDestroy() { super.onDestroy();
DameApplication app = (DameApplication) getApplication(); app.setDameService(null);
}
public String hoge() { return "hogehoge"; } } この
Service
のhoge
メソッドから文字列を受け取り、画面に表示するだけの乱暴な実装 のActivity
がリスト1.31
になります。 リスト1.31: 悪い例 ResidentDameActivity.javapublic class ResidentDameActivity extends Activity implements View.OnClickListener {
private EditText editText1; @Override
protected void onCreate(Bundle savedInstanceState) { /* 省略 */
}
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) {
DameApplication app = (DameApplication) getApplication(); ResidentDameService service = app.getDameService();
if (service != null) { editText1.setText(app.getDameService().hoge()); } } } } こ の
Activity
はResidentDameService#onCreate
が 呼 び 出 さ れ た 後 な ら ば 動 く こ と は 動 き ま す 。し か しService
が 起 動 す る 前 や 再 起 動 し た 場 合 はDameApplica-tion.getDameService()
がnull
を返すため正常に動作しません。またActivity
側で対応し ようとしても、起動や再起動が発生したか否かのイベントが通知されないため、対応が行えません。これを回避するには
Application
に保持させたりせず、標準の手順にあるとおりServiceConnection
を使います。まず
onCreate
メソッド内でApplication
に自身を保持させたり、onDestroy
メソッド内で 解除したりするような乱暴な実装を削除します。リスト1.32: 良い例 ResidentRefineService.java
public class ResidentRefineService extends Service {
public class LocalBinder extends IResidentRefineService.Stub { public ResidentRefineService getService() {
return ResidentRefineService.this; }
}
private LocalBinder mBinder = new LocalBinder(); @Override
public IBinder onBind(Intent intent) { return mBinder;
}
public String hoge() { return "hogehoge"; } }
Activity
側もリスト1.33
のように、ServiceConnection
を作成し、標準の手続きにした がってService
をbind
するようにします。 リスト1.33: 良い例 ResidentRefineActivity.javapublic class ResidentRefineActivity extends Activity implements View.OnClickListener {
private EditText editText1;
private ResidentRefineService mService;
ServiceConnection mServiceConnection = new ServiceConnection() { @Override
public void onServiceConnected
(ComponentName name, IBinder service) {
mService = ((LocalBinder) service).getService(); }
public void onServiceDisconnected(ComponentName name) { mService = null;
} };
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button1).setOnClickListener(this); editText1 = (EditText) findViewById(R.id.editText1); }
@Override
protected void onStart() { super.onStart();
Intent intent = new Intent(this, ResidentRefineService.class); bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); }
@Override
protected void onPause() { super.onPause();
unbindService(mServiceConnection); }
@Override
public void onClick(View v) { if (v.getId() == R.id.button1) { if (mService != null) { editText1.setText(mService.hoge()); } } } } 一見面倒には見えますが、不安定な状態をハンドリングできるようにするために必要な手続 きです。これは
Activity
とService
にはライフサイクルの違いこそあれ、2つは対等な関係 であるからです。対等な2つを接続するということはそれなりの手順が必要になるというこ とです。ServiceCon-nection
のリークに繋がります。この例ではService
をbind/unbind
するタイミングはon-Start()/onStop()
を使用していますが、onCreate()/onDestroy()
やonPause()/onResume()
など、
Activity
のライフサイクル上で必ず通る組み合わせを使用するとリークを回避すること ができます。1.11
OutOfMemoryError
避けが悪影響だけ出している
OutOfMemoryError
、通称OOM
が発生したときどのような対策をしているでしょうか。 まさか原因の特定もせずにそこらじゅうの変数に手当たりしだいnull
を代入したりしていな いでしょうか。Android
のアプリケーション開発ではMemory Analyzer(
以下MAT)
と呼ば れるツールが使用できます。このプラグインでは実行中のアプリケーションのメモリをダン プし、どのクラスのインスタンスがどれだけメモリを専有し、それが何故解放されないのかを 調べることができます。MAT
はEclipse
のプラグインとして動作しますが、スタンドアロン 版もあります。Eclipse
の場合、このプラグインをインストールすると、DDMS
パースペクティブのDump
HPROF file
ボタンを押すとダンプしたメモリを解析できるようになります。図1.2 Dump HPROF fileボタン
たとえば次のような無駄に大きな
byte
配列をもつActivity
の解析をしてみましょう。メン バ変数として16MB
のbyte
配列を保持しています。リスト1.34: LargeBytesActivity.java
public class LargeBytesActivity extends ActionBarActivity { private byte[] mLargeBytes = new byte[16 * 1024 * 1024]; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } この
16MB
のbyte
配列をメモリリークの原因ということにして、MAT
を使って追ってみ ましょう。まず最初に
Dump HPROF file
ボタンを押します。すると図1.3
のような画面が表示され ます。この画面ではおおよそメモリを大きく使用しているクラスが何であるのかが円グラフ で表示されます。図1.3 Memory Analyzerの初期表示
今回はメモリを大きく保持しているクラスが何であるかを確認したいので、ヒストグラムを 表示します。ヒストグラムを表示するには図
1.4
のヒストグラムのボタンを押します。図1.4 Create a histogramボタン
すると図