- 2011年1月31日 11:45
- Android
Androidアプリには様々な制約があります。
その一つが、
「1つの画面上のUI(コンポーネント)を直接操作できるのはUIスレッドのみ」
というものです。
UIスレッドとは、ユーザーが行うイベント(タッチする、触れる、スライドするなど)を
受け取り処理を開始するスレッドのことです。
データの検索やネットワーク通信によるデータの受信・送信処理、
他のアプリの起動などの処理は、場合によっては
やや時間を有することでしょう。
UIスレッドが一連の処理をすべて行うと、ユーザーは
UIスレッドが処理を完了し終わるまで何ら操作ができない事になります。
つまり、場合によってはユーザーにストレスを与え、
操作性が悪いなどと言われかねません。
そこで登場するのが並行して異なる処理を行う、マルチスレッド処理です。
基本的な形としては、UIスレッドがユーザーの窓口となり
他のスレッドに必要な処理を委託します。
UIスレッドは委託したスレッドから結果を受け取ったタイミングで
ユーザーへ結果を表示します。
結果を受け取るまではユーザーに対して、例えばダイアログを表示するとか
特に何もせずにユーザーが他の操作を実行できるように待機するなど、
ユーザーのアクションに対して、すぐにリアクションをすることは、
ユーザーのストレスを軽減させることになります。
他のスレッドはUIスレッドにデータを渡さず、自分でUIを変更して
結果を表示すれば良いじゃないかと考えられます。
しかしそのようにできないような仕様となっているのは、
複数のスレッドのUI操作による非整合性を避けるためです。
そのためUIスレッド以外のスレッドの処理結果をUIに反映させたい場合には
UIスレッドと共有できる変数やデータファイルを使用するか
Handlerオブジェクトを使用します。![]()
Handlerオブジェクトは内部にRunnableオブジェクトを格納するキュー
(入れた順番に取り出すデータ構造)を持っており、
UIスレッドに適宜Runnableオブジェクトを渡すことができます。
つまり、UIスレッド以外のスレッドがUIスレッドに何らかのデータを渡したい場合、
このHandlerオブジェクトを使用するとデータ共有ができるのです。
共有データはMessageオブジェクトに格納しHandlerオブジェクトに渡す
という仕組みとなっています。
サンプルを元にその内容を説明していきます。
画面上のボタンをクリックすると、UIスレッドが内部で生成されたスレッド
(以下、内部スレッド)から表示データを受け取り、
ボタンの下に存在するScrollViewで表示します。



