作為IT新手,總以為只要有時間,有精力,什么東西都能做出來。這種念頭我也有過,但很快就熄滅了,因為現實是殘酷的,就算一開始的時間和精力非常充足,也會隨着項目的推進而逐步消磨殆盡。我們會發現,自己越來越消極怠工,只是在無意義的敲代碼,敲的還是網上抄來的代碼,如果不行,繼續找。
這就是項目進度沒有規划好而導致的。
最近在做有關藍牙的項目,一開始的進度都安排得很順利,但是因為測試需要兩部手機,而且還要是android手機,暑假已經開始了,同學們都回家了,加上我手機的藍牙壞了,導致我的進度嚴重被打亂!而且更加可怕的是,就算我手機這邊調試完畢,我最終的目標是實現手機與藍牙模塊的通信,那個測試板至今未送過來,所以,我開始消極怠工了。
經驗教訓非常簡單:根據整個項目的時間長度規划好每天的進度,視實際情況的變化而改變規划,就算真的是無法開展工作,像是現在這樣抽空出來寫寫博客都要好過無意義的敲代碼。
今天講的內容非常簡單,只是講講有關於android界面更新的方面。
1.利用Looper更新UI界面
如果我們的代碼需要隨時將處理后的數據交給UI更新,那么我們想到的方法就是另開一個線程更新數據(也必須這么做,如果我們的數據更新運算量較大,就會阻塞UI線程),也就是界面更新和數據更新是在不同線程中(android采用的是UI單線程模型,所以我們也只能在主線程中對UI進行操作),但這會導致另一個問題:如何在兩個線程間通信呢?android提供了Handler機制來保證這種通信。
先是一個簡單的例子:
public class MainActivity extends Activity { private Button mButton; private TextView mText; @SuppressLint("HandlerLeak") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mButton = (Button)this.findViewById(R.id.button); mText = (TextView)this.findViewById(R.id.text); final Handler handler = new Handler(){ @Override public void handleMessage(Message msg){ super.handleMessage(msg); if(msg.what == 1){ mText.setText("更新后"); } } }; mText.setText("更新前"); final Thread thread = new Thread(new Runnable(){ @Override public void run() { Message message = new Message(); message.what = 1; handler.sendMessage(message); } }); mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { thread.start(); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
在Main主線程中新開一個線程,該線程負責數據的更新,然后將更新后的數據放在Message里面,然后通過Handler傳遞給相應的UI進行更新。
使用TextView或者其他組件的時候,如果出現這樣的錯誤:
android.content.res.Resources$NotFoundException:String resource ID #0x86
這樣的錯誤誤導性真大!我以為是我的資源ID用錯了,但就是這個ID,一下子就沒法子了,查了很久,結果發現是TextView.setText()要求的是字符串,但我傳入了一個int!就這個問題,原本是傳參錯誤,但android竟然沒有報錯,而且這個錯誤提示也太那個了吧!!
Message的任務很簡單,就是用來傳遞數據更新信息,但有幾點也是值得注意的:我們可以使用構造方法來創建Message,但出於節省內存資源的考量,我們應該使用Message.obtain()從消息池中獲得空消息對象,而且如果Message只是攜帶簡單的int信息,優先使用Message.arg1和Message.arg2來傳遞信息,這樣比起使用Bundle更省內存,而Message.what用於標識信息的類型。
我們現在來了解Handler的工作機制。
Handler的作用就是兩個:在新啟動的線程中發送消息和在主線程中獲取和處理消息。像是上面例子中的Handler就包含了這兩個方面:我們在新啟動的線程thread中調用Handler的sendMessage()方法來發送消息。發送給誰呢?從代碼中可以看到,就發送給主線程創建的Handler中的handleMessage()方法處理。這就是回調的方式:我們只要在創建Handler的時候覆寫handleMessage()方法,然后在新啟動的線程發送消息時自動調用該方法。
要想真正明白Handler的工作機制,我們就要知道Looper,Message和MessageQueue。
Looper正如字面上的意思,就是一個"循環者",它的主要作用就是使我們的一個普通線程變成一個循環線程。如果我們想要得到一個循環線程,我們必須要這樣:
class LooperThread extends Thread{ public Handler mHandler; public void run(){ Looper.prepare(); mHandler = new Handler(){ public void handleMessage(Message msg){ //process incoming message here } }; Looper.loop(); } }
Looper.prepare()就是用來使當前的線程變成一個LooperThread,然后我們在這個線程中用Handler來處理消息隊列中的消息,接着利用Looper.loop()來遍歷消息隊列中的所有消息。
話是這么說,但是最后處理的是消息隊列中的最后一個消息:
mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); mTextView.setText(msg.what + ""); } }; mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { LooperThread thread = new LooperThread(); thread.setHandler(mHandler); thread.start(); } }); } class LooperThread extends Thread { Handler handler; public void setHandler(Handler handler){ this.handler = handler; } @Override public void run() { Looper.prepare(); for (int i = 0; i < 10; i++) { Message message = Message.obtain(); message.arg1 = i; handler.sendMessage(message); } Looper.loop(); } }
結果顯示的是9!!難道說MessageQueue是"先進后出"的隊列?
這只是因為處理得太快,如果我們這樣子:
try{ Thread.sleep(1000); handler.sendMessage(message); }catch(InterruptedException e){}
我們就可以看到TextView從0一直數到9。
由此可知道,sendMessage()方法的實現是回調了handleMessage(),所以說是處理消息隊列中的所有消息也是正確的,因為消息一發送到消息隊列中就立即被處理。
Looper線程應該怎么使用,得到一個Looper引用我們能干嘛?
讓我們繼續思考這個問題。
每個線程最多只有一個Looper對象,它的本質是一個ThreadLocal,而ThreadLocal是在JDK1.2中引入的,它為解決多線程程序的並發問題提供了一種新思路。
ThreadLocal並不是一個Thread,它是Thread的局部變量,正確的命名應該是ThreadLocalVariable才對。如果是經常看android源碼的同學,有時候也會發現它的一些變量的命名也很隨便。
ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立的改變自己的副本而不會影響到其他線程的副本。這種解決方案就是為每一個線程提供獨立的副本,而不是同步該變量。
但是該變量並不是在線程中聲明的,它是該線程使用的變量,因為對於線程來說,它所使用的變量就是它的本地變量,所以Local就是取該意。
學過java的同學都知道,編寫線程局部變量比起同步該變量來說,實在是太笨拙了,所以我們更多使用同步的方式,而且java對該方式也提供了非常便利的支持。
現在最大的問題就是:ThreadLocal是如何維護該變量的副本呢?
實現的方式非常簡單:在ThreadLocal中有一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應的是該線程的變量副本。
同樣是為了解決多線程中相同變量的訪問沖突問題,ThreadLocal和同步機制相比,有什么優勢呢?
使用同步機制,我們必須通過對象的鎖機制保證同一時間只有一個線程訪問變量。所以,我們必須分析什么時候對該變量進行讀寫,什么時候需要鎖定某個對象,又是什么時候該釋放對象鎖等問題,更糟糕的是,我們根本就無法保證這樣做事萬無一失的。
ThreadLocal是通過為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問沖突,所以我們也就沒有必要使用對象鎖這種難用的東西,這種方式更加安全。
ThreadLocal最大的問題就是它需要為每個線程維護一個副本,也就是"以空間換時間"的方式。我們知道,內存空間是非常寶貴的資源,這也是我們大部分時候都不會考慮該方式的原因。
為什么Looper是一個ThreadLocal呢?Looper本身最大的意義就是它內部有一個消息隊列,而其他線程是可以向該消息隊列中添加消息的,所以Looper本身就是一個ThreadLocal,每個線程都維護一個副本,添加到消息隊列中的消息都會被處理掉。
mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); if(msg.what == 1){ mTextView.setText(msg.what + ""); }else{ Toast.makeText(MainActivity.this, msg.what + "", Toast.LENGTH_LONG).show(); } } }; mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Thread1 thread1 = new Thread1(); thread1.setHandler(mHandler); thread1.start(); Thread2 thread2 = new Thread2(); thread2.setHandler(mHandler); thread2.start(); } }); } class Thread2 extends Thread { Handler handler; public void setHandler(Handler handler){ this.handler = handler; } @Override public void run() { Message message = Message.obtain(); message.what = 2; handler.sendMessage(message); } } class Thread1 extends Thread { Handler handler; public void setHandler(Handler handler){ this.handler = handler; } @Override public void run() { Message message = Message.obtain();
message.what = 1; handler.sendMessage(message); } }
上面這段代碼是新建兩個線程,每個線程都維護一個Handler,然后都向這個Handler發送消息,結果就是這兩個消息同時被處理。
Hanlder本身就持有一個MessageQueue和Looper的引用,默認情況下是創建該Handler的線程的Looper和該Looper的MessageQueue。
Hanler只能處理由自己發出的消息,它會通知MessageQueue,表明它要執行一個任務,然后在輪到自己的時候執行該任務,這個過程是異步的,因為它不是采用同步Looper的方式而是采用維護副本的方式解決多線程共享的問題。
一個線程可以有多個Handler,但是只能有一個Looper,理由同上:維護同一個Looper的副本。
到了這里,我們可以發現:新開一個線程用於處理數據的更新,在主線程中更新UI,這種方式是非常自然的,而且這也是所謂的觀察者模式的使用(使用回調的方式來更新UI,幾乎可以認為是使用了觀察者模式)。
我們繼續就着Looper探討下去。
因為Handler需要當前線程的MessageQueue,所以我們必須通過Looper.prepare()來為Handler啟動MessageQueue,而主線程默認是有MessageQueue,所以我們不需要在主線程中調用prepare()方法。在Looper.loop()后面的代碼是不會被執行的,除非我們顯式的調用Handler.getLooper().quit()方法來離開MessageQueue。
到了這里,我們之前的問題:LooperThread應該如何使用?已經有了很好的答案了: LooperThread用於UI的更新,而其他線程向其Handler發送消息以更新數據。因為主線程原本就是一個LooperThread,所以我們平時的習慣都是在主線程里創建Handler,然后再在其他線程里更新數據,這種做法也是非常保險的,因為UI組件只能在主線程里面更新。
當然,Handler並不僅僅是用於處理UI的更新,它本身的真正意義就是實現線程間的通信:
new LooperThread().start(); mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { final int MESSAGE_HELLO = 0; String message = "hello"; mHandler.obtainMessage(MESSAGE_HELLO, message).sendToTarget(); } }); } class LooperThread extends Thread { @Override public void run() { Looper.prepare(); mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_HELLO: Toast.makeText(MainActivity.this, (String) msg.obj, Toast.LENGTH_SHORT).show(); break; default: break; } } }; Looper.loop(); } }
上面是Handler非常經典的用法:我們通過Handler的obtainMessage()方法來創建一個新的Message(int what, Object obj),然后通過sendToTarget()發送到創建該Handler的線程中。如果大家做過類似藍牙編程這樣需要通過socket通信的項目,就會清楚的知道,判斷socket的狀態是多么重要,而Message的what就是用來存儲這些狀態值(通常這些狀態值是final int),值得注意的是,obj是Object,所以我們需要強制轉型。但這樣的編碼會讓我們的代碼擁有一大堆常量值,而且switch的使用是不可避免的,如果狀態值很多,那這個switch就真的是太臃腫了,就連android的藍牙官方實例也無法避免這點。
總結一下:Android使用消息機制實現線程間的通信,線程通過Looper建立自己的消息循環,MessageQueue是FIFO的消息隊列,Looper負責從MessageQueue中取出消息,並且分發到引用該Looper的Handler對象,該Handler對象持有線程的局部變量Looper,並且封裝了發送消息和處理消息的接口。
如果Handler僅僅是用來處理UI的更新,還可以有另一種使用方式:
mHandler = new Handler(); mRunnable = new Runnable() { @Override public void run() { mTextView.setText("haha"); } }; mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { new Thread() { public void run() { mHandler.post(mRunnable); } }.start(); } }); }
使用Handler的post()方法就顯得UI的更新處理非常簡單:在一個Runnable對象中更新UI,然后在另一個線程中通過Handler的post()執行該更新動作。值得注意的是,我們就算不用新開一個新線程照樣可以更新UI,因為UI的更新線程就是Handler的創建線程---主線程。
表面上Handler似乎可以發送兩種消息:Runnable對象和Message對象,實際上Runnable對象會被封裝成Message對象。
2.AsyncTask利用線程任務異步更新UI界面
AsyncTask的原理和Handler很接近,都是通過往主線程發送消息來更新主線程的UI,這種方式是異步的,所以就叫AsyncTask。使用AsyncTask的場合像是下載文件這種會嚴重阻塞主線程的任務就必須放在異步線程里面:
public class MainActivity extends Activity { private Button mButton; private ImageView mImageView; private ProgressBar mProgressBar; @SuppressLint("HandlerLeak") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mButton = (Button) this.findViewById(R.id.button); mImageView = (ImageView) this.findViewById(R.id.image); mProgressBar = (ProgressBar) this.findViewById(R.id.progressBar); mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { AsyncTaskThread thread = new AsyncTaskThread(); thread.execute("http://g.search2.alicdn.com/img/bao/uploaded/i4/" + "i4/12701024275153897/T1dahpFapbXXXXXXXX_!!0-item_pic.jpg_210x210.jpg"); } }); } class AsyncTaskThread extends AsyncTask<String, Integer, Bitmap> { @Override protected Bitmap doInBackground(String... params) { publishProgress(0); HttpClient client = new DefaultHttpClient(); publishProgress(30); HttpGet get = new HttpGet(params[0]); final Bitmap bitmap; try { HttpResponse response = client.execute(get); bitmap = BitmapFactory.decodeStream(response.getEntity() .getContent()); } catch (Exception e) { return null; } publishProgress(100); return bitmap; } protected void onProgressUpdate(Integer... progress) { mProgressBar.setProgress(progress[0]); } protected void onPostExecute(Bitmap result) { if (result != null) { Toast.makeText(MainActivity.this, "成功獲取圖片", Toast.LENGTH_LONG) .show(); mImageView.setImageBitmap(result); } else { Toast.makeText(MainActivity.this, "獲取圖片失敗", Toast.LENGTH_LONG) .show(); } } protected void onPreExecute() { mImageView.setImageBitmap(null); mProgressBar.setProgress(0); } protected void onCancelled() { mProgressBar.setProgress(0); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
實際的效果如圖:
當我們點擊下載按鈕的時候,就會啟動下載圖片的線程,主線程這里顯示下載進度條,然后在下載成功的時候就會顯示圖片,這時我們再點擊按鈕的時候就會清空圖片,進度條也重新清零。
仔細看上面的代碼,我們會發現很多有趣的東西。
AsyncTask是為了方便編寫后台線程與UI線程交互的輔助類,它的內部實現是一個線程池,每個后台任務會提交到線程池中的線程執行,然后通過向UI線程的Handler傳遞消息的方式調用相應的回調方法實現UI界面的更新。
AsyncTask的構造方法有三個模板參數:Params(傳遞給后台任務的參數類型),Progress(后台計算執行過程中,進度單位(progress units)的類型,也就是后台程序已經執行了百分之幾)和Result(后台執行返回的結果的類型)。
protected Bitmap doInBackground(String... params) { publishProgress(0); HttpClient client = new DefaultHttpClient(); publishProgress(30); HttpGet get = new HttpGet(params[0]); final Bitmap bitmap; try { HttpResponse response = client.execute(get); bitmap = BitmapFactory.decodeStream(response.getEntity() .getContent()); } catch (Exception e) { return null; } publishProgress(100); return bitmap; }
params是一個可變參數列表,publishProgress()中的參數就是Progress,同樣是一個可變參數列表,它用於向UI線程提交后台的進度,這里我們一開始設置為0,然后在30%的時候開始獲取圖片,一旦獲取成功,就設置為100%。中間的代碼用於下載和獲取網上的圖片資源。
protected void onProgressUpdate(Integer... progress) { mProgressBar.setProgress(progress[0]); }
onProgressUpdate()方法用於更新進度條的進度。
protected void onPostExecute(Bitmap result) { if (result != null) { Toast.makeText(MainActivity.this, "成功獲取圖片", Toast.LENGTH_LONG).show(); mImageView.setImageBitmap(result); } else { Toast.makeText(MainActivity.this, "獲取圖片失敗", Toast.LENGTH_LONG).show(); }
}
onPostExecute()方法用於處理Result的顯示,也就是UI的更新。
protected void onPreExecute() { mImageView.setImageBitmap(null); mProgressBar.setProgress(0); } protected void onCancelled() { mProgressBar.setProgress(0); }
這兩個方法主要用於在執行前和執行后清空圖片和進度。
最后我們只需要調用AsyncTask的execute()方法並將Params參數傳遞進來進行。完整的流程是這樣的:
UI線程執行onPreExecute()方法把ImageView的圖片和ProgressBar的進度清空,然后后台線程執行doInBackground()方法,千萬不要在這個方法里面更新UI,因為此時是在另一條線程上,在使用publishProgress()方法的時候會調用onProgressUpdate()方法更新進度條,最后返回result---Bitmap,當后台任務執行完成后,會調用onPostExecute()方法來更新ImageView。
AsyncTask本質上是一個靜態的線程池,由它派生出來的子類可以實現不同的異步任務,但這些任務都是提交到該靜態線程池中執行,執行的時候通過調用doInBackground()方法執行異步任務,期間會通過Handler將相關的信息發送到UI線程中,但神奇的是,並不是調用UI線程中的回調方法,而是AsyncTask本身就有一個Handler的子類InternalHandler會響應這些消息並調用AsyncTask中相應的回調方法。從上面的代碼中我們也可以看到,UI的ProgressBar的更新是在AsyncTask的onProgressUpdate(),而ImageView是在onPostExecute()方法里。這是因為InternalHandler其實是在UI線程里面創建的,所以它能夠調用相應的回調方法來更新UI。
AsyncTask就是專門用來處理后台任務的,而且它針對后台任務的五種狀態提供了五個相應的回調接口,使得我們處理后台任務變得非常方便。
如果只是普通的UI更新操作,像是不斷更新TextView這種動態的操作,可以使用Handler,但如果是涉及到后台操作,像是下載任務,然后根據后台任務的進展來更新UI,就得使用AsyncTask,但如果前者我們就使用AsyncTask,那真的是太大材小用了!!
要想真正理解好AsyncTask,首先就要理解很多並發知識,像是靜態線程池這些難以理解的概念是必不可少的,作為新手,其實沒有必要在實現細節上過分追究,否則很容易陷入細節的泥潭中,我們先要明白它是怎么用的,等用得多了,就會開始思考為什么它能這么用,接着就是怎么才能用得更好,這都是一個自然的學習過程,誰也無法越過,什么階段就做什么事。因此,關於AsyncTask的討論我就先放到一邊,接下來的東西我也根本理解不了,又怎能講好呢?
3.利用Runnable更新UI界面
剩下的方法都是圍繞着Runnable對象來更新UI。
一些組件本身就有提供方法來更新自己,像是ProgressBar本身就有一個post()方法,只要我們傳進一個Runnable對象,就能更新它的進度。只要是繼承自View的組件,都可以利用post()方法,而且我們還可以使用postDelay()方法來延遲執行該Runnable對象。android的這種做法就真的是讓人稱道了,至少我不用為了一個ProgressBar的進度更新就寫出一大堆難懂的代碼出來。
還有另一種利用Runnable的方式:Activity.runOnUiThread()方法。這名字實在是太直白了!!使用該方法需要新啟一個線程:
class ProgressThread extends Thread { @Override public void run() { super.run(); while (mProgress <= 100) { runOnUiThread(new Runnable() { @Override public void run() { mProgressBar.setProgress(mProgress); mProgress++; } }); try { Thread.sleep(100); } catch (InterruptedException e) { } } } }
4.總結
上面提供了三種思路來解決UI更新的問題,有些地方的討論已經嚴重脫離標題,那也是沒有辦法,因為要說明一些概念,就必須涉及到並發的其他相關知識。方法很多,但它們都有自己適合的場合:
1.如果只是單純的想要更新UI而不涉及到多線程的話,使用View.post()就可以了;
2.需要另開線程處理數據以免阻塞UI線程,像是IO操作或者是循環,可以使用Activity.runOnUiThread();
3.如果需要傳遞狀態值等信息,像是藍牙編程中的socket連接,就需要利用狀態值來提示連接狀態以及做相應的處理,就需要使用Handler + Thread的方式;
4.如果是后台任務,像是下載任務等,就需要使用AsyncTask。
本來只是因為藍牙項目而開始這篇博客,但沒想到在寫的過程發現越來越多的東西,於是也一起寫上來了,寫得不好是一定的,因為是大三菜鳥,正在拼命增強自己薄弱的編程基礎中,如果錯誤的地方,還希望能夠指點迷津。