2013年4月26日金曜日

launchModeとonNewIntentメソッド

android:launchModeとonNewIntentメソッドについてメモ。

android:launchMode
http://bit.ly/cuooyE

onNewIntent
http://bit.ly/g8n7QT


以下、やりたかったこと。


(1)自分のアプリのメインActivity(入力欄を持っている)からブラウザを起動。
(2)ブラウザのURL共有機能で上記のActivityを呼ぶ。
(3)この時、ブラウザ起動前に入力された内容が消滅しないこと。
(4)入力欄に共有URLをappendする。

android:launchModeにsingleTaskを指定すると(3)は実現できました。
デフォルトのstandardだとダメだったということです。

でも、それだけだと共有URL情報を持ったIntentを受け取れませんでした。
つまり、(4)が実現できませんでした。

なので、onNewIntentメソッドをOverrideしつつ書きました。
これで(4)も実現できました。


@Override
protected void onNewIntent(Intent intent) {
// TODO
}


伴い、以下の問題が発生しました。

(1)自分のアプリのメインActivity以外のActivityを表示する。
(2)homeボタン長押しで他のアプリを起動する。
(3)homeボタン長押しで自分のアプリを起動する。
(4)自分のアプリはメインActivityが表示される。

android:launchModeを書いていなかった時(すなわちデフォルトのstandardだっだ場合)は
(4)のタイミングで(1)のActivityが表示されていました。

ちなみにandroid:launchModeをsingleInstanceにしてみたところ、
やりたかったことも上記の問題も発生はしませんでした。
ただ、WebViewが正しく表示されなくなってしまいました。
コレはさすがに致命的だったので、とりあえずsingleTaskにしています。

アクティビティとタスク

「開発の基礎」の「アクティビティとタスク」の節がなかなか理解できなかったのでまとめ。

タスク
アクティビティのスタック。ブラウザでいえばHistoryみたいなもの。タスクそれぞれにアクティビティのスタックがある。

タスクはブラウザのタブみたいなもの。ただし、タブのように "見える化" されていなくてタスクに含まれるアプリケーションを再び起動しようとしたときにそのアプリケーションのタスクがフォアグラウンドになる。ただし、インテントのflagプロパティやアクティビティの起動モード定義によって挙動は変わる。

Affinity (親和性?)
アクティビティは自身と同じAffinityが設定されているタスクに所属しようとする。

タスクのAffinityはルートのアクティビティによって決まる。

アクティビティのAffinityはAndroidManifest.xmlのactivity要素のtaskAffinity属性で設定(たぶん任意の文字列で指定可能)。指定がなければ、application要素のtaskAffinity属性を継承する。アプリケーションのAffinityのデフォルトはmanifest要素のパッケージ名。

インテントにFLAG_ACTIVITY_NEW_TASK フラグが設定されている場合、新しいアクティビティは別のタスクに所属しようとするが、そのアクティビティと同じAffinityが設定されている既存のタスクがあればそこに追加される。なければ新しいタスクが開始される。

[あとで検証] もしFLAG_ACTIVITY_NEW_TASK フラグを設定したインテントで新しいアクティビティを起動するとき、startActivity()を呼び出したタスクと新しいアクティビティに同じAffinityが設定されていたら新しいタスクが起動するかどうか。たぶん新しいタスクはできないと予想。

activity要素に allowTaskReparenting="true" と設定されていると、このアクティビティと同じAffinityが設定されているタスクがフォアグラウンドに移ったときに、アクティビティを開始したタスクからそのタスクに移動できる。たとえば taskAffinity="A" のタスクで taskAffinity="B" のアクティビティを開始すると B は A のタスクに属するが、taskAffinity="B" のアプリケーションを起動すると B のアクティビティがこのタスクに移動する。

起動モード
activity要素の launchMode属性で定義。

"standard"
インテントを開始した(startActivity() を呼び出した)タスクに保持される。
複数回インスタンス化できる。
新しいインテントに応答するときには必ず新しいインスタンスが作成される。

"singleTop"
インテントを開始した(startActivity() を呼び出した)タスクに保持される。
複数回インスタンス化できる。
既存のインスタンスがスタックの最上位にあれば再利用して新しいインテントを処理する。最上位にない場合は新しいインスタンスが作成される。

"singleTask"
アクティビティが常にタスクのルート アクティビティになる。
アクティビティのインスタンスは1つに制限される。
同じタスクに属する別のアクティビティを開始することができる。
このアクティビティがスタックの最上位にない場合インテントはドロップされる。インテントがドロップされたとしても、タスクがフォアグラウンドに移ったままになる。

"singleInstance"
アクティビティが常にタスクのルート アクティビティになる。
アクティビティのインスタンスは1つに制限される。
そのタスク内の唯一のアクティビティとして単独で動作。ここから別のアクティビティを開始した場合、そのアクティビティは別のタスクで起動する。
後述するが、アプリケーションのメインアクティビティには singleTask が向いているはずだが、実際は上記のような挙動にならないため standard に設定しておくと良い。

スタックのクリア
ユーザーがタスクを長時間放置したときは、ルート以外のアクティビティがクリアされる。この挙動はactivity要素の属性で変更できる。

alwaysRetainTaskState = "true"

アクティビティはクリアされない。

clearTaskOnLaunch = "true"

タスクを離れるとルートを含めた全てのアクティビティがクリアされる。

finishOnTaskLaunch = "true"

タスクを離れるとこのアクティビティはクリアされる。それがルートであっても。

