2012年11月5日月曜日

Android - Androidプログラマかを見分ける12の質問(回答編)

目的としては、それなりに信念を持ってAndroidアプリ開発にある程度の期間携わってきたことを見抜くことなので、完全な回答を求めているわけではない事に注意。 多少間違っていてもそれなりに内容をもって答えられていればほぼ正解としてよいものとする。

1. ListViewを使うとただ単にViewを適宜生成、連結してリストを作った場合に対して大幅な利点が存在するが、それは何か。実装の癖を交えて簡単に説明せよ。

リスト表示対象の情報が100件以上と多い場合に、パフォーマンスの差が顕著になること。これに関しては、やったことがないのであれば自分で1000件、2000件データを連結したリスト表示を「ListViewを使わずに」行ってみて、 描画に恐ろしく時間がかかることを確認するべき。Webアプリの方には無い概念なので印象に残るはず。 ListViewの方は、画面に表示されている部分しか描画処理を行わない為同等のデータ量で試した場合比べものにならない位高速に動作する。

ListViewでは、以下のようにArrayAdapterやCursorAdapterにListViewの「振る舞い」を定義してsetAdapter()して使うが、 以下のコードのようにリストの1行分のビューを使いまわす記法がイディオムとなっており、まさしくAndroidアプリ開発を行う上での必修項目となっている。 この使いまわしを行う事で、幾らリストをスクロールさせたところでその都度convertView(一行分のレイアウト)がnewされる事がない為これが原因でGCが起こることが無くなり、パフォーマンス向上に貢献する。 ちなみに以下のコードでは使ってないが、findViewById()を毎回行わないようにすると更にパフォーマンスが上がるらしい(ViewHolderで検索せよ)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ListViewに振る舞いをセット. 「Data」はリストに表示すべきデータを格納している任意のPOJOとする.
((ListView)findViewById(R.id.list)).setAdapter(new ArrayAdapter<Data>(this, 0, dataList) {
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 既にconvertViewが作られていたらそれを使いまわし, 無かったら(初期表示であれば)インフレート(生成)する定番のイディオム.
        final View layout = convertView == null ? getLayoutInflater().inflate(R.layout.list_item, null) : convertView;
         
        // 「1行分のレイアウトの各項目」にそれぞれ対象位置のデータをセットする.
        ((TextView)layout.findViewById(R.id.name)).setText(getItem(position).name);
        ((TextView)layout.findViewById(R.id.address)).setText(getItem(position).address);
        ((TextView)layout.findViewById(R.id.tel)).setText(getItem(position).tel);
        return layout;
    }
});

「1行分のレイアウトを使いまわす」という独特の方法故、それに起因するバグがある事も知っていれば尚良し。

  1. convertViewには使いまわす前のデータが既に描画されてしまっているので、全ての要素に明示的にデータをセットしない可能性があるのであれば、古い表示を消すという処理を適宜入れる必要がある
  2. それぞれのレイアウトで非同期に画像などを取りに行くような処理をしている場合、画像取得リクエスト→ユーザがスクロールする→ビューが使いまわされる→新しいビューに古い画像が表示される、のようなマルチスレッド特有のバグが出やすい

そして、このAdapterで振る舞いを定義して使うスタイルはAndroidアプリ開発では度々登場する(GalleryやViewPager等)為超重要

2. HTTP通信を行う際に普通に行うと画面が固まってしまう(時間が長いと警告ダイアログが出てしまう)が、どのように実装すべきか。

これも基本中の基本なのだが、これすらも知らない「自称Androidプログラマ」が多すぎて閉口する。
長い処理はメインスレッド(UI Thread)で行わず、別スレッド(Worker Thread)を立てて行うようにするといった事が言えれば充分である。 とは言っても慣れてくると自分でThreadをnewするような事は殆ど無くなり、AsyncTaskという便利な非同期処理用のクラスがあるので、それを使用できていればよい。 2,3秒UIが固まっただけでもユーザはイライラしてしまうので、いかにUIがフリーズした事(ANR:application not responding)を示すダイアログを表示させないかが鍵となってくる。

