一段壞代碼如何變成優雅代碼
原創文章,如有轉載,請註明出處:http://blog.csdn.net/yihui823/article/details/6741411
今天看到一段很糟糕的代碼。於是做了一個工程,類比這段代碼,向大家說明一下線程在使用中要注意的幾點。這個例子適合給新手,也歡迎各位高手來指點一下。
首先,上代碼。
第一個類LoginService,這是一個類比類,把業務剝離出去了。只是類比登入操作而已。
package com.study;package com.study;/** * 虛擬一個登入服務. * @author yihui823 */public class LoginService {//單例private static LoginService oneInstance = new LoginService();/** * 得到唯一的一個單例 * @return 唯一的一個單例 */public static LoginService getInstance() {return oneInstance;}//登入成功標記private boolean hadLogin = false;/** * 類比登入操作 * @return true:登入成功 */public boolean login() {try {Thread.sleep(2000);} catch (InterruptedException e) {}hadLogin = true;return hadLogin;}/** * 判斷是否登入 * @return true:已經登入 */public boolean isLogin() {return hadLogin;}}
第二個類就是我們的Activity了。
package com.study;import android.app.Activity;import android.os.Bundle;import android.widget.Toast;/** * 一個段不好的代碼 * @author yihui823 */public class BadCodeActivity extends Activity {//登入服務private LoginService lService = LoginService.getInstance(); /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);new Thread(new Runnable() {@Overridepublic void run() {while(!lService.isLogin() ) {try {lService.login();Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}Toast.makeText(BadCodeActivity.this, "登入成功", Toast.LENGTH_LONG).show();}}).start(); }}
這個例子呢,顯然啟動並執行時候會報錯的。請見我的另一篇文章:Android畫面UI中的線程約束。我們在非UI線程裡去控制UI介面,就必須使用Handler來發送訊息。修改代碼如下:
package com.study;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.widget.Toast;/** * 一個段不好的代碼 * @author yihui823 */public class BadCodeActivity extends Activity {//登入服務private LoginService lService = LoginService.getInstance();//外線程訪問UI線程的Handleprivate Handler mhandle = new Handler(){@Overridepublic void handleMessage(Message msg) {Toast.makeText(BadCodeActivity.this, "登入成功", Toast.LENGTH_LONG).show();}}; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);new Thread(new Runnable() {@Overridepublic void run() {while(!lService.isLogin() ) {try {lService.login();Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}mhandle.sendEmptyMessage(0);}}).start(); }}
紅色部分代碼,就是修改的地方。現在,這段代碼可以運行了,而且還貌似不錯,是吧。
但是,一個好的程式,不能只是應付正常情況,還要應付錯誤情況,是吧。如果登入總是出錯怎麼樣呢?我們把LoginService類略微改動,如下:
package com.study;import android.util.Log;/** * 虛擬一個登入服務. * @author yihui823 */public class LoginService {private static final String TAG = "LoginService";//單例private static LoginService oneInstance = new LoginService();/** * 得到唯一的一個單例 * @return 唯一的一個單例 */public static LoginService getInstance() {return oneInstance;}//登入成功標記private boolean hadLogin = false;/** * 類比登入操作 * @return true:登入成功 */public boolean login() {try {Thread.sleep(2000);} catch (InterruptedException e) {}Log.d(TAG, "we are login");//hadLogin = true;return hadLogin;}/** * 判斷是否登入 * @return true:已經登入 */public boolean isLogin() {return hadLogin;}}
增加了Log,以便查看登入情況。類比業務代碼只改了一行,就是登入永遠是失敗。現在運行一下呢。停在頁面上沒有動靜了,logcat裡也不斷的打出:
we are login
這個也不會有什麼錯誤,對吧。但是,我們如果按“返回”鍵退出頁面,再看看logcat呢?
we are login的log還在不停的輸出,是嗎?
我想現在大家應該知道哪裡出了問題了。就是說,我們的線程啟動之後,就沒法停掉了。
這裡我要說一下。我一直認為,
new Thread(new Runnable() {…}().start();
這種代碼寫的非常的不好。你直接構造了一個對象,但是這個對象你沒有任何的變數去指向它。這個線程被你啟動之後,你已經無法再去跟蹤、調用、管理了。這個線程,只能自生自滅,永遠游離在你的控制範圍之外。你會不會覺得,這個線程跟殭屍一樣?對,這就是殭屍進程,如果它沒有停止的條件,就永遠在你的系統裡消耗你的資源。
所以我覺得使用線程的一個基本認識:產生的線程類,一定要有一個變數去指向它,以便在合適的時候銷毀。
這裡說到銷毀,這就是另一個問題了。Thread類已經廢棄了stop方法了,因為線程需要自行去釋放該釋放的資源,不能光依賴於運行架構的控制。我們需要在Thread裡面,加上他自己停止的代碼。也就是說,不論如何,線程應該會自己去停止掉,而不應該是無限制的運行。
另外,我們在Android裡面,還應該注意Activity的各個狀態周轉。一般來說,線程的啟動在onCreate裡是不合適的,我們必須考慮到onResume和onPause的情況。
那麼,我們總結下,Activity裡使用線程有三個注意:
1, 線程對象一定要有變數指向它,以便我們可以控制。
2, 線程類一定要有停止條件,以便外界通知線程自行停止。
3, 在onResume裡啟動線程,在onPause裡停止線程。
我們根據以上三點,重新寫一下Activity。
package com.study;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.util.Log;import android.widget.Toast;/** * 一個段不好的代碼 * @author yihui823 */public class BadCodeActivity extends Activity {private static final String TAG = "BadCodeActivity";//登入服務private LoginService lService = LoginService.getInstance();//外線程訪問UI線程的Handleprivate Handler mhandle = new Handler(){@Overridepublic void handleMessage(Message msg) {Toast.makeText(BadCodeActivity.this, "登入成功", Toast.LENGTH_LONG).show();}};//通知停止線程的標記private boolean stopFlag = false;//登入成功標記private boolean loginOk = false;/** * 登入用的線程類 */private class LoginThread extends Thread {@Overridepublic void run() {while(!stopFlag) {loginOk = lService.isLogin();if (loginOk) {break;}try {lService.login();Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}mhandle.sendEmptyMessage(0);}/** * 通知線程需要停止 */public void stopLogin() {stopFlag = true;}};//用來登入的線程private LoginThread loginThread = new LoginThread(); /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.d(TAG, "BadCodeActivity instance is called onCreate :" + this.hashCode()); } public void onResume() { super.onResume(); Log.d(TAG, "BadCodeActivity instance is called onResume :" + this.hashCode()); loginThread.start(); } public void onPause() { super.onPause(); loginThread.stopLogin(); }}
現在,我們的線程可以在頁面退出的時候正常停止了。
但是這段代碼還是有問題的。我們仔細看看,線程在Activity構造的時候就已經建立了,然後在程式進到前台的時候啟動,退到背景時候停止。但是線程有這麼一個特性:
一旦線程的run()函數運行結束了,這個線程就銷毀了,不能再啟動了。
現在我們的程式,在退出後將不可能再次顯示,所以系統會馬上回收掉Activity。如果我們的頁面增加一個按鈕,遷移到另一個頁面,那麼在那個頁面返回的時候,就會有異常出現。我們修改一下代碼來試試。
增加一個Activity:
package com.study;import android.app.Activity;import android.os.Bundle;/** * 臨時頁面 * @author yihui823 */public class TempActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); }}
別忘了修改AndroidManifest.xml,增加Activity的說明:
<activity android:name=".TempActivity"
android:label="@string/app_name"/>
修改BadCodeActivity:
package com.study;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.util.Log;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.Toast;/** * 一個段不好的代碼 * @author yihui823 */public class BadCodeActivity extends Activity {private static final String TAG = "BadCodeActivity";//登入服務private LoginService lService = LoginService.getInstance();//外線程訪問UI線程的Handleprivate Handler mhandle = new Handler(){@Overridepublic void handleMessage(Message msg) {Toast.makeText(BadCodeActivity.this, "登入成功", Toast.LENGTH_LONG).show();}};//通知停止線程的標記private boolean stopFlag = false;//登入成功標記private boolean loginOk = false;/** * 登入用的線程類 */private class LoginThread extends Thread {@Overridepublic void run() {while(!stopFlag) {loginOk = lService.isLogin();if (loginOk) {break;}try {lService.login();Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}mhandle.sendEmptyMessage(0);}/** * 通知線程需要停止 */public void stopLogin() {stopFlag = true;}};//用來登入的線程private LoginThread loginThread = new LoginThread(); /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Button btn = (Button)findViewById(R.id.btn); btn.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View arg0) { startActivity(new Intent(BadCodeActivity.this,TempActivity.class));} }); Log.d(TAG, "BadCodeActivity instance is called onCreate :" + this.hashCode()); } public void onResume() { super.onResume(); Log.d(TAG, "BadCodeActivity instance is called onResume :" + this.hashCode()); loginThread.start(); } public void onPause() { super.onPause(); loginThread.stopLogin(); }}
其實就是加了一個按鈕,做一個頁面遷移。別忘了在main.xml裡面加上:
<Buttonandroid:id="@+id/btn" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" />
現在我們運行程式。運行之後,點擊按鈕,畫面閃動一下說明是切換了頁面。我們偷了個懶,兩個Activity共用一個layout,所以頁面沒有任何變化。但是沒關係,我們看log,we are login已經停止輸出了。這個時候,我們再按返回鍵,應該是切換回BadCodeActivity。這個時候系統報錯:
java.lang.IllegalThreadStateException: Thread already started.
顯然,就是說線程已經啟動過了,不能再次被利用。
我們對代碼需要做一點點修改。當然,我們也順手改掉一個BUG:在退出的時候還會報告登入成功。
並且,我們把控制變數都放在內部類裡,做到變數最小化生存空間。
修改後如下:
package com.study;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.util.Log;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.Toast;/** * 一個段不好的代碼 * @author yihui823 */public class BadCodeActivity extends Activity {private static final String TAG = "BadCodeActivity";//登入服務private LoginService lService = LoginService.getInstance();//外線程訪問UI線程的Handleprivate Handler mhandle = new Handler(){@Overridepublic void handleMessage(Message msg) {Toast.makeText(BadCodeActivity.this, "登入成功", Toast.LENGTH_LONG).show();}};/** * 登入用的線程類 */private class LoginThread extends Thread {//通知停止線程的標記private boolean stopFlag = false;//登入成功標記private boolean loginOk = false;@Overridepublic void run() {while(!stopFlag) {loginOk = lService.isLogin();if (loginOk) {break;}try {lService.login();Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}if (loginOk) {mhandle.sendEmptyMessage(0);}}/** * 通知線程需要停止 */public void stopLogin() {stopFlag = true;}};//用來登入的線程private LoginThread loginThread; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Button btn = (Button)findViewById(R.id.btn); btn.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View arg0) { startActivity(new Intent(BadCodeActivity.this,TempActivity.class));} }); Log.d(TAG, "BadCodeActivity instance is called onCreate :" + this.hashCode()); } public void onResume() { super.onResume(); Log.d(TAG, "BadCodeActivity instance is called onResume :" + this.hashCode()); loginThread = new LoginThread(); loginThread.start(); } public void onPause() { super.onPause(); loginThread.stopLogin(); }}
現在,我們點擊按鈕,進入到TempActivity的時候,登入log停止輸出;然後按返回鍵,回到BadCodeActivity的時候,登入log又繼續輸出。程式基本完成,沒有殭屍線程存在了。紅色的那行代碼是關鍵!
我們總結一下:
1, 線程對象一定要有變數指向它,以便我們可以控制。
2, 線程類一定要有停止條件,以便外界通知線程自行停止。
3, 線程啟動之後,不管是不是已經停止了,都是不能再次利用的。
4, 在onResume裡建立和啟動線程,在onPause裡停止線程。