Intent に FLAG_ACTIVITY_CLEAR_TOP フラグを設定すると、インテントを処理するアクティビティのインスタンスが対象タスクのスタック内に存在する場合、そのインスタンスより上位(新しい方)のアクティビティはすべてクリアされる。

startActivityForResult() で呼び出したアクティビティがクリアされてしまった場合、呼び出し元アクティビティの onActivityResult() が resultCode = RESULT_CANCELED で呼ばれる。

タスクの開始
アクティビティのインテントフィルタのアクションを android.intent.action.MAIN、カテゴリを android.intent.category.LAUNCHER に設定すると、アクティビティのアイコンとラベルがアプリケーションランチャに表示され、アプリケーションのエントリポイントになる。

ドキュメントでは、この設定をしたアクティビティはユーザーが他のタスクに移動したあとでアプリケーションに戻ってこられるように、起動モード singleTask か singleInstance を設定したほうが良いとされているが、実際にやってみると起動モード singleTask ではルートアクティビティ以外がクリアされた状態で起動してしまう。なぜか起動モード standard のタスクではスタックが保持されたまま元のタスクに戻ることができた。

onNewIntent()

android.app.Activityクラス
protected void onNewIntent(Intent intent)
startActivity()を呼んだときに、これはlaunchModeに"simgleTop"と設定したアクティビティへ、あるいはFLAG_ACTIVITY_SINGLE_TOPフラグをつかったクライアント、呼ばれる。
onNewIntent()はアクティビティを再起動するために使用されたIntentとともに既存のインスタンス上で呼び出される。
アクティビティは常に新しいインテントを受信する前に一時停止されるので、このメソッドの後にonResume()が呼ばれることを期待できる。
getIntent()はまだオリジナルのインテントを返すことに注意せよ。setInent()で新しいインテントに更新することができる。
引数
intent アクティビティに向けてスタートさせられた新しいアクティビティ

startActivity()で新しいアクティビティが作られるのではなく既存のアクティビティが使われる場合にこのメソッドが呼ばれる。
AndroidManifest.xmlのactivity要素のlaunchMode属性の値によって、新しいアクティビティのインスタンスが作られてインテントを処理するのか、既存のアクティビティがインテントを処理するのかが決まる。
リファレンスのメソッドの説明では"singleTop"しか書かれていないが、singleTaskとsingleInstanceの場合もこのメソッドが呼ばれる場合がある。
下記の参考ページで非常にわかりやすく説明されている。

本に載っていたTwitterクライアントでは、launchModeはsingleInstanceになっていた。
アプリの動きは
WebViewでTwitterの認証画面を表示
->認証に通ったら"myapp://mainactivity"というURLデータを持ったIntentがこのクライアントに投げられる(startActivity()が呼ばれる)。
->このクライアントのインスタンスのonNewIntent()が投げられたIntentを処理する。
となっていると推測している。
#きちんと調べてないので推測でしかない。

AndroidManifest.xmlのactivity要素。
<activity>
android:label="@string/app_name"
android:name=".Twinto"
android:launchMode="singleInstance" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="mainactivity" />
</intent-filter>
</activity>
デバッグログの出力
xx-xx xx:xx:xx.xxx: V/TEST(xxx): onNewIntent():action=android.intent.action.VIEW,category=[android.intent.category.BROWSABLE],uri=myapp://mainactivity?oauth_token=xxxxxxxxxx&oauth_verifier=xxxxxxxxxx

2013年4月25日木曜日

HandlerThreadを使うと何が出来るのか

HandlerThreadは、Handler経由でメッセージをsendできる拡張スレッド、のようです。下記は調べたメモです。

IntentServiceのソースコード
HandlerThreadを調べる発端となったIntentServiceは、Serviceのサブクラスです。ソースを見てみると、onCreate時にHandlerThreadを生成して、スタートさせています。

IntentService.javaのonCreate
@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
さて、このHandlerThreadとは何者でしょうか。

HandlerThreadは内部にlooperを持つスレッド
HandlerThreadは、UIスレッドのようなlooperを内部に持つ拡張スレッドのようです。

Handlerは、引数なしで生成すると生成時のスレッドのlooperがsend先になります。

Handler.javaの引数なしコンストラクタ
public Handler() {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = null;
}
HandlerをUIスレッド以外からnewすると「Can't create handler inside thread that has not called Looper.prepare()」とよく怒られていたんですが、やっと合点がいきました。引数なしで生成されたHandlerは問答無用でUIスレッドにメッセージをsendするわけではなくて、newされたスレッドのlooperに対してメッセージをsendしていたんですね。

IntentService#onCreateでやっているように、new Handler(Looper looper)のコンストラクタを使うと、引数で渡されたlooperに対してメッセージをsendできるようです。

HandlerThreadを試してみる
上記を踏まえて、HandlerThreadの動作を試してみます。

まず、UIスレッドにHandler経由でメッセージをsendするコードです。

public class SampleHandlerThreadActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btn1 = (Button) findViewById(R.id.button1);
btn1.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
Handler mainHandler = new Handler();
mainHandler.post(new Runnable() {
public void run() {
Log.v("hoge", "thread name:" + Thread.currentThread().getName());
}
});

}
});
}
}
実行結果
04-10 15:41:58.006: VERBOSE/hoge(902): thread name:main

postされたrunnableが、mainスレッド(つまりUIスレッド)で実行されたことが確認できます。

これを、post先を独自のスレッドになるように書き換えてみます。