今回のサンプルソースを貼っておきます。
表示データはSQLiteデータファイルから抽出したものを使っています。
Androidには標準でSQLiteが組み込まれており、
アプリで管理するデータの管理で利用されるデータファイルです。
今回この記事ではデータファイルへの使用方法は述べませんが、
次回その部分に触れていきます。
以下表示画面用のActivityクラスのソースコードです。
TopActivity.java
/**
* スレッドを作成しそのスレッドから取得したデータを
* 画面に表示する。
*/
package com.ken.android.multithread;
import java.util.ArrayList;
import com.ken.android.dao.DatabaseHelper;
import com.ken.android.dto.UserDataDto;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
public class TopActivity extends Activity {
/** Called when the activity is first created. */
//スレッド間のメッセージの送受信を提供するクラス
private Handler handler;
//Viewとウィジェットの横幅、縦幅
private final int FP = ViewGroup.LayoutParams.FILL_PARENT;
private final int WC = ViewGroup.LayoutParams.WRAP_CONTENT;
//データを表示するView
private ScrollView scrollview;
//アクティビティ起動時に実行される
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//データを取得するボタン
Button btnDisp = (Button)findViewById(R.id.btn1);
btnDisp.setOnClickListener(new BtnDispClickListener());
//データを入力するボタン
Button btnDB = (Button)findViewById(R.id.btnDb);
btnDB.setOnClickListener(new BtnDBOnClickListener());
//非表示ボタン
Button btnNodisp = (Button)findViewById(R.id.btnNondisp);
btnNodisp.setOnClickListener(new BtnNodispOnClickListener());
//表示データを格納するView
scrollview = (ScrollView)findViewById(R.id.scrollview);
//ハンドラーオブジェクトの生成
handler = new Handler(){
/*
* 生成したスレッドからデータを受信した際、画面ScrollView内に
* 受信したデータを登録する
*/
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
LinearLayout dataList = (LinearLayout)msg.obj;
scrollview.addView(dataList);
}
};//end Handler()
}// end onCreate()
//データ取得ボタンをクリックしたときの処理を定義するクラス
private class BtnDispClickListener implements View.OnClickListener{
public void onClick(View v) {
//表示されているデータを全て削除し画面をリセットする
scrollview.removeAllViews();
//スレッドの生成と起動
Thread thread1 = new Thread(new ThreadRunnable());
thread1.start();
}
}
//データを入力するボタンをクリックしたときの処理を定義するクラス
private class BtnDBOnClickListener implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO 自動生成されたメソッド・スタブ
Intent intent = new Intent(TopActivity.this,DBActivity.class);
startActivity(intent);
}
}
//非表示ボタンをクリックしたときの処理を定義するクラス
private class BtnNodispOnClickListener implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO 自動生成されたメソッド・スタブ
scrollview.removeAllViews();
}
}
//生成したスレッドの動作内容を定義するクラス
private class ThreadRunnable implements Runnable{
/*
* 取得した個々のデータをLinearLayoutでまとめた後、
* それらをまとめるLinearLayoutに格納しHandlerに渡す
*/
public void run() {
//個々のデータをまとめるVIew
LinearLayout linearlayout_inner = new LinearLayout(TopActivity.this);
//個々のデータを縦に配置する
linearlayout_inner.setOrientation(LinearLayout.VERTICAL);
//横幅と縦幅の設定
LinearLayout.LayoutParams layoutparams_inner = new LinearLayout.LayoutParams(FP,WC);
linearlayout_inner.setLayoutParams(layoutparams_inner);
try{
/*
* 取得データからViewを生成する。
* ここはデータベースへのアクセスやHTTP通信でサーバから
* データを取得なども考えられる。
*/
//データベース操作オブジェクトを取得する
DatabaseHelper helper = new DatabaseHelper(TopActivity.this);
//データベース内のデータを全て取得する
ArrayList<UserDataDto> list = helper.selectAll(DatabaseHelper.TABLE_NAME_KEN_TABLE);
Log.i("UserData count",list.size() + " ");
for(int i = 0 ; i < list.size(); i++){
Log.i("Check" + i, "OK");
//1件分の情報を取り出す
final UserDataDto user = list.get(i);
//個々のデータを格納するView
LinearLayout linearlayout = new LinearLayout(TopActivity.this);
//横に配置する
linearlayout.setOrientation(LinearLayout.HORIZONTAL);
//クリックイベント登録
linearlayout.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
// TODO 自動生成されたメソッド・スタブ
Toast.makeText(TopActivity.this,user.getName() , Toast.LENGTH_LONG).show();
}
});
//横幅と縦幅の設定およびデータ間の間隔を調整する
LinearLayout.LayoutParams layoutparams = new LinearLayout.LayoutParams(FP,WC);
//left top right bottom
layoutparams.setMargins(0, 0, 0, 5);
linearlayout.setLayoutParams(layoutparams);
/*
* このサンプルでは個々のデータを縞模様で表示したいので
* 処理順番が偶数か奇数で背景色を設定する
*/
if( (i % 2) == 1){
linearlayout.setBackgroundResource(R.color.commentBgColorWhite);
}else{
linearlayout.setBackgroundResource(R.color.commentBgColorBlue2);
}
//画像と名前をまとめるレイアウトView
LinearLayout linearlayout_img_name = new LinearLayout(TopActivity.this);
linearlayout_img_name.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams linearlayout_img_name_params = new LinearLayout.LayoutParams(100,WC);
linearlayout_img_name.setLayoutParams(linearlayout_img_name_params);
//画像を管理するView
ImageView imgview = new ImageView(TopActivity.this);
//使用する画像を設定する
imgview.setImageResource(R.drawable.icon);
//画像表示枠内の余白を調整する(left,top,right,bottomをピクセル指定)
imgview.setPadding(0, 5, 5, 0);
//画像Viewの表示幅(横、縦)の設定
ViewGroup.LayoutParams v_layoutparams = new ViewGroup.LayoutParams(WC, WC);
imgview.setLayoutParams(v_layoutparams);
//画像Viewを登録する
linearlayout_img_name.addView(imgview);
//名前を管理するView
TextView username = new TextView(TopActivity.this);
ViewGroup.LayoutParams username_params = new ViewGroup.LayoutParams(WC, WC);
username.setLayoutParams(username_params);
//表示文字列の設定
username.setText(user.getName());
//テキストフォントColorの設定(黒)
username.setTextColor(Color.rgb(0, 0, 0));
linearlayout_img_name.addView(username);
//画像と名前をまとめたものを登録する
linearlayout.addView(linearlayout_img_name);
//テキストを管理するView
TextView textview = new TextView(TopActivity.this);
//テキストView内の余白を調整する(left,top,right,bottomをピクセル指定)
textview.setPadding(10, 5, 0, 0);
//テキストViewの表示幅(横、縦)の設定
ViewGroup.LayoutParams t_layoutparams = new ViewGroup.LayoutParams(WC, WC);
textview.setLayoutParams(t_layoutparams);
//テキストフォントColorの設定(黒)
textview.setTextColor(Color.rgb(0, 0, 0));
//表示する文字列の設定
textview.setText(user.getComment());
//テキストViewを登録する
linearlayout.addView(textview);
//1人分のLinearLayoutオブジェクトを登録する
linearlayout_inner.addView(linearlayout);
}
//スレッド間で受け渡すメッセージオブジェクト
Message msg = new Message();
//メッセージオブジェクトの識別値
msg.what = RESULT_OK;
//登録データ
msg.obj = linearlayout_inner;
//メッセージオブジェクトの登録
handler.sendMessage(msg);
}catch(Exception e){
Log.e("thread error!!!", e.toString());
}
}// end run()
}// end ThreadRunnable
}// end TopAcrivity
ポイントは2点です。
onCreateメソッド内においてActivityが起動した際に、Handlerオブジェクト
を生成し、handleMessageメソッドをオーバーライドしています。
このメソッドはHandlerオブジェクトがMessageオブジェクトを受信した際に
UIスレッドによって自動的に呼び出されます。
handler = new Handler(){
/*
* 生成したスレッドからデータを受信した際、画面ScrollView内に
* 受信したデータを登録する
*/
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
LinearLayout dataList = (LinearLayout)msg.obj;
//ScrollViewに追加する
scrollview.addView(dataList);
}
};//end Handler()
handlerMessageメソッドの引数には
内部スレッドが生成したMessageオブジェクトが代入されます。
今回このMessageオブジェクト内にはLinearLayoutオブジェクトが
保持されており、UIスレッドはMessageオブジェクトから
このLinearLayoutオブジェクトを取得しScrollView内に追加しています。
内部スレッド内の動作内容はThreadRunnableクラスの
runメソッドで定義してあります。
DatabaseHelperオブジェクトからデータベース内にある
ken_table表の全データをArrayListオブジェクトとして受け取っています。
ArrayListにはUserDataDtoオブジェクトが格納されており、
このオブジェクトには1ユーザー情報(ユーザー名、コメント)が
格納されています。
内部スレッドは、ArrayListからUserDataDtoオブジェクトを1つずつ取り出し、
1人分のレイアウトをLinearLayoutオブジェクトで作成した後、
それとは別な1つのLinearLayoutオブジェクトにどんどん格納していきます。
そして最後にMessageオブジェクトを生成し、
全データが入ったLinearLayoutオブジェクトを登録した後、
HandlerへMessageオブジェクトを渡すことで
UIスレッドにMessageオブジェクトが渡されることとなります。
//スレッド間で受け渡すメッセージオブジェクト
Message msg = new Message();
//メッセージオブジェクトの識別値(int型であれば何でもよい)
msg.what = RESULT_OK;
//登録データ
msg.obj = linearlayout_inner;
//メッセージオブジェクトの登録
handler.sendMessage(msg);
もし内部スレッドのrunメソッドにおいて、ScrollViewに追加する処理を作ると
実行時にエラーとなります。
public void run(){
省略
//ScrollViewに追加する
scrollview.addView(dataList);
省略
}//end run()
これは前述した通り、画面上のUI(コンポーネント)は
UIスレッドでしか操作できないからです。
UIスレッドとは別なスレッドがUIを操作・参照できないことから
今回はHandlerクラスを紹介しました。
Handlerクラス以外にも並行処理を実現するAsyncTaskクラスがありますが、
その詳細は別な機会に致します。
今回独自に作成したクラスがいくつかありますので
サンプルプロジェクトファイルを参照してください。
データベース内のデータは、全て同じアプリ内の別なアクティビティから
登録を行なっていますが、ネットワーク通信を行い得たデータを
データファイルに保存する形態も多いかと思います。
そのようなネットワーク通信処理については
次回の連載でお伝えする予定です。
--------------------------------------------------------------------------------
パソコンスクール KENスクール北千住校 Programインストラクター
http://www.kenschool.jp/school/shinjuku/index.html
KENスクールでAndroidアプリを開発したい方は、Program講座へ!
http://www.kenschool.jp/Program/index.html
- 次の記事へ: ITILV3取得講座 開講!!
- 前の記事へ: Android特集 第13回 Googleドキュメントを使ってみる
