做爲IT新手,總覺得只要有時間,有精力,什麼東西都能作出來。這種念頭我也有過,但很快就熄滅了,由於現實是殘酷的,就算一開始的時間和精力很是充足,也會隨着項目的推動而逐步消磨殆盡。咱們會發現,本身愈來愈消極怠工,只是在無心義的敲代碼,敲的仍是網上抄來的代碼,若是不行,繼續找。java
這就是項目進度沒有規劃好而致使的。android
最近在作有關藍牙的項目,一開始的進度都安排得很順利,可是由於測試須要兩部手機,並且還要是android手機,暑假已經開始了,同窗們都回家了,加上我手機的藍牙壞了,致使個人進度嚴重被打亂!並且更加可怕的是,就算我手機這邊調試完畢,我最終的目標是實現手機與藍牙模塊的通訊,那個測試板至今未送過來,因此,我開始消極怠工了。編程
經驗教訓很是簡單:根據整個項目的時間長度規劃好天天的進度,視實際狀況的變化而改變規劃,就算真的是沒法開展工做,像是如今這樣抽空出來寫寫博客都要好過無心義的敲代碼。安全
今天講的內容很是簡單,只是講講有關於android界面更新的方面。多線程
1.利用Looper更新UI界面併發
若是咱們的代碼須要隨時將處理後的數據交給UI更新,那麼咱們想到的方法就是另開一個線程更新數據(也必須這麼作,若是咱們的數據更新運算量較大,就會阻塞UI線程),也就是界面更新和數據更新是在不一樣線程中(android採用的是UI單線程模型,因此咱們也只能在主線程中對UI進行操做),但這會致使另外一個問題:如何在兩個線程間通訊呢?android提供了Handler機制來保證這種通訊。異步
先是一個簡單的例子:socket
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進行更新。ide
使用TextView或者其餘組件的時候,若是出現這樣的錯誤:oop
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。 原本只是由於藍牙項目而開始這篇博客,但沒想到在寫的過程發現愈來愈多的東西,因而也一塊兒寫上來了,寫得很差是必定的,由於是大三菜鳥,正在拼命加強本身薄弱的編程基礎中,若是錯誤的地方,還但願可以指點迷津。