独自のスレッドにHandlerからpostする
public class SampleHandlerThreadActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btn1 = (Button) findViewById(R.id.button1);
btn1.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
HandlerThread hogeThread = new HandlerThread("hogeThread");
hogeThread.start();
Handler hogeThreadHandler = new Handler(hogeThread.getLooper());
hogeThreadHandler.post(new Runnable() {
public void run() {
Log.v("hoge", "thread name:" + Thread.currentThread().getName());
}
});


}
});
}
}
実行結果
04-10 15:49:14.966: VERBOSE/hoge(951): thread name:hogeThread

hogeThreadで、postしたrunnableが実行されていることを確認できました。

HandlerThreadはrunの中でLooper.prepare()する
なお、上記のコードではhogeThreadをstartさせてからHandlerをnewしています。HandlerThreadはstartされてからはじめてlooperを自身に設定するためです。

startするまえにgetLooperしてもnullが返るので、new Handler(Looper)は失敗します。

HandlerThreadのrun()
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

androidでHandlerThread,Handler,Handler.Callbackを使う

androidでネットワークプログラミングを
書くときには普通に書くとANRが発生するので
別スレッドで処理が必要。

でもこれをきれいに書く方法を知らなかったのですが
HandlerThread
Handler
Handler.Callback
を使えばきれいに書けると分かったので
そのやり方

話すよりコード出した方が早いので
以下。


public void hogehoge(){
HandlerThread handlerThread = new HandlerThread("test");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper(), callback);

Message mes = new Message();
mes.what = 1; //数字は適当
mes.arg1 = 2; //数字は適当
handler.sendMessage(mes);
}

Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch(msg.what){
case 1:
//ネットワークの処理
break;
return true;
}
};


HandlerThread作ってlooperとコールバック渡してhandlerを作ればよい。
後はsendしてsend先で処理を書けば
割ときれいなんじゃないでしょうかね。

Toastを表示したスレッドについて

AsyncQueryHandler という Content Resolver を経由してデータ取得を非同期に行う処理に関連して見つかりました。このAsyncQueryHandler クラスを調べると、コンストラクタでHandlerThread というインスタンスを生成してそれを使っていることがわかります。

AsyncQueryHandlerのコンストラクタ

public AsyncQueryHandler(ContentResolver cr) {
super();
mResolver = new WeakReference(cr);
synchronized (AsyncQueryHandler.class) {
if (sLooper == null) {
HandlerThread thread = new HandlerThread("AsyncQueryWorker");
thread.start();

sLooper = thread.getLooper();
}
}
mWorkerThreadHandler = createHandler(sLooper);
}


HandlerThreadとは?


このHandlerThreadがポイントなんだろうなと思ったので、これについて調べてみました。HandlerThread <http://developer.android.com/intl/ja/reference/android/os/HandlerThread.html> は内部にlooperを持つスレッドということのようです。looperってなんだったっけ?と思い出すと、スレッドの内部でメッセージキューを処理するものだったように思います(詳しくはAndroid のHandlerとは何か? <http://www.adamrocker.com/blog/261/what-is-the-handler-in-android.html> などを参考にしてください)。

次に、普通に別スレッドでToastを表示したときのエラーを確認すると、

Can't create handler inside thread that has not called Looper.prepare()


とログに出力されていると思います(『現在のスレッドではLooper.prepareが呼び出されていないのでHandlerが作れない』、の意味)。

それではと、Handlerのソースを見てみると、


public Handler() {
if (FIND_POTENTIAL_LEAKS) {
final Class klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = null;
}




とあり、Looperが無い場合はエラーで落としています。

Looper.mylooper() <http://developer.android.com/intl/ja/reference/android/os/Looper.html#myLooper%28%29> を見ると、実行中のスレッドのLooperインスタンスを取得するもののようです。また、Looper <http://developer.android.com/intl/ja/reference/android/os/Looper.html> の説明を見ると、通常のスレッドはlooperがないけど、prepare()を呼び出すことで、そのスレッドにlooperを準備するということのようです。

ということで、上記のエラーはlooperがないスレッドからToastを表示しようとして発生しており、一方HandlerThreadはlooperがあるので、問題なく表示できた、ということになりそうです。

(参考サイト)

* 別スレッドでキュー管理(Handler, Looper, HandlerThread) <http://daichan4649.hatenablog.jp/entry/20111004/1317724067>
* HandlerThreadとHandlerとLooperの関係 <http://d.hatena.ne.jp/kaw0909/20110410/1302418486>
* Handlerクラスの正しい使い方(Androidでスレッド間通信) <http://d.hatena.ne.jp/sankumee/20120329/1333021847>

Toastの振る舞い
では、Toastはどうなっているのかをみてみます。ToastのstaticメソッドのmakeTextを見ると、最初にToastオブジェクトを生成しています。

public static Toast makeText(Context context, CharSequence text, int duration) {
Toast result = new Toast(context);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}


Toastのコンストラクタの処理は下記の通りでした。

public Toast(Context context) {
mContext = context;
mTN = new TN();
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
}


ここで、TNはprivate宣言された内部クラスであり、これを見ると、

private static class TN extends ITransientNotification.Stub {
final Runnable mShow = new Runnable() {
public void run() {
handleShow();
}
};

final Runnable mHide = new Runnable() {
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};

private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
final Handler mHandler = new Handler();

int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;


とあるように、メンバ変数でHandlerを引数なしでnewしています。

どうもこのために、UIスレッド以外のlooperがないスレッドで、Toastを表示しようとするとnew Handler() の箇所でエラーになってるように思われます。


サンプルコード


上記のことから、Looper を持つスレッド上であれば、UI スレッドではなくてもToast が表示できるのではないかと推測されます。そこで、下記のようなサンプルコードを作成してみました。

package com.mori_soft.android.toasttest;

import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button button1 = (Button) this.findViewById(R.id.button1);
Button button2 = (Button) this.findViewById(R.id.button2);
Button button3 = (Button) this.findViewById(R.id.button3);
Button button4 = (Button) this.findViewById(R.id.button4);

// ボタンを押したら、Toastを表示する


// (1)UIスレッドから直接呼び出しの場合
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
displayToast();
}
});