Froyo(Android 2.2)からAndroidHttpClientというものが登場し、DefaultHttpClientがAndroid向けにいろいろ最適化されているとの事だが、 UIスレッドで使用すると例外をスローするようになっているので、これを使用していれば自然とこの問題は起きないはず。 Froyo以降をターゲットとしているのであれば、積極的に活用していくべき。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         
        // ダメな例. このように直接投げるとHTTPレスポンスが返ってくるまでUIが固まる.
        try {
            HttpResponse response = new DefaultHttpClient().execute(new HttpGet("http://hoge.huga/"));
        catch (IOException e) {
            e.printStackTrace();
        }
 
        // こんな感じで非同期で投げる. doInBackground()内が別スレッドで実行される.
        new AsyncTask<String, Void, String>() {
            @Override
            protected String doInBackground(String... params) {
                // Worker Thread内処理. HTTP通信を行う.
                AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
                try {
                    // TODO リクエストパラメータを詰めたり云々.
                    HttpResponse response = client.execute(new HttpGet("http://hoge.huga/"));
                    // TODO 本当はHTTPステータスコードのチェックが要る.
                    return EntityUtils.toString(response.getEntity());
                } catch (IOException e) {
                    e.printStackTrace();
                     
                    // TODO 通信失敗時の処理を記述.
                    return null;
                } finally {
                    client.close();
                }
            }
             
            @Override
            protected void onPostExecute(String result) {
                // UI Threadでの後処理.
                // TODO resultの値をチェックしてJSONやXMLにパースしたり.
            };
        }.execute();
    }
}

3. ダイアログは気軽に出せて便利だが、気をつけないとWindowLeakedやWindowManager.BadTokenExceptionが発生してアプリが落ちる。どう気をつけるべきなのか。

