Android--線程詳解
什麼是線程?
線程或者線程執行本質上就是一串命令(也是程式碼),然後我們把它發送給作業系統執行。
一般來說,我們的CPU在任何時候一個核只能處理一個線程。多核處理器(目前大多數Android裝置已經都是多核)顧名思義,就是可以同時處理多線程(通俗地講就是可以同時處理多件事)。
多核處理與單核多任務處理的實質
上面我說的是一般情況,並不是所有的描述都是一定正確的。因為單核也可以用多任務類比出多線程。
每個運行線上程中的任務都可以分解成多條指令,而且這些指令不用同時執行。所以,單核裝置可以首先切換到線程1去執行指令1A,然後切換到線程2去執行指令2A,接著返回到線程1再去執行1B、1C、1D,然後繼續切換到線程2,執行2B、2C等等,以此類推。
這個線程之間的切換十分迅速,以至於在單核的裝置中也會發生。幾乎所有的線程都在相同的時間內進行任務處理。其實,這都是因為速度太快造成的假象,就像電影《駭客帝國》裡的特工Brown一樣,可以變幻出很多的頭和手。
接下來我們來看一些代碼。
Java核心裡的線程
在Java中,如果要想做平行任務處理的話,會在Runnable裡面執行你的代碼。可以繼承Thread類,或者實現Runnable介面:
// Version 1public class IAmAThread extends Thread { public IAmAThread() { super("IAmAThread"); } @Override public void run() { // your code (sequence of instructions) }}// to execute this sequence of instructions in a separate thread.new IAmAThread().start(); // Version 2public class IAmARunnable implements Runnable { @Override public void run() { // your code (sequence of instructions) }}// to execute this sequence of instructions in a separate thread.IAmARunnable myRunnable = new IAmARunnable();new Thread(myRunnable).start();
這兩個方法基本上是一樣的。第一個版本是建立一個Thread類,第二個版本是需要建立一個Runnable對象,然後也需要一個Thread類來調用它。
第二個版是通常建議使用的方法。這也是一個很大的主題了,超過了本文的範圍,以後會再做討論。
Android上的線程
無論何時啟動APP,所有的組件都會運行在一個單獨的線程中(預設的)——叫做主線程。這個線程主要用於處理UI的操作並為視圖組件和小組件分發事件等,因此主線程也被稱作UI線程。
如果你在UI線程中運行一個耗時操作,那麼UI就會被鎖住,直到這個耗時操作結束。對於使用者體驗來說,這是非常糟糕的!這也就是為什麼我們要理解Android上的線程機制了。理解這些機制就可以把一些複雜的工作移動到其它的線程中去執行。如果你在UI線程中運行一個耗時的任務,那麼很有可能會發生ANR(應用無響應),這樣使用者就會很快地結束掉你的APP。
Android和Java一樣,它支援使用Java裡面的Thread類來進行一步任務處理。所以可以輕鬆地像上面Java的例子一樣來使用Android上的線程,不過那好像還是有點困難。
為什麼在Android上使用標準Java的線程會困難呢?
其實平行任務處理沒有想象中的那麼簡單,你必須在多線程中保證並發,就像偉大的Tim Bray說的那樣:ordinary humans can’t do concurrency at scale (or really at all) …
特別對於Android來說,以下這些功能就略顯臃腫:
非同步對於UI線程來說是一個主要的PITA(如果你需要在後台線程中向主線程更新介面,那麼你就會用到)。 如果螢幕方向或者螢幕配置改變的話,就會出現一些更加奇怪的現象。因為改變螢幕方向,會引起Activity重建(所以後台線程就需要去改變被銷毀的Activity的狀態了,而如果後台線程不是在UI線程之上的話,那情況會更加複雜,原因如條件1)。 對於線程池來說,沒有預設的處理方式。 取消線程操作需要自訂代碼實現。
那麼在Android上怎麼進行任務並發處理呢?
你可能聽過一些Android上一些常見的名詞:
1、Handler
這就是我們今天要討論的詳細主題。
2、AsyncTask
使用AsyncTask是在Android上操作線程最簡單的方式,也是最容易出錯的方式。
3、IntentService
這種方式需要寫更多的代碼,但是這是把耗時任務移動到背景很好的方式,也是我最喜歡的方式。配上使用一個EventBus機制的架構如Otto,這樣的話實現IntentService就非常簡單了。
4、Loader
關於處理非同步任務,還有很多事情需要做,比如從資料庫或者內容提供者那裡處理一些資料。
5、Service
如果你曾經使用過Service的話,你應該知道這裡會有一點誤區,其中一個常見的誤解就是服務是運行在後台線程的。其實不是!看似運行在後台是因為它們不與UI組件關聯,但是它們(預設)是運行在UI線程上的……所以預設運行在UI線程上,甚至在上面沒有UI組件。
如果想要把服務運行在後台線程中,那麼必須自訂一個線程,然後把作業碼都運行在那個線程中(與上面提到的方法很類似)。事實上你應該使用IntentService實現,但是這不是本文討論的主題。
Android上的Handler
以下是從Android developer documentation for Handlers:中摘選的一段話:
> A Handler allows you to send and process Message and Runnable objects associated with a thread’s MessageQueue. Each Handler instance is associated with a single thread and that thread’s message queue. When you create a new Handler, it is bound to the thread/message queue of the thread that is creating it — from that point on, it will deliver messages and runnables to that message queue and execute them as they come out of the message queue.
為了更好地瞭解這個概念,也許你需要去看看什麼是Message Queues。
訊息佇列
線上程裡基本都有一個叫做“訊息佇列”的東西,它負責線程間通訊。這是一種設計模式,所有控制指令或者內容線上程間傳遞。
訊息佇列如同它的名字那樣,對於線程來說,它就是一個指令隊列。這裡我們還可以做一些更酷的事:
定時訊息和線程在某個時間點才執行。 需要在另一個線程中去添加入隊動作,而不是在本線程中。
注意:這裡說的“訊息”和Runnable對象、指令隊列的概念是一樣的。
回到Android上的Handler……如果你仔細閱讀的話,可以看到文檔是這樣說的:
> A Handler allows you to send and process Message and Runnable objects associated with a thread’s MessageQueue.
所以Handler可以讓你給線程隊列發訊息:
> Each Handler instance is associated with a single thread and that thread’s message queue.
一個Handler對象只能和一個線程關聯:
> When you create a new Handler, it is bound to the thread / message queue of the thread that is creating it
所以一個Handler到底和哪個線程關聯呢?就是創造它的線程。
> — from that point on, it will deliver messages and runnables to that message queue and execute them as they come out of the message queue.、
在我們瞭解這些知識後,請繼續看……
小貼士:這裡有幾點可能你還不知道。每個線程都和一個Handler類執行個體綁定,而且可以和別的線程一起運行,相互連信。
還有一個小建議(如果用過AsyncTask的話),AsyncTask內部也是使用Handler進行處理的,只是不是運行在UI線程而已,它會提供一個channel來和UI線程通訊,使用postExecute方法即可實現。
這還挺酷的,那怎麼建立Handler呢?
有兩種方式:
使用預設的構造方法:new Handler()。 使用帶參的構造方法,參數是一個Runnable對象或者回調對象。Handler裡面有什麼實用的API嗎?
請記住:
Handler只是簡單往訊息佇列中發送訊息而已(或者使用post方式) 它們有更方便的方法可以協助與UI線程通訊。
如果你現在看看Handler的API,可以清楚看到這幾個方法:
post postDelayed postAtTime程式碼範例
這裡的代碼都是很基礎的,不過你可以好好看看注釋。
樣本1:使用Handler的“post”方法
public class TestActivity extends Activity { // ...// all standard stuff @Overridepublic void onCreate(Bundle savedInstanceState) { // ... // all standard stuff // we're creating a new handler here // and we're in the UI Thread (default) // so this Handler is associated with the UI thread Handler mHandler = new Handler(); // I want to start doing something really long // which means I should run the fella in another thread. // I do that by sending a message - in the form of another runnable object // But first, I'm going to create a Runnable object or a message for this Runnable mRunnableOnSeparateThread = new Runnable() { @Override public void run () { // do some long operation longOperation(); // After mRunnableOnSeparateThread is done with it's job, // I need to tell the user that i'm done // which means I need to send a message back to the UI thread // who do we know that's associated with the UI thread? mHandler.post(new Runnable(){ @Override public void run(){ // do some UI related thing // like update a progress bar or TextView // .... } }); } }; // Cool but I've not executed the mRunnableOnSeparateThread yet // I've only defined the message to be sent // When I execute it though, I want it to be in a different thread // that was the whole point. new Thread(mRunnableOnSeparateThread).start();} }
如果根本就沒有Handler對象,回調post方法會比較難辦。
樣本2:使用postDelayed方法
近期本站新介紹的特性中,我每次都要類比EditText的自動完成功能,每次文字改變後都會觸發一個API的調用,從伺服器中檢索資料。
我想減少APP調用API的次數,所以決定使用Handler的postDelayed方法來實現這個功能。
本例不針對平行處理,只是關於Handler給訊息佇列發送訊息還有安排訊息在未來的某一點執行等。
// the below code is inside a TextWatcher// which implements the onTextChanged method// I've simplified it to only highlight the parts we're// interested in private long lastChange = 0; @Overridepublic void onTextChanged(final CharSequence chars, int start, int before, int count) { // The handler is spawned from the UI thread new Handler().postDelayed( // argument 1 for postDelated = message to be sent new Runnable() { @Override public void run() { if (noChangeInText_InTheLastFewSeconds()) { searchAndPopulateListView(chars.toString()); // logic } } }, // argument 2 for postDelated = delay before execution 300); lastChange = System.currentTimeMillis();} private boolean noChangeInText_InTheLastFewSeconds() { return System.currentTimeMillis() - lastChange >= 300}最後我就把“postAtTime”這個方法作為聯絡留給讀者們了,掌握Handler了嗎?如果是的話,那麼可以盡情使用線程了。1. Android進程
在瞭解Android線程之前得先瞭解一下Android的進程。當一個程式第一次啟動的時候,Android會啟動一個LINUX進程和一個主線程。預設的情況下,所有該程式的組件都將在該進程和線程中運行。同時,Android會為每個應用程式分配一個單獨的LINUX使用者。Android會盡量保留一個正在運行進程,只在記憶體資源出現不足時,Android會嘗試停止一些進程從而釋放足夠的資源給其他新的進程使用, 也能保證使用者正在訪問的當前進程有足夠的資源去及時地響應使用者的事件。Android會根據進程中啟動並執行組件類別以及組件的狀態來判斷該進程的重要性,Android會首先停止那些不重要的進程。按照重要性從高到低一共有五個層級:
前台進程
前台進程是使用者當前正在使用的進程。只有一些前台進程可以在任何時候都存在。他們是最後一個被結束的,當記憶體低到根本連他們都不能啟動並執行時候。一般來說, 在這種情況下,裝置會進行記憶體調度,中止一些前台進程來保持對使用者互動的響應。 可見進程
可見進程不包含前台的組件但是會在螢幕上顯示一個可見的進程是的重要程度很高,除非前台進程需要擷取它的資源,不然不會被中止。 服務進程
運行著一個通過startService() 方法啟動的service,這個service不屬於上面提到的2種更高重要性的。service所在的進程雖然對使用者不是直接可見的,但是他們執行了使用者非常關注的任務(比如播放mp3,從網路下載資料)。只要前台進程和可見進程有足夠的記憶體,系統不會回收他們。 後台進程
運行著一個對使用者不可見的activity(調用過 onStop() 方法).這些進程對使用者體驗沒有直接的影響,可以在服務進程、可見進程、前台進 程需要記憶體的時候回收。通常,系統中會有很多不可見進程在運行,他們被儲存在LRU (least recently used) 列表中,以便記憶體不足的時候被第一時間回收。如果一個activity正 確的執行了它的生命週期,關閉這個進程對於使用者體驗沒有太大的影響。 空進程
未運行任何程式組件。運行這些進程的唯一原因是作為一個緩衝,縮短下次程式需要重新使用的啟動時間。系統經常中止這些進程,這樣可以調節程式緩衝和系統緩衝的平衡。
Android 對進程的重要性評級的時候,選取它最高的層級。另外,當被另外的一個進程依賴的時候,某個進程的層級可能會增高。一個為其他進程服務的進程永遠不會比被服務的進程重要級低。因為服務進程比後台activity進程重要級高,因此一個要進行耗時工作的activity最好啟動一個service來做這個工作,而不是開啟一個子進程――特別是這個操作需要的時間比activity存在的時間還要長的時候。例如,在背景播放音樂,向網上上傳網路攝影機拍到的圖片,使用service可以使進程最少擷取到“服務進程”層級的重要級,而不用考慮activity目前是什麼狀態。broadcast receivers做費時的工作的時候,也應該啟用一個服務而不是開一個線程。
2. 單執行緒模式
當一個程式第一次啟動時,Android會同時啟動一個對應的主線程(Main Thread),主線程主要負責處理與UI相關的事件,如使用者的按鍵事件,使用者接觸螢幕的事件以及螢幕繪圖事件,並把相關的事件分發到對應的組件進行處理。所以主線程通常又被叫做UI線程。在開發Android應用時必須遵守單執行緒模式的原則: Android UI操作並不是安全執行緒的並且這些操作必須在UI線程中執行。
2.1 子線程更新UI
Android的UI是單線程(Single-threaded)的。為了避免拖住GUI,一些較費時的對象應該交給獨立的線程去執行。如果幕後的線程來執行UI對象,Android就會發出錯誤訊息CalledFromWrongThreadException。以後遇到這樣的異常拋出時就要知道怎麼回事了!
2.2 Message Queue
在單執行緒模式下,為瞭解決類似的問題,Android設計了一個Message Queue(訊息佇列), 線程間可以通過該Message Queue並結合Handler和Looper組件進行資訊交換。下面將對它們進行分別介紹:
1. MessageMessage訊息,理解為線程間交流的資訊,處理資料後台線程需要更新UI,則發送Message內含一些資料給UI線程。 2. HandlerHandler處理者,是Message的主要處理者,負責Message的發送,Message內容的執行處理。後台線程就是通過傳進來的Handler對象引用來sendMessage(Message)。而使用Handler,需要implement 該類的handleMessage(Message)方法,它是處理這些Message的操作內容,例如Update UI。通常需要子類化Handler來實現handleMessage方法。 3. Message QueueMessage Queue訊息佇列,用來存放通過Handler發布的訊息,按照先進先出執行。每個message queue都會有一個對應的Handler。Handler會向message queue通過兩種方法發送訊息:sendMessage或post。這兩種訊息都會插在message queue隊尾並按先進先出執行。但通過這兩種方法發送的訊息執行的方式略有不同:通過sendMessage發送的是一個message對象,會被Handler的handleMessage()函數處理;而通過post方法發送的是一個runnable對象,則會自己執行。 4. LooperLooper是每條線程裡的Message Queue的管家。Android沒有Global的Message Queue,而Android會自動替主線程(UI線程)建立Message Queue,但在子線程裡並沒有建立Message Queue。所以調用Looper.getMainLooper()得到的主線程的Looper不為NULL,但調用Looper.myLooper()得到當前線程的Looper就有可能為NULL。
對於子線程使用Looper,API Doc提供了正確的使用方法:
class LooperThread extends Thread { public Handler mHandler; public void run() { Looper.prepare(); //建立本線程的Looper並建立一個MessageQueue mHandler = new Handler() { public void handleMessage(Message msg) { // process incoming messages here } }; Looper.loop(); //開始運行Looper,監聽Message Queue } }
這個Message機制的大概流程:
1. 在Looper.loop()方法運行開始後,迴圈地按照接收順序取出Message Queue裡面的非NULL的Message。
2. 一開始Message Queue裡面的Message都是NULL的。當Handler.sendMessage(Message)到Message Queue,該函數裡面設定了那個Message對象的target屬性是當前的Handler對象。隨後Looper取出了那個Message,則調用該Message的target指向的Hander的dispatchMessage函數對Message進行處理。
在dispatchMessage方法裡,如何處理Message則由使用者指定,三個判斷,優先順序從高到低:
1) Message裡面的Callback,一個實現了Runnable介面的對象,其中run函數做處理工作;
2) Handler裡面的mCallback指向的一個實現了Callback介面的對象,由其handleMessage進行處理;
3) 處理訊息Handler對象對應的類繼承並實現了其中handleMessage函數,通過這個實現的handleMessage函數處理訊息。
由此可見,我們實現的handleMessage方法是優先順序最低的!
3. Handler處理完該Message (update UI) 後,Looper則設定該Message為NULL,以便回收!
在網上有很多文章講述主線程和其他子線程如何互動,傳送資訊,最終誰來執行處理資訊之類的,個人理解是最簡單的方法——判斷Handler對象裡面的Looper對象是屬於哪條線程的,則由該線程來執行!1. 當Handler對象的建構函式的參數為空白,則為當前所線上程的Looper;2.Looper.getMainLooper()得到的是主線程的Looper對象,Looper.myLooper()得到的是當前線程的Looper對象。
現在來看一個例子,類比從網路擷取資料,載入到ListView的過程:
public class ListProgressDemo extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.listprogress); ((Button) findViewById(R.id.load_Handler)).setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view) { data = null; data = new ArrayList(); adapter = null; showDialog(PROGRESS_DIALOG); new ProgressThread(handler, data).start(); } }); } @Override protected Dialog onCreateDialog(int id) { switch(id) { case PROGRESS_DIALOG: return ProgressDialog.show(this, "", "Loading. Please wait...", true); default: return null; } } private class ProgressThread extends Thread { private Handler handler; private ArrayList data; public ProgressThread(Handler handler, ArrayList data) { this.handler = handler; this.data = data; } @Override public void run() { for (int i=0; i<8; i++) { data.add("ListItem"); //後台資料處理 try { Thread.sleep(100); }catch(InterruptedException e) { Message msg = handler.obtainMessage(); Bundle b = new Bundle(); b.putInt("state", STATE_ERROR); msg.setData(b); handler.sendMessage(msg); } } Message msg = handler.obtainMessage(); Bundle b = new Bundle(); b.putInt("state", STATE_FINISH); msg.setData(b); handler.sendMessage(msg); } } // 此處甚至可以不需要設定Looper,因為Handler預設就使用當前線程的Looper private final Handler handler = new Handler(Looper.getMainLooper()) { public void handleMessage(Message msg) { // 處理Message,更新ListView int state = msg.getData().getInt("state"); switch(state){ case STATE_FINISH: dismissDialog(PROGRESS_DIALOG); Toast.makeText(getApplicationContext(), "載入完成!", Toast.LENGTH_LONG) .show(); adapter = new ArrayAdapter(getApplicationContext(), android.R.layout.simple_list_item_1, data ); setListAdapter(adapter); break; case STATE_ERROR: dismissDialog(PROGRESS_DIALOG); Toast.makeText(getApplicationContext(), "處理過程發生錯誤!", Toast.LENGTH_LONG) .show(); adapter = new ArrayAdapter(getApplicationContext(), android.R.layout.simple_list_item_1, data ); setListAdapter(adapter); break; default: } } }; private ArrayAdapter adapter; private ArrayList data; private static final int PROGRESS_DIALOG = 1; private static final int STATE_FINISH = 1; private static final int STATE_ERROR = -1; }
這個例子,我自己寫完後覺得還是有點亂,要稍微整理才能看明白線程間互動的過程以及資料的前後變化。隨後瞭解到AsyncTask類,相應修改後就很容易明白了!
2.3 AsyncTaskAsyncTask版:
((Button) findViewById(R.id.load_AsyncTask)).setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view) { data = null; data = new ArrayList(); adapter = null; //顯示ProgressDialog放到AsyncTask.onPreExecute()裡 //showDialog(PROGRESS_DIALOG); new ProgressTask().execute(data); } }); private class ProgressTask extends AsyncTask, Void, Integer> { /* 該方法將在執行實際的後台操作前被UI thread調用。可以在該方法中做一些準備工作,如在介面上顯示一個進度條。*/ @Override protected void onPreExecute() { // 先顯示ProgressDialog showDialog(PROGRESS_DIALOG); } /* 執行那些很耗時的後台計算工作。可以調用publishProgress方法來更新即時的任務進度。 */ @Override protected Integer doInBackground(ArrayList... datas) { ArrayList data = datas[0]; for (int i=0; i<8; i++) { data.add("ListItem"); } return STATE_FINISH; } /* 在doInBackground 執行完成後,onPostExecute 方法將被UI thread調用, * 背景計算結果將通過該方法傳遞到UI thread. */ @Override protected void onPostExecute(Integer result) { int state = result.intValue(); switch(state){ case STATE_FINISH: dismissDialog(PROGRESS_DIALOG); Toast.makeText(getApplicationContext(), "載入完成!", Toast.LENGTH_LONG) .show(); adapter = new ArrayAdapter(getApplicationContext(), android.R.layout.simple_list_item_1, data ); setListAdapter(adapter); break; case STATE_ERROR: dismissDialog(PROGRESS_DIALOG); Toast.makeText(getApplicationContext(), "處理過程發生錯誤!", Toast.LENGTH_LONG) .show(); adapter = new ArrayAdapter(getApplicationContext(), android.R.layout.simple_list_item_1, data ); setListAdapter(adapter); break; default: }}
Android另外提供了一個工具類:AsyncTask。它使得UI thread的使用變得異常簡單。它使建立需要與使用者介面互動的長時間啟動並執行任務變得更簡單,不需要藉助線程和Handler即可實現。
1) 子類化AsyncTask2) 實現AsyncTask中定義的下面一個或幾個方法onPreExecute() 開始執行前的準備工作;doInBackground(Params...) 開始執行幕後處理,可以調用publishProgress方法來更新即時的任務進度;onProgressUpdate(Progress...) 在publishProgress方法被調用後,UI thread將調用這個方法從而在介面上展示任務的進展情況,例如通過一個進度條進行展示。onPostExecute(Result) 執行完成後的操作,傳送結果給UI 線程。 這4個方法都不能手動調用。而且除了doInBackground(Params...)方法,其餘3個方法都是被UI線程所調用的,所以要求:1) AsyncTask的執行個體必須在UI thread中建立;2) AsyncTask.execute方法必須在UI thread中調用; 同時要注意:該task只能被執行一次,否則多次調用時將會出現異常。而且是不能手動停止的,這一點要注意,看是否符合你的需求! 在使用過程中,發現AsyncTask的建構函式的參數設定需要看明白:AsyncTaskParams對應doInBackground(Params...)的參數類型。而new AsyncTask().execute(Params... params),就是傳進來的Params資料,你可以execute(data)來傳送一個資料,或者execute(data1, data2, data3)這樣多個資料。Progress對應onProgressUpdate(Progress...)的參數類型;Result對應onPostExecute(Result)的參數類型。當以上的參數類型都不需要指明某個時,則使用Void,注意不是void。不明白的可以參考上面的例子,或者API Doc裡面的例子。