http://hatsukiakio.blogspot.com/2009/06 ... on-in.html
2009年6月7日星期日
IllegalThreadStateException in LunarLander
在實驗LunarLander 的時後,如果在run LunarLander的途中,按下Home跳出
而不是按下return的話,此時在進入LunarLander,就會跑出 IllegalThreadStateException這樣的例外
稍微研究了一下這個原因
首先看一下這張圖,綠色框起來是Return,藍色框起來是Home
比較這兩鍵按下去返回主選單在回去遊戲的不同
Return生命週期變化
onCreate->onStart->onResume->按下Retuen->onPause->onStop->onDestroy->回到LunarLander->onCreate->onStart->onResume
Home生命週期變化
onCreate->onStart->onResume->按下Home->onPause->onStop->回到LunarLander->onRestart->onStart->onResume
上面的比較可以發現,按下Return的時候,整個Activity會依照正常程序Destroy在Create
但是按下Home回到主選單的時候Activity不會Destroy掉,他的資料仍然會存在記憶體,之後只是Restart他
而LunarLander遊戲設計是假設SurfaceView的生命週期跟Activity一致,在Activity的Create跟Destroy這段期間,只會發生一次的surfaceCreate跟surfaceDestroy
來觀察一下按下Home的時候,SurfaceView的生命週期
onCreate->onStart->onResumeonWindowFocusChanged->surfaceCreated->surfaceChanged->按下Home->onPause->surfaceDestroyed->onWindowFocusChanged->onStop->回到LunarLander->onRestart->onStart->onResume->surfaceCreated->surfaceChanged->onWindowFocusChanged
就像上面所示,因為Activity沒有照正常程序被Destroy掉
而在Activity的create跟destroy之間,SurfaceView的create跟Destroy執行了超過一次以上,這是沒有在當出遊戲設計者的預料之中,所以會發生IllegalThreadStateException這個例外而不能執行的Bug
而IllegalThreadStateException是怎麼發生的呢?
先來看他的定義
Thrown when an operation is attempted which is not possible given the state that the executing thread is in.
也就是試圖從相同的程序中start相同的Thread
舉個例子
Thread t=new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub}});t.start();t.start();//start兩次,拋出IllegalThreadStateException
而要避開這例外,必須使用新的Thread
Thread t=new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub}});t.start();t=new Thread(..........);t.start();
而在Lunar,IllegalThreadStateException是從LunarView的surfaceCreated中拋出來的
/* * Callback invoked when the Surface has been created and is ready to be * used. */public void surfaceCreated(SurfaceHolder holder) {....thread.start();//由此函式拋出IllegalThreadStateException }
如上面所示,因為surfaceCreated在Activity執行了兩次以上的話,相同的thread就會start兩次以上,thread的產生是在Activity的onCreate呼叫建構式LunarView時誕生,所以只會被產生一次
public LunarView(Context context, AttributeSet attrs) {....thread = new LunarThread(holder, _context, new Handler() { @Override public void handleMessage(Message m) { mStatusText.setVisibility(m.getData().getInt("viz")); mStatusText.setText(m.getData().getString("text")); }});....}
在此我提出一個暴力法的解決方案解決這Bug,就是在surfaceCreated的時候就產生一個新的LunarThread,這樣可以避免同樣的thread被start兩次以上,不過我沒做State的Retore,所以他就跟按下Return一樣是從新開始一個遊戲
我的作法就是讓LunarThread的生命週期跟Surface一致
surfaceCreated:產生LunarThread
surfaceDestroyed:消滅LunarThread
修改方法如下,粗體是我新增的程式碼
surfaceCreated
public void surfaceCreated(SurfaceHolder holder) {/*判斷thread是否為null,是的話產生新的Thread*/ if (thread == null) {/*把建構式創造thread的程式碼copy過來,記得建構式保持原樣就好,不須要更改*/ thread = new LunarThread(holder, _context, new Handler() { @Override public void handleMessage(Message m) { mStatusText.setVisibility(m.getData().getInt("viz")); mStatusText.setText(m.getData().getString("text")); } }); setFocusable(true);/*這邊是跟建構式不同的地方,因為按下Home的時候,會改變thread的狀態在這邊要手動把狀態設置為ready來讓keyEvent能正常動作*/ thread.setState(LunarThread.STATE_READY);}thread.setRunning(true);thread.start();}
surfaceDestroyed
public void surfaceDestroyed(SurfaceHolder holder) {boolean retry = true;thread.setRunning(false);while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { } }/*在此把thread設為null消滅現有的thread這邊是為了配合surfaceCreated的if(thread==null)*/ thread=null;}
onWindowFocusChanged
public void onWindowFocusChanged(boolean hasWindowFocus) { if (!hasWindowFocus) {/*surfaceDestroy發生後會在發生 onWindowFocusChanged事件 因為thread已經被設為null了所以不能在執行函式動作 這也是為什麼surfaceCreated要手動設置Ready */ if(thread!=null) thread.pause(); }}
以上,改完就大功告成了
--------------------
更好的解決方案
在建構式ListView的時候創造跟啟動執行緒(相較於在surfaceCreate啟動執行緒)
創造一個lock的物件
而在surfaceCreate的時候使用notify跟restore
在surfaceDestroy的時候使用wait跟相關儲存動作
創造一個函式destroyGame()真正跳出整個遊戲,只有在Activity的onDestroy去呼叫他(相較於在surfaceDestroy結束執行緒)
不過我沒有去真正實作上面的方案,只是構想,不過因該是八九不離十