先日の「スレッドからオブジェクトをイジると異常終了する」件。
2つの方法で解決した。
おそらくは邪道な方法だと思うので、参考ぐらいにとどめた方がいいと思う。(^_^;
前提となる、アプリの構造は以下のような感じだ。
Activity
└GameRun (スレッド)
└TextView
※.この他にも、TextView と同列のView があるのだが、ここでは省略。
【動作】
GameRun がスレッドとして動作し、表示メッセージをTextView へ送る。
【問題】
GameRunスレッドから、setText や class変数の変更など、TextViewの操作を行うと異常終了する。
解決するには、Handler を使用するしかないようだ。
ただそれには不都合があり、今回は、別の方法を探ってみた。
リスナーを使う
Handler ときて、最初に連想したのがこの方法だ。
リスナーも通知先をハンドラという。
ならば、リスナー経由で操作を行えば、根本の原因を回避できるのではないか?
というワケ。
GameRun
↓ 通知 ( _Listener.setPadding( padding[] ); )
TextView
method が実行され、変数に値が格納される。
setPadding( padding[] ){
mesPadding = padding;
}
ただしこの方法は、必ずmethod を介さなければならない。
上記の例でいえば、スレッドを使わない場合、次のようにシンプルだ。
TextView.mesPadding = padding;
ただの代入でも、method を介さなければならないのは、ちょいとシャク。
ただそれだけの話ではあるんだけどね。
この方法はことのほか、すんなり問題を解決した。
ソースもすっきりシンプル。
値を返すようなものにも使用できる。
リスナー と Handler を使う
前記方法で問題は解決したように見えたが、別の箇所で再び同じ問題が起きた。
そこではsetTextやLayoutView のremove を使っており、前記方法では解決されなかったのだ。
おそらく、描画しているものに対して、直接操作を行おうとしたからだろう。
解決策としてまず思い浮かんだのが、タイマーである。
タイマーはキューを受けると、時間をおいて処理を実行する仕掛け。
View側であらかじめ、setTextやremoveするようなタイマーを作り、スレッド側はキューを送る。
処理の実行はView側なので、根本原因は回避できるハズだ。
……って、待て待て。
それって、Handler そのものなんじゃね? (^_^;
そこでView のmethod にHandler を仕掛け、↓のように改良してみた。
結果は成功である。
View側 method
private Handler mHandler = new Handler();
private String mesStr = "";
public void setMes( String mes ){
mesStr = mes;
mHandler.post( new Runnable() {
public void run() {
setText( mesStr );
}
});
}
スレッド側 method実行
_MesView.setMes( "うふふ……" );
View側で、引数 mes を、class変数 mesStr に一旦代入しているのは、そうしないとrun内で認識されないから。(シンタックス・エラーになる)
というのも、new Runnable() で暗黙的にThread を生成しているからだ。
つまりrun内は、別class または別method の領域となってしまうのだね。
それはわかるが、イマイチ好きになれない書き方…。
まぁ、是非もナシやね。
これで問題は解決し、もうひとつの問題が発生した。
先のsetText をappend にした場合である。
public void appendMes( String mes ){
mesStr = mes;
mHandler.post( new Runnable() {
public void run() {
append( mesStr );
}
});
}
append は現在表示しているText に文字列を追加するmethodである。
たとえば、こんな動作をする。
[ソース]
setText( "うふふ……" );
append( "えっち♪" );
[表示動作]
うふふ……
↓append
うふふ……えっち♪
ソースからすると、上記のような動作になるハズなのだが。
実際にやってみると…こうなってしまう。
うふふ……
↓append
えっち♪えっち♪
これはclass変数を介しているために起きてしまう現象だ。
普段、意識しないが、Javaでの変数は参照が基本。
内部的にsetText されたのは、class変数のメモリー位置で、class変数の内容が変われば、当然、変わってしまう…。
おそらくは、スレッドを介しているのも関係があるんだろうね。
そこでappend は使わず、自前で追加するように変更した。
これで意図どおりに実行されるようになった。
public void appendMes( String mes ){
mesStr = mesStr+mes;
mHandler.post( new Runnable() {
public void run() {
setText( mesStr );
}
});
}
以上の方法には、リスナーのみの時とは、異なる注意点がある。
1. 即応性がない。
実行タイミングは、Handler(Looper) 任せなので、いつ実行されるか不明だ。
付随して、返り値を求めるようなmethod には適応できない。
2. classローカルでも、Handler を介することになる。
たとえば、例にあげたTextView自身がsetMesを実行すると、Handler を介した実行になる。
なんだか、自分の尻を他人に拭いてもらうようで、まどろっこしい。(笑
また、即応性の面からすると、潜在的なバグとなる可能性もある。
必要なら、ローカル用とHandler用、別々のmethod を作った方がいいだろうね。
3. class変数で引数を保持する。
先の失敗例のように、Handler が実行する前に、値が変わってしまわないようにしなければならない。
by the way
setTextは、スレッドからの実行で、ちゃんと動作している場合と、異常終了する場合が見られた。
これはsetTextだけの話ではない。
おそらく動作する例は、その時はまだ画面に表示されていないか、UIがひとつだけの状態だったのが原因ではないかと思う。
一度だけの実行で、不具合ナシと、安心してはいけないね。
補足
Handler の使い方は、まだよく把握していない。
見よう見まねの試行錯誤で、「一応、動作している」にすぎない。
もっとうまく、正しい方法もあると思う。
ちゃんとした解説を見て、試して、よく理解する必要があるね。(^_^;
しかし…。
なんだかJavaというより、Cocoaって感じの構造になったな。(w