// (2)別スレッドから呼び出しの場合(エラー発生)
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
displayToastNotUiThread();
}
});

// (3)別スレッドからHandler経由で呼び出し場合(成功)
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
displayToastThroughHandler();
}
});

// (4)別スレッドでの呼び出しの場合
button4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
displayToastThroughHandlerThread();
}
});
}

private void displayToast() {
final long id = Thread.currentThread().getId();
final String thName = Thread.currentThread().getName();

Toast.makeText(MainActivity.this, "Toast displayed. thread id:" + id + ", name:" + thName, Toast.LENGTH_LONG).show();
Log.d("MainActivity", "Toast displayed. thread id:" + id + ", name:" + thName);
}

private void displayToastNotUiThread() {
new Thread(new Runnable() {
@Override
public void run() {
displayToast();
}
}, "TestThread#1").start();
}

private void displayToastThroughHandler() {
final Handler h = new Handler();
new Thread(new Runnable() {

@Override
public void run() {
Log.d("MainActivity", "thread run() called. thread name:" + Thread.currentThread().getName());
h.post(new Runnable() {
@Override
public void run() {
displayToast();
}
});
}
}, "TestThread#2").start();
}

private void displayToastThroughHandlerThread() {
final HandlerThread ht = new HandlerThread("TestThread#3");
ht.start();

Handler h = new Handler(ht.getLooper());
h.post(new Runnable() {
@Override
public void run() {
displayToast();
}
});

// 別スレッドを停止
//ht.quit();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}


Activityにボタンを4つ配置し、ボタンを押すと異なる方法でToastを表示するものです。Toast表示時にスレッドのidと名前を表示します。

(1)がUIスレッド、(2)が別スレッドだけどHandlerを使わないのでエラーになる方法、(3)が別スレッドからHandler経由で表示する方法、(4)が別スレッド上で表示する方法になります。

実験

では、試してみましょう。(1)はUIスレッドで、(3)は別スレッドからUIスレッドにToastを投げて表示します。どちらの場合もUIスレッド(mainスレッド)でToastが処理され、表示されます。一方、(3)の場合は実行時のログを見ると、
Handler#run()は別スレッド(スレッド名が、TestThread#2)で動いていますが、Toast表示処理はUIスレッドになっていることがわかります。

(2)は上記で示したようなエラーになります。(4)は下記画面に示すように、別スレッド(スレッド名がTestThread#3)上でToastが処理されていることがわかります。
このようにあっけないぐらい簡単に別スレッドから表示できました。

なお、上記のサンプルでは、HandlerThread#quit() を呼び出していませんが、適切なタイミングでquitを呼びスレッドを終了させるほうがよいと思います。

まだ疑問が・・・・
Toastを別スレッドで表示することができましたが、とはいえ、UIに関する処理をしているのに本当にこれで良いのか?なぜ別スレッドで処理できる?という疑問が残ります。

Toastの内部では、NotificationManagerService というサービスを呼出しており(呼び出す際の名称は"notification")、Toast#show()でこのサービスのキューに表示する情報を入れています。

なので、このあたりでよしなにやってくれてるんだろうと思うのですが、これ以上追いかけれませんでした。また、時間ができたら見てみたいと思います。

あと、現時点では、上記で見たようにLooper を持つ別スレッドでToastが表示できるとしても、これが効率的なやり方なのか、推奨のやり方なのかという点は正直よくわかりませんし、アプリの種類によってはうまくいかなかったりする可能性もないとは言い切れません。

なので、こういうことができた、という参考程度に捉えておいてもらえるとありがたいです。

なお、下記にもHandlerThraedを使ったToast表示の例が載ってました。

Can't create handler inside thread that has not called Looper.prepare() <http://stackoverflow.com/questions/3875184/cant-create-handler-inside-thread-that-has-not-called-looper-prepare>

別スレッドでキュー管理(Handler, Looper, HandlerThread)

非同期処理を組んでると、
「別スレッド上でも Handler みたいなキュー管理がしたい」
って時があったりしますよねー。(きっと)


そんなとき、HandlerThread を使うとわりと簡単に実装できたりする。


HandlerThread(公式)


HandlerThread は java.lang.Thread を継承したクラスで、
android標準の Handler と組み合わせて使うための仕組みが入ってたり。


具体的な使い方。


まずは Handler(defaultコンストラクタ) を使う場合。

@Override  public void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.main);        Handler handler = new Handler();      handler.post(new Runnable() {          @Override          public void run() {              System.out.println(Thread.currentThread().getName());          }      });  }  

実行すると当然 「main」 と出力される。
当然、mainスレ上でキューが実行される。
これは問題ないですね。


次に Handler+HandlerThread の場合。

@Override  public void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.main);        // 別スレ生成 -> 開始      HandlerThread handlerThread = new HandlerThread("other");      handlerThread.start();        Handler handler = new Handler(handlerThread.getLooper());      handler.post(new Runnable() {          @Override          public void run() {              System.out.println(Thread.currentThread().getName());          }      });  }  