基本としては、そのアクティビティが生存していないと、その上にダイアログを表示する事も消すこともできない。スレッド処理とダイアログ表示を組み合わせる際は要注意。
恐らく通信するアプリを作成した場合は誰もが経験する事だろう。が、この中の質問としては些か難易度が高いかもしれない。まぁ現象を何となく言えれば良いのでは。

  • 非同期でDialogを表示若しくは消す時に対象となるActivityが既にdestroy後だった場合
  • 既に消えているダイアログに対して再度消そうと(dismissDialog())した場合
  • などなど(まだあった気がしたが……)。Activity#showDialog()やDialogFragment等でも対処が異なるのも厄介
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
new AsyncTask<Void, Void, Void>() {
    @Override
    protected void onPreExecute() {
        // このAsyncTaskが非同期で開始される場合は要注意.
        showDialog(0);
    };
     
    @Override
    protected Void doInBackground(Void... params) {
        // 重い処理.
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
     
    @Override
    protected void onPostExecute(Void result) {
        // doInBackground()処理中にActivityを終了すると落ちる.
        dismissDialog(0);
    };
}.execute();

Activity#isFinishing()でActivityが終了しているかが確認できるので、それで終了していない場合のみ表示、のようにすれば回避できる場合が多い。
そして、DialogFragmentの頁にも書いたが、多くの書籍、サンプルコードに書いてある、直接AlertDialog.Builderからその場でshow()する方法は正しくない。 ので、端末によってはこれが原因で落ちる事もある。

とにかく、Android開発ではダイアログはハマりがちな項目である。 もし選択肢として選べるのであれば、ダイアログを使わないで済むようなパターンも検討するとよい。

  • ProgressDialogの代わりにProgressBarを使用した半透明ビューをFrameLayout or RelativeLayoutを使用して上から被せる
  • AlertDialogの代わりに半透明ビューを以下同様

4. FrameLayout、LinearLayout、RelativeLayoutの特長を軽く説明せよ。

ボーナス問題その1。この3種を適切な場面で使用する事が出来るようになるまでXMLを書いて自分を鍛えると良い。

  • FrameLayout: 左上を起点として複数のビューを重ねて表示する事ができる単純なレイアウト
  • LinearLayout: 愚直に横若しくは縦にビューを並べて使用する初心者が多用するレイアウト
  • RelativeLayout: 「指定IDのビューに対して」「親レイアウトに対して」等相対的にビューを配置していくレイアウト。これに習熟するとレイアウト階層が減らせる為パフォーマンス向上に寄与する

5. wrap_content, match_parent(fill_parent)の概念を説明せよ。

ボーナス問題その2。とはいえ、HTMLの概念と大きく異なる部分なので、初めは感覚に慣れずに戸惑う概念ではある。回数を重ねると慣れる。

  • wrap_content: 内包するcontentに合わせるようにwidth若しくはheightの値を決める
  • match_parent(fill_parent): 親ViewGroupのpaddingの分を引いたwidth若しくはheightに合わせるようにwidth若しくはheightの値を決める

要するに、中身のサイズに合わせるか、親のサイズに合わせるか。

6. BroadcastReceiverはどういった用途に使用するものか。

AndroidにはIntentをbroadcast(アプリ内だけでなく端末内に「発信」)する仕組みがあるが、これを受け取る為の「受信機」に当たるもの。 AndroidManifest.xmlに定義する事によって静的に定義することもできるし、Activity内で動的にBroadcastReceiverを登録・解除する事もできる。 前者はAlarmManagerからの戻りを受け取る時に一番手軽な方法であるし、後者はServiceとActivity間の値の受け渡しを行いたい際に、 Activityに依存する処理を「Activityが生存している時のみ」行いたい場合などに重宝する。

7. アプリを閉じていても定時(例えば明日の7:00にとか)に処理をしたい。どのような技術を使うか。また、可能であればどのような事に気をつけなければならないか。

AlarmManagerを使用する事が言えれば充分である。 そして、AlarmManagerは、端末の電源を切ったり、アプリをアップデートしたり、SDカードにインストールしておりSDカードをマウント・アンマウントした際にリセットされてしまうのに注意する。 これらのイベントはBroadcastされるので、BroadcastReceiverで処理を行うように実装してやればよい。

8. AndroidManifext.xmlでActivityに対して定義されるlaunchModeのstandard, singleTop, singleTask, singleInstanceの違いを軽く説明せよ。

まず、一般的にAPK単位で「Androidアプリ」と呼称しているが、実際に駆動するアプリケーションの単位としては(うまい日本語が見つからないが)あまり正しくなく、 Android内部ではそれぞれのアプリ(?)は「タスク」単位で駆動する事を認識しておく必要がある。 一般的に「タスク」には「複数のActivity」が所属しており、主にIntentを使用して新しいActivityを起動しつつ現在のタスクに「所属させる」処理を続けつつ画面遷移を行っていく。 筆者もそれぞれを設定した際の振る舞いの詳細は覚えていないが、以下のような事が言えれば良いかと思う。

  • standard: 何も指定しなかった際はこのモードとなる。タスクのスタック構造の一部として「自然に」振る舞う
  • singleTop: standardに対しタスク最前面にいるActivityと同一のActivityをcreateする際にActivityのインスタンスを使いまわす
  • singleTask: 常にタスクのルートアクティビティ(一番前のActivity)として振る舞う
  • singleInstance: 指定されたActivityが「タスク内の唯一のActivity」として振る舞う

よく分からないが「スタックに積みたくないから」と全てのActivityをsingleInstanceにしてあったプロジェクトを見たことがあったが……。タスクが違うとonActivityResult()で値を返せなくなったり、不自然な実装になる。 で、AndroidManifest.xmlにこれらを定義せず、IntentにFlagを渡すことで「動的に振る舞いを変化」させる事ができるので、問題ないのであればこちらを使用した方が柔軟であるといえる。

9. Androidでは位置情報を特定するのにGPSとネットワークを用いた取得方法があるが、それぞれどのような特性があるか。

後で個別のトピックで書きたい深いお題目だが、とりあえずここでは簡潔に。それぞれ特性があるので場合によってこれを組み合わせて位置情報を取得しに行くべき。

  1. GPS: 衛星を使った方法。場所にもよるが精度の高い位置情報が取れる。初回取得までに多少の時間がかかる。屋内では取れない場合が多い
  2. ネットワーク(Wi-Fi/3G): 精度は低めだが、ある程度取得を続けると多少正確性が増す。取得までの時間は早いし、ネットワークさえ繋がれば屋内でも取れる。

iPhoneの方はこのように分かれていないらしい(「位置情報サービス」と抽象化されている)ので、 この辺りは熟知しておかないと技術者でない人間どころかiPhoneアプリ技術者とも話が合わないので注意する。 「位置情報取得と言えばGPS」という意識で話をしてくる人間が圧倒的に多く、基地局から位置情報が取れる事はほとんど知られていない。 故に「GPSを使って云々」という指示が来るが、実際にテスト段階で室内で取れなくて「こんなはずじゃ無かったのに」といったプロジェクトが多かった。

Google謹製のマップアプリの動きやMyLocationOverlayでの現在地表示を試してみて感覚を頭に叩きこむべし。 どちらも、GPS/ネットワークをうまく使用して位置情報を取得している。端末の設定から「GPS」「ネットワークによる位置情報取得」をそれぞれON、OFFにして動作を確認するとよい。

10. 外部のカメラアプリに暗黙的インテントを投げて、戻ってきた画像データをそのまま使うとよくOutOfMemoryで落ちる。どのように対処すべきか。

カメラアプリから返ってくる画像はそのままだとスマホで扱うには大きすぎる場合が多いので、現実的なサイズにリサイズしたり、圧縮率を下げたりしてから読み込む。 これも実装したことがないと分からない。なので、「カメラ機能を使用したアプリの作成経験が無い」場合は減点対象としないものとする。 とはいえ、「桁違いに大きい画像をHTTPで取得してBitmapに格納した時」「桁違いに大きい画像を/res/drawableに置いて表示した時」なども同様なので、 Androidプログラマとしては当然理由と対処法を知っておくべき。

AndroidのBitmap内のデータは所謂Dalvikヒープ(Javaのインスタンス等を格納するVM上のメモリ領域)には入っておらず、Nativeヒープに入っている(Honeycomb以前)。 このNativeヒープの使用出来る容量が一般的に非常に少なく、ある程度大きい画像を読み込もうとするとすぐに落ちてしまう。 DDMSの「Heap」はDalvikヒープを指すので、見ていてもBitmapのメモリリークを追うことはできない。専用のメソッドがあるのでそちらでログ出力して追うこと。

11. 画面Aから画面Bに遷移し、その後画面Bを閉じ画面Aに戻るが、画面Bで特定の処理を行った場合のみ画面Aでデータの更新処理を行いたい。どのように実装すべきか。

画面AのActivityから画面BのActivityにIntentを送る際にstartActivity()でなくstartActivityForResult()を使う。 画面BのActivity上で結果をsetResult()でセットしてfinish()、 すると画面AのActivityのonActivityResult()に処理が移るのでそこで結果を見つつ処理を行う、と言った事が言えれば満点ではないだろうか。

12. Android 3.0から追加されたFragmentとはどのような概念で、使うとどのように有利になるのか。

非常に有用なAndroid Support Packageの存在自体を知らないAndroidプログラマも多かった。が、最近ではどうだろう。 Support Packageを使用すればAndroid 3.x以前の端末に向けたFragmentやLoaderを使ったアプリを開発することができる。

Android SDK Toolsがr20になってから、新規プロジェクトを作るとandroid-support-v4.jarがデフォルトで同梱されるようになった。 その為、今後は使用される事は最早「常識」という認識で間違い無いだろう。 とても便利なので、まだご存知でない方はLoader(CursorLoader / AsyncTaskLoader)と合わせて勉強して頂きたい。

FragmentとはViewとロジックをセットにしたコンポーネントみたいなもので、Fragment自体がActivityのように(Activityよりも状態遷移が多い)ライフサイクルを持って駆動する、と言ったことが言えれば良いかと思う。 よく言われるのが、スマホとタブレットの両対応を行う際に非常に有用である、という意見である。画面の部品にロジックがセットになっているので、 「スマホでは部品Aしか表示しないが、タブレットでは部品AとBを表示したい」ような時に無理のない実装になる。

Fragmentを使用することでレガシーなActivityGroupとTabActivityを完全排除できる。 むしろActivityGroupとTabActivityは混乱の元なので使用すべきではない。

後は何気に便利なのが、「ライフサイクルに依存した共通処理を書きたい」時だったりする。位置情報を取得する箇所を共通処理にしたいが、onResume()でregisterしてonPause()でunregisterしたいとか。

0 件のコメント:

コメントを投稿