実行すると 「other」 と出力される。
キューが実行されてるスレッドが mainスレ から 別スレ に変更されます。
もちろんこの Handlerインスタンス は別スレ上で使用してOK。


手順をまとめるとこんなかんじ。

  1. 別スレッド(HandlerThread)を生成
  2. 別スレ開始
  3. HandlerThreadインスタンス から Looperインスタンス 取得
  4. Handlerインスタンスを生成(3で取得した Looperインスタンス を 引数指定)
  5. 4 で作成した Handlerインスタンス を使用(Handler#post 等)


今まで PriorityBlockingQueue とか使って、
自前のキュー管理クラス作ってた自分涙目。
キューをFIFOで実行するだけのキュー管理であれば、
このやり方でまったく問題なさそうですね。


framework ではどうやってるのかなーと思い、
Looper、Handler、HandlerThread のソースを読んでみた。


ポイントはこのあたり。

  • コンストラクタ Handler(default) は「mainスレッド上で作成された」 Looperインスタンス を使用
  • コンストラクタ Handler(Looper) は「引数で渡された」 Looperインスタンス を使用
  • Looperインスタンスは Looper#prepare を呼び出すと生成される(ThreadLocal保持)
  • HandlerThread は Thread を継承したクラス
  • HandlerThread#run の中で Looper#prepare を呼び出している


Looperインスタンスの生成場所 の違い。

2013年4月24日水曜日

IntentService - 非同期、自動終了、キュー・・・便利なサービスの実装

サービスの実装にかかわる問題点、注意点は 単純なサービス などで説明しました。

ポイントは、サービスの処理はそれを起動するスレッドとは別の作業用スレッドで行うべき、処理終了時は明示的にサービスを stopService (または stopSelf) で終了状態にするなどです。

作業用のワーカースレッドを作り、そこで作業を行うのは一般的に煩雑な処理になりがちなのですが、サービスの実装においては IntentService を利用することで非常に簡単に実装することができます。

IntenetService の利用方法

IntentService を利用するには Service クラスではなく、次のように IntentService クラスを派生します。 そしてサービス固有の、一般的に時間のかかる処理を onHandleIntent で行います。

package com.keicode.android.test;    import android.app.IntentService;  import android.content.Intent;  import android.util.Log;    public class MyIntentService extends IntentService {    	final static String TAG = "MyIntentService";  	  	public MyIntentService() {  		super("MyIntentService");  	}    	@Override  	protected void onHandleIntent(Intent intent) {		  		try {  			Log.d(TAG, "onHandleIntent");  			Thread.sleep(5000);  		} catch (InterruptedException e) {  			e.printStackTrace();  		}  	}    	@Override  	public void onDestroy() {  		super.onDestroy();  		Log.d(TAG, "onDestroy");  	}    }  

上記を呼び出すと次のようなログが記録されます。

ここで実装した Android IntentService 実行後のログの様子

onHandleIntent はひとつのワーカースレッドで処理される

onHandleIntent はメインスレッドではなく、専用のワーカースレッド (worker thread) で処理されます。コンストラクタに渡した文字列は、このワーカースレッドの名前になります。

上記コードを実行時にデバッガでスレッドをみてみると、確かにコンストラクタに渡した名前がスレッドの名前になっていることが確認できます。

デバッガで見たワーカースレッド

IntentService が複数ある場合は、この名前でそれぞれを区別するとよいでしょう。

順次処理 -「ワークキュープロッセサ」パターン

ひとつのワーカースレッドで処理をするために、onHandleIntent メソッド実行時にさらに同サービスの開始リクエストが行われた場合は、 その要求はただちには処理されずキューに置かれます。

ワークキューにおかれる

こうした処理の流れ・パターンを "ワークキュープロセッサ" (work queue processor) パターンなどといいます。

キュー内の処理が全て終わるとサービス停止

onHandleIntent が実行し終わったときに、ワークキューをチェックして、待ち作業がないときに、onDestroy が呼ばれます。

onDestroy は最後に一回

一回の onHandleIntent に対応して一度ずつ onDestroy が呼ばれるわけではないことに注意が必要です。必要なクリーンアップ処理は onDestory にまとめないで、 onHandleIntent 内で行うことも検討すべきかもしれません。

明示的にサービスを停止する必要なし

上で書いた通り、サービスの停止はキュー内の作業が全て終わったときに自動的に行われます。これは stopService や stopSelf を明示的に呼ばなくても行われます。

stopService あるいは stopSelf を呼ぶ必要が無いため、Service を直接派生したときにしなければならないサービスの止め忘れを未然に防ぐことができます。

以上説明した通り、IntentService はかなり便利なのでサービス実装時には利用を検討すべきでしょう。

IntentService

IntentService is a base class for Services that handle asynchronous requests (expressed as Intents) on demand. Clients send requests through startService(Intent) calls; the service is started as needed, handles each Intent in turn using a worker thread, and stops itself when it runs out of work.

This "work queue processor" pattern is commonly used to offload tasks from an application's main thread. The IntentService class exists to simplify this pattern and take care of the mechanics. To use it, extend IntentService and implement onHandleIntent(Intent). IntentService will receive the Intents, launch a worker thread, and stop the service as appropriate.

All requests are handled on a single worker thread -- they may take as long as necessary (and will not block the application's main loop), but only one request will be processed at a time.

IntentService

先日のGoogleDeveloperDays2010JapanでAndroidの高速化の際に出てきてたクラス。

気になってググったらやっぱり日本語の説明があんまりないのでメモ的に簡単に説明。

ServiceをオーバーライドしたIntentService

任意の特定の処理をメインスレッドとは別のスレッドで非同期で行うためのクラス。

このサービスは通常メインスレッドでハンドリングしないような処理を行うためのものらしい

呼び出し方はサービス的だけれども同一プロセス内で別スレッドを立ち上げて処理する。

実際に実装するには、IntentServieを拡張しonHandleIntent内に特定の処理を記述する。


IntentService拡張

サービスとしてのクラスなのでインナークラスでの記述は不可能

public class CustomIntentService extends IntentService{  	private static final String name="TEST";    	public CustomIntentService() {  		super(name);  	}  	@Override  	protected void onHandleIntent(Intent intent) {  		//ここに時間のかかる重い処理を記述  	}  }  

Activity側での呼び出し
    @Override      public void onResume(){      	super.onResume();      	Intent intent = new Intent(Intent.ACTION_SYNC,null,this,CustomIntentService.class);      	this.startService(intent); //ここからスタート!      }  

ちゃんとAndroidManifest.xmlにも記述
    <application android:icon="@drawable/icon" android:label="@string/app_name">          ・          ・          ・      <service android:name="CustomIntentService"></service> //先ほどの拡張クラスを明記!      </application>  

そんなIntentServiceの中身

中身はHandlerThreadを1つ作ってそのループにキューを投げるというだけっぽいので、

たとえば同時に複数回startServiceメソッドを呼び出したとしてもここで要求される全てのリクエストは、

サービスを開始した順に同一プロセス内でシングルスレットで一つずつ処理される。

すべてのキューが終了したら通常のスレッドと同じように破棄される、という流れなのかな。

とはいえ、AsyncTaskと違うのはIntentServiceはService自身をスタートさせたActivityに依存せずに処理が続く。

AsyncTaskのように元Activityの生き死にで処理の中断などはされない。

AndroidManifest.xml 詳細

Manifest File のルート要素です。

  • package

    アプリケーションのパッケージ名を Full Qualified Name で指定します。
    これは以下の用途で利用されます。

    • 基準パッケージ名

      コンポーネントのクラス名など、このパッケージからの相対パス名で記述できます。

    • プロセス名

      アプリケーションが実行するときのデフォルトプロセス名として使用されます。

    • Affinity

      コンポーネントのデフォルト Affinity として使用されます。

  • android:sharedUserId

    他のアプリケーションと共有したいLinuxユーザIDを指定します。
    これによって、他のアプリケーションと同じプロセスでアプリケーションを稼動させることができます。
    これは同じ証明書によって署名されたアプリケーションである必要があります。
    つまり、偶然他のアプリケーションと同じユーザIDを指定していたとしても
    それが別の作者によって署名されたアプリケーションならば両者は同じユーザIDを共有することができません。

  • android:sharedUserLabel

    Human Readable なユーザ名を指定します。
    これは android:sharedUserId を指定した場合のみ有効です。

  • android:versionCode

    アプリケーションのバージョン番号を、整数で指定します。
    この値は、同一のアプリケーションが二つあったとき、そのどちらが新しいかを決定する目的で使用されます。
    値が大きいものほど、新しいと判断されます。

  • android:versionName

    ユーザに見せる目的のバージョン名を指定します。
    例えば、3.2.10a のように。ここには任意の文字列が使えます。

<uses-permission>

Permission を許可するために使います。

  • android:name

    許可したい Permission 名を指定します。
    ここにはシステムが提供する、もしくはユーザが独自に定義した名前を使う必要があります。
    Android はデフォルトで全ての Permission が拒否されています。
    つまり、ここに記述した Permission だけが許可されるという事です。

<permission>

ユーザ独自の Permission を定義します。

  • android:icon

    この Permission を表現するアイコンを指定します。

  • android:label

    この Permission のラベル名称を指定します。この文字列はユーザに見せる目的で使われます。

  • android:name

    この Permission の名称を指定します。
    これが ID となります。
    "com.example.project.PERMITTED_ACTION" のようなフォーマットが一般的です。

  • android:protectionLevel

    この Permission が持つ危険度を指定します。
    "normal" / "dangerous" / "signature" / "signatureOrSystem" が使えます。

<permission-tree>

このアプリケーションで定義する Permission の基準名を指定します。

<permission-group>

Permission グループを定義します。

<instrumentation>

Instrumentation を定義します。
これを使うと、アプリケーションを様々な方式で監視(モニタリング)することができます。

<uses-sdk>

このアプリケーションがどの Android SDK で動作可能なのかを定義します。

  • android:targetSdkVersion

    このアプリケーションがターゲットとしている API Level を整数で指定します。

  • android:maxSdkVersion

    このアプリケーションが動作する最大の API Level を整数で指定します。

<uses-configuration>

このアプリケーションが必要とするハードウェアやソフトウェアの機能を指定します。
例えば、ハードウェアキーボードが必須であるとか、タッチスクリーンでなければ動かないとか、です。

<uses-feature>

このアプリケーションがどのような機能を使っているかを定義します。
例えば、オートフォーカス機能を持ったカメラ機能が必須である、とかです。

<supports-screens>

このアプリケーションが対応する画面の解像度を指定します。

<application>

アプリケーションのメイン要素です。
この子要素に、様々な要素を指定することができます。

  • android:allowClearUserData

    ユーザがこのアプリケーションで使うユーザデータを削除できるかどうかを指定します。
    デフォルトは "true" です。

  • android:allowTaskReparenting

    このアプリケーション内の Activity が Task の親を変更可能かどうかを指定します。
    デフォルトは "false" です。
    これは Activity 単位でも定義できます。

  • android:debuggable

    このアプリケーションがデバッグ可能かどうかを指定します。
    デフォルトは "false" です。

  • android:enabled

    アプリケーション内のコンポーネントを初期化することが可能かどうかを指定します。
    デフォルトは "true" です。

    これはコンポーネント単位でも定義できます。

  • android:hasCode

    アプリケーションが自身のコードを持っているかどうかを指定します。
    デフォルトは "true" です。

    これは、AliasActivity などの特殊な場面でのみ使われる属性です。

  • android:icon

    アプリケーションのデフォルトアイコンを指定します。
    これはコンポーネント単位でも定義できます。

  • android:label

    アプリケーションのラベル名称を指定します。

  • android:manageSpaceActivity

    デバイス上のアプリケーションに使用されるメモリを管理する Activity 名を Full Qualified Name で指定します。
    この Activity は <activity> 要素で定義されている必要があります。

  • android:name

    Application を継承したクラス名を Full Qualified Name で指定します。
    この指定は通常必要ありません。この場合、Android は基本 Application クラスを使います。

  • android:permission

    クライアントがアプリケーションを使うために必要な Permission 名を指定します。
    これは、アプリケーション全体で Permission を指定する簡単な方法です。
    各コンポーネントで個別に Permission を指定することもできます。

  • android:persistent

    アプリケーションが常時稼動するべきかどうかを指定します。
    デフォルトは "false" です。

    アプリケーションは通常このフラグをセットするべきではありません。
    persistence mode は特別なシステムアプリケーションでのみ使うことを意図されたものです。

  • android:process

    このアプリケーションを実行するプロセス名を指定します。
    各コンポーネントで個別に指定することもできます。

    デフォルトでは、Android は最初にコンポーネントを作成するときに
    プロセスを作り、全てのコンポーネントはそのプロセス上で実行されます。
    デフォルトのプロセス名は、<manifest> 要素で定義されたパッケージ名になります。

    この属性を指定することで、他のアプリケーションと同じプロセス上でアプリケーションを動かすことが可能になります。
    ただし、両者のアプリケーションは同じユーザIDを共有していて、同じ作者によって署名されていることが必要です。

    コロン(:)で始まるプロセス名を付けると、このアプリケーションに private な新しいプロセスが作成されます。
    小文字で始めるプロセス名を付けると(これが通常)、グローバルプロセスが作成されます。
    グローバルプロセスは、リソースの使用量を減らすために他のアプリケーションと共有することができます。

  • android:taskAffinity

    このアプリケーションのデフォルト Affinity を指定します。
    デフォルトの Affinity は、<manifest> 要素で定義されたパッケージ名になります。

  • android:theme

    アプリケーションで採用するテーマ名を指定します。

<activity>

Application と同じ要素がいくつかあります。

android:enabled / android:icon / android:label / android:name
android:permission / android:process / android:taskAffinity / android:theme

以上については、Application の方を参照して下さい。

  • android:allowTaskReparenting

    この Activity が、Task の親を変更可能かどうかを指定します。
    詳しい説明は、allowTaskReparenting を読んで下さい。
    この属性は android:launchMode が "standard" または "singleTop" の Activity でのみ有効です。
    ※ それ以外の launchMode では、この Activity は常に自身が親となる

  • android:alwaysRetainTaskState

    Task の状態を保持するかどうかを指定します。
    "true" にすると、Task の状態は常に保持されます。
    "false" の場合、システムは Task を長時間放置した場合にそれを初期状態にリセットすることが許されます。
    このとき、RootActivity 以外の Activity が全てクリアされます。
    デフォルトは "false" です。

    この属性は Task の RootActivity のみで有効です。

  • android:clearTaskOnLaunch

    "true" にすると、ユーザが一瞬でもこの Activity を含む Task から離れると
    次に戻ってきたときには RootActivity も含めて 全ての Activity を Task からクリアし
    最初からやり直しさせます。
    デフォルトは "false" です。

    この属性は Task の RootActivity のみで有効です。 

  • android:configChanges

    ここに指定した設定が変更されたとき、この Activity がそれをハンドリングできるようにします。

    ここに指定されていない設定が変更されると、この Activity はシャットダウンされ、再起動します。
    ここに指定してある設定が変更されると、onConfigurationChanged() が呼ばれ Activity は処理を継続できます。

    以下に設定の種類を挙げます。これらは | で繋ぐことができます。
    例えば、"locale|navigation|orientation" のように指定します。

    意味
    "mcc" 端末の地域コードの変更をキャッチします。
    "mnc" 端末のネットワークコードの変更をキャッチします。
    "locale" ロケールの変更をキャッチします。
    "touchscreen" タッチスクリーンの変更をキャッチします。(通常こんなことは起きません)
    "keyboard" キーボードの種類の変更をキャッチします。例えばユーザが外部キーボードを接続した場合などです。
    "keyboardHidden" キーボードの Accessibility の変更をキャッチします。例えばユーザが端末をスライドさせてキーボードをしまった時などです。
    "navigation" ナビゲーションの種類の変更をキャッチします。(通常こんなことは起きません)
    "orientation" 端末の縦横を反転させたことをキャッチします。
    "fontScale" フォントサイズの変更をキャッチします。

  • android:excludeFromRecents

    "true" にすると、最近実行した Activity のリストに自身が載らなくなります。
    デフォルトは "false" です。

  • android:exported

    この Activity が外部から呼び出されるかどうかを指定します。
    "false" にすると、この Activity は同一アプリケーション内、または同一ユーザIDを持つ
    アプリケーション内のコンポーネントからのみ起動できます。

    属性のデフォルト値は、この Activity が持つ Intent Filter によって変化します。
    もし Activity が Filter を一つも持っていなかったら、この Activity は明示的なクラス名によってのみ
    呼び出されると判断されます。これは、Activity が外部から呼び出されないことを意味します。
    なぜなら、他のアプリケーションはこの Activity のクラス名をおそらく知らないはずだからです。
    この場合、デフォルト値は "false" になります。

    もし一つでも Filter が定義されていれば、Activity は外部から利用されると判断されます。
    この場合、デフォルト値は "true" になります。

    この属性は、この Activity を他のアプリケーションから使用することを制限する唯一の要素ではありません。
    permission 属性を使い、呼び出しの制限を掛けることもできます。

  • android:finishOnTaskLaunch

    この属性を "true" にすると、ユーザが一瞬でも Task を離れるとこの Activity がシャットダウンされます。
    デフォルトは "false" です。

  • android:multiprocess

    "true" にすると、この Activity のインスタンスは呼び出し元のプロセスで作成されます。
    この場合、インスタンスは複数のプロセス上で動く可能性があります。

    デフォルトは "false" です。
    この場合、Activity のインスタンスは常に定義されたプロセスと同一のプロセス上で作成されます。
    従って、全てのインスタンスは同一のプロセス上で動きます。

  • android:noHistory

    "true" にすると、この Activity はユーザ上から見えなくなったときに Task から取り除かれ finish() がコールされます。
    デフォルトは "false" です。

  • android:screenOrientation

    この Activity の方位を指定します。

    "unspecified" これがデフォルトです。方位は特定しません。
    "landscape" 横長の画面です。
    "portrait" 縦長の画面です。
    "user" ユーザが現在設定している方位を使います。
    "sensor" 端末のセンサーによって方位を決定します。
    "nosensor" "unspecified" と同じです。

  • android:stateNotNeeded

    この Activity が、状態を保存しなくても正常に再起動できるかどうかを指定します。
    デフォルトは "false" です。

    通常、Activity はシャットダウンする前に onSaveInstanceState() が呼び出されて値を一時的に保存します。
    このメソッドは、現在の状態を Bundle オブジェクトに格納し
    Activity が再起動されたときに onCreate() メソッドの引数として渡されます。

    この属性を "true" にすると、onSaveInstanceState() は呼び出されず、onCreate() には null が渡されます。
    つまり、常に Activity を最初に起動したときと同じ挙動になります。

  • android:windowSoftInputMode

    ソフトウェアキーボードに関する設定を記述します。

    以下に設定の種類を挙げます。これらは | で繋ぐことができます。

    "stateUnspecified" キーボードの状態について、特に指定はしません。これがデフォルトです。
    "stateUnchanged" キーボードの状態を、前回から変化させません。
    "stateHidden" Activity がアクティブ化されたとき、キーボードを隠します。
    "stateAlwaysHidden" この Activity にフォーカスがある間中、キーボードを表示しません。
    "stateVisible" Activity がアクティブ化されたとき、キーボードを表示します。
    "stateAlwaysVisible" この Activity にフォーカスがある間中、キーボードを表示したままにします。
    "adjustUnspecified" キーボードのサイズについて、特に指定しません。
    "adjustResize" キーボードを表示させるために、Activity をリサイズします。
    "adjustPan" 詳細は不明です。

<intent-filter>

Intent Filter の親要素を定義します。
この中に action, category, data の子要素を定義します。

親要素 : <manifest> / <activity> / <activity-alias> / <service> / <receiver>

android:icon / android:label

  • android:priority

    この Intent Filter によって応答するコンポーネントの、優先度を1〜100で指定します。

<action>

Intent Filter でフィルタリングするアクションを定義します。

親要素 : <intent-filter>

  • android:name

    アクション名を定義します。Intent クラスに定義された定数が使えます。
    "android.intent.action.WEB_SEARCH" のようなものがあります。

<categoty>

Intent Filter でフィルタリングするカテゴリを定義します。

親要素 : <intent-filter>

  • android:name

    カテゴリ名を定義します。Intent クラスに定義された定数が使えます。
    "android.intent.category.LAUNCHER" のようなものがあります。

<data>

Intent Filter でフィルタリングするURIを定義します。
URI は scheme://host:port/path で構成されます。

親要素 : <intent-filter>

  • android:host

    ホスト名を指定します。
    これは android:scheme が定義された場面でのみ有効です。

  • android:mimeType

    MIME Type を指定します。* によるワイルドカードが使えます。

  • android:pathPattern

    パスを指定します(完全一致)。* によるワイルドカードが使えます。

  • android:port

    ポート番号を指定します。 
    これは android:scheme および android:host が定義された場面でのみ有効です。

  • android:scheme

    スキーマを指定します。
    "http" のように、コロンは付けずに指定します。
    この値は必ず一つ以上指定して下さい。そうでないと、他の指定が有効になりません。

<meta-data>

Name - Value 形式の値を定義します。
この値はコンポーネントから使うことができます。

親要素 : <manifest> / <activity> / <activity-alias> / <service> / <receiver>

<activity-alias>

Activity のエイリアス(別名)を定義します。
エイリアス対象の Activity は、同一の Manefest File 内、しかも
この要素より前に定義しておく必要があります。

<service>

サービスを定義します。

<receiver>

Broadcast Receiver を定義します。

<provider>

Content Provider を定義します。

<uses-library>

このアプリケーションで必要な外部ライブラリを定義します。