淺析Android線程模型

摘要:隨着中國移動在8月份相繼發佈基於Google Android的OPhone平臺和手機網上應用商店Mobile Market,以及各大手機生產廠商在2009年北京國際通訊展‎上展出了各自基於Android的手機,Android技術受到愈來愈多的關注。基於這樣的背景下,本文將經過一個例子來初探Android的線程模型。android

關鍵詞:Android;UI thread;線程模型

1引言數據庫

Android一詞本義指機器人,Google於2007年11月發佈了以Android命名的開源移動設備綜合平臺,包括其基於Linux的操做系統、中間件和關鍵的手機應用。而且組建了開放手機聯盟,其成員囊括了全球著名的各大手機生產商和移動運營商。2008年8月,Google又發佈了網上應用商店Android Market。任何一個開發者只須要藉助Android發佈的SDK開發手機應用,便可把開發的應用在Android Market上銷售。目前Android Market上已經有一萬多的應用程序,大大豐富了Android手機用戶的功能。一個完整的產業鏈已經造成。所以開源Android吸引了原來越多的開發人員加入進來。本文將跟讀者一塊兒學習Android的線程模型。api

2 Android進程緩存

在瞭解Android線程之間得先了解一下Android的進程。當一個程序第一次啓動的時候,Android會啓動一個LINUX進程和一個主線程。默認的狀況下,全部該程序的組件都將在該進程和線程中運行。同時,Android會爲每一個應用程序分配一個單獨的LINUX用戶。Android會勁量保留一個正在運行進程,只在內存資源出現不足時,Android會參試中止一些進程從而釋放足夠的資源給其餘新的進程使用,也能保證用戶正在訪問的當前進程有足夠的資源去及時的響應用戶的事件。Android會根據進程中運行的組件類別以及組件的狀態來判斷該進程的重要性,Android會首先中止那些不重要的進程。按照重要性從高到低一共有五個級別:安全

l  前臺進程網絡

前臺進程是用戶當前正在使用的進程。只有一些前臺進程能夠在任什麼時候候都存在。他們是最後一個被結束的,當內存低到根本連他們都不能運行的時候。通常來講,在這種狀況下,設備會進行內存調度,停止一些前臺進程來保持對用戶交互的響應。ide

l  可見進程工具

可見進程不包含前臺的組件可是會在屏幕上顯示一個可見的進程是的重要程度很高,除非前臺進程須要獲取它的資源,否則不會被停止。oop

l  服務進程post

運行着一個經過startService() 方法啓動的service,這個service不屬於上面提到的2種更高重要性的。service所在的進程雖然對用戶不是直接可見的,可是他們執行了用戶很是關注的任務(好比播放mp3,從網絡下載數據)。只要前臺進程和可見進程有足夠的內存,系統不會回收他們。

l  後臺進程

運行着一個對用戶不可見的activity(調用過 onStop() 方法).這些進程對用戶體驗沒有直接的影響,能夠在服務進程、可見進程、前臺進程須要內存的時候回收。一般,系統中會有不少不可見進程在運行,他們被保存在LRU (least recently used) 列表中,以便內存不足的時候被第一時間回收。若是一個activity正確的執行了它的生命週期,關閉這個進程對於用戶體驗沒有太大的影響。

l  空進程

未運行任何程序組件。運行這些進程的惟一緣由是做爲一個緩存,縮短下次程序須要從新使用的啓動時間。系統常常停止這些進程,這樣能夠調節程序緩存和系統緩存的平衡。

Android 對進程的重要性評級的時候,選取它最高的級別。另外,當被另外的一個進程依賴的時候,某個進程的級別可能會增高。一個爲其餘進程服務的進程永遠不會比被服務的進程重要級低。由於服務進程比後臺activity進程重要級高,所以一個要進行耗時工做的activity最好啓動一個service來作這個工做,而不是開啓一個子進程――特別是這個操做須要的時間比activity存在的時間還要長的時候。例如,在後臺播放音樂,向網上上傳攝像頭拍到的圖片,使用service可使進程最少獲取到「服務進程」級別的重要級,而不用考慮activity目前是什麼狀態。broadcast receivers作費時的工做的時候,也應該啓用一個服務而不是開一個線程。

 2單線程模型

    當一個程序第一次啓動時,Android會同時啓動一個對應的主線程(Main Thread),主線程主要負責處理與UI相關的事件,如:用戶的按鍵事件,用戶接觸屏幕的事件以及屏幕繪圖事件,並把相關的事件分發到對應的組件進行處理。因此主線程一般又被叫作UI線程。在開發Android應用時必須遵照單線程模型的原則: Android UI操做並非線程安全的而且這些操做必須在UI線程中執行。

2.1 案例演示

若是在沒有理解這樣的單線程模型的狀況下,設計的程序可能會使程序性能低下,由於全部的動做都在同一個線程中觸發。例如當主線程正在作一些比較耗時的操做的時候,如正從網絡上下載一個大圖片,或者訪問數據庫,因爲主線程被這些耗時的操做阻塞住,沒法及時的響應用戶的事件,從用戶的角度看會以爲程序已經死掉。若是程序長時間不響應,用戶還可能得重啓系統。爲了不這樣的狀況,Android設置了一個5秒的超時時間,一旦用戶的事件因爲主線程阻塞而超過5秒鐘沒有響應,Android會彈出一個應用程序沒有響應的對話框。下面將經過一個案例來演示這種狀況:

本程序將設計和實現查看指定城市的當每天氣狀況的功能,

1.  首先,須要選擇一個天氣查詢的服務接口,目前可供選擇的接口不少,諸如YAHOO的天氣API和Google提供的天氣API。本文將選擇GOOGLE 的天氣查詢API。該接口提供了多種查詢方式,能夠經過指定具體城市的經緯度進行查詢,也能夠經過城市名稱進行查詢。

2.  用戶在輸入框內輸入須要查詢的城市名稱,而後點擊查詢按鈕

3.  當用戶點擊查詢按鈕後,使用已經內置在Android SDK中的HttpClient API來調用GOOGLE 的天氣查詢API,而後解析返回的指定城市的天氣信息,並把該天氣信息顯示在Title上

主要代碼以下:

public class WeatherReport extends Activity implements OnClickListener {

    private static final String GOOGLE_API_URL = "http://www.google.com/ig/api?weather=";

    private static final String NETWORK_ERROR = "網絡異常";

    private EditText editText;

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        editText = (EditText) findViewById(R.id.weather_city_edit);

        Button button = (Button) findViewById(R.id.goQuery);

        button.setOnClickListener(this);

    }

    @Override

    public void onClick(View v) {

        //得到用戶輸入的城市名稱

        String city = editText.getText().toString();

        //調用Google 天氣API查詢指定城市的當日天氣狀況

        String weather = getWetherByCity(city);

        //把天氣信息顯示在title上

        setTitle(weather);

    }

    

    public String getWetherByCity(String city) {

        HttpClient httpClient = new DefaultHttpClient();

        HttpContext localContext = new BasicHttpContext();

        HttpGet httpGet = new HttpGet(GOOGLE_API_URL + city);

        try {

            HttpResponse response = httpClient.execute(httpGet, localContext);

            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {

                httpGet.abort();

            } else {

                HttpEntity httpEntity = response.getEntity();

                return parseWeather(httpEntity.getContent());

            }

        } catch (Exception e) {

            Log.e("WeatherReport", "Failed to get weather", e);

        } finally {

            httpClient.getConnectionManager().shutdown();

        }

        return NETWORK_ERROR;

    }

當用戶輸入城市名稱,而後單擊按鈕進行查詢後,程序會調用Google API的接口得到指定城市的當日天氣狀況。因爲須要訪問網絡,因此當網絡出現異常或者服務繁忙的時候都會使訪問網絡的動做很耗時。本文爲了要演示超時的現象,只須要製造一種網絡異常的情況,最簡單的方式就是斷開網絡鏈接,而後啓動該程序,同時觸發一個用戶事件,好比按一下MENU鍵,因爲主線程由於網絡異常而被長時間阻塞,因此用戶的按鍵事件在5秒鐘內得不到響應,Android會提示一個程序沒法響應的異常,以下圖:

該對話框會詢問用戶是繼續等待仍是強行退出程序。當你的程序須要去訪問未知的網絡的時候都會可能會發生相似的超時的狀況,用戶的響應得不到及時的迴應會大大的下降用戶體驗。因此咱們須要參試以別的方式來實現

2.1 子線程更新UI

    顯然若是你的程序須要執行耗時的操做的話,若是像上例同樣由主線程來負責執行該操做是錯誤的。因此咱們須要在onClick方法中建立一個新的子線程來負責調用GOOGLE API來得到天氣數據。剛接觸Android的開發者最容易想到的方式就是以下:

    public void onClick(View v) {

       //建立一個子線程執行耗時的從網絡上獲取天氣信息的操做

       new Thread() {

           @Override

           public void run() {

              //得到用戶輸入的城市名稱

              String city = editText.getText().toString();

              //調用Google 天氣API查詢指定城市的當日天氣狀況

              String weather = getWetherByCity(city);

              //把天氣信息顯示在title上

              setTitle(weather);

           }

       }.start();

    }

可是很不幸,你會發現Android會提示程序因爲異常而終止。爲何在其餘平臺上看起來很簡單的代碼在Android上運行的時候依然會出錯呢?若是你觀察LogCat中打印的日誌信息就會發現這樣的錯誤日誌:

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

從錯誤信息不難看出Android禁止其餘子線程來更新由UI thread建立的試圖。本例中顯示天氣信息的title實際是就是一個由UI thread所建立的TextView,因此參試在一個子線程中去更改TextView的時候就出錯了。這顯示違背了單線程模型的原則:Android UI操做並非線程安全的而且這些操做必須在UI線程中執行

2.2 Message Queue

在單線程模型下,爲了解決相似的問題,Android設計了一個Message Queue(消息隊列),線程間能夠經過該Message Queue並結合Handler和Looper組件進行信息交換。下面將對它們進行分別介紹:

l  Message Queue

Message Queue是一個消息隊列,用來存放經過Handler發佈的消息。消息隊列一般附屬於某一個建立它的線程,能夠經過Looper.myQueue()獲得當前線程的消息隊列。Android在第一啓動程序時會默認會爲UI thread建立一個關聯的消息隊列,用來管理程序的一些上層組件,activities,broadcast receivers 等等。你能夠在本身的子線程中建立Handler與UI thread通信。

l  Handler

經過Handler你能夠發佈或者處理一個消息或者是一個Runnable的實例。沒個Handler都會與惟一的一個線程以及該線程的消息隊列管理。當你建立一個新的Handler時候,默認狀況下,它將關聯到建立它的這個線程和該線程的消息隊列。也就是說,若是你經過Handler發佈消息的話,消息將只會發送到與它關聯的這個消息隊列,固然也只能處理該消息隊列中的消息。

主要的方法有:

1)   public final boolean sendMessage(Message msg)

把消息放入該Handler所關聯的消息隊列,放置在全部當前時間前未被處理的消息後。

2)   public void handleMessage(Message msg)

關聯該消息隊列的線程將經過調用Handler的handleMessage方法來接收和處理消息,一般須要子類化Handler來實現handleMessage。

l  Looper

Looper扮演着一個Handler和消息隊列之間通信橋樑的角色。程序組件首先經過Handler把消息傳遞給Looper,Looper把消息放入隊列。Looper也把消息隊列裏的消息廣播給全部的Handler,Handler接受到消息後調用handleMessage進行處理。

1)   能夠經過Looper類的靜態方法Looper.myLooper獲得當前線程的Looper實例,若是當前線程未關聯一個Looper實例,該方法將返回空。

2)   能夠經過靜態方法Looper. getMainLooper方法獲得主線程的Looper實例

線程,消息隊列,Handler,Looper之間的關係能夠經過一個圖來展現:

在瞭解了消息隊列及其相關組件的設計思想後,咱們將把天氣預報的案例經過消息隊列來從新實現:

在瞭解了消息隊列及其相關組件的設計思想後,咱們將把天氣預報的案例經過消息隊列來從新實現:

private EditText editText;

    private Handler messageHandler;

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        editText = (EditText) findViewById(R.id.weather_city_edit);

        Button button = (Button) findViewById(R.id.goQuery);

        button.setOnClickListener(this);

        //獲得當前線程的Looper實例,因爲當前線程是UI線程也能夠經過Looper.getMainLooper()獲得

        Looper looper = Looper.myLooper();

        //此處甚至能夠不須要設置Looper,由於 Handler默認就使用當前線程的Looper

        messageHandler = new MessageHandler(looper);

    }

    @Override

    public void onClick(View v) {

        //建立一個子線程去作耗時的網絡鏈接工做

        new Thread() {

            @Override

            public void run() {

                //活動用戶輸入的城市名稱

                String city = editText.getText().toString();

                //調用Google 天氣API查詢指定城市的當日天氣狀況

                String weather = getWetherByCity(city);

                //建立一個Message對象,並把獲得的天氣信息賦值給Message對象

                Message message = Message.obtain();

                message.obj = weather;

                //經過Handler發佈攜帶有天氣狀況的消息

                messageHandler.sendMessage(message);

            }

        }.start();

    }

    //子類化一個Handler

    class MessageHandler extends Handler {

        public MessageHandler(Looper looper) {

            super(looper);

        }

        @Override

        public void handleMessage(Message msg) {

            //處理收到的消息,把天氣信息顯示在title上

            setTitle((String) msg.obj);

        }

    }

經過消息隊列改寫事後的天氣預報程序已經能夠成功運行,由於Handler的handleMessage方法實際是由關聯有該消息隊列的UI thread調用,而在UI thread中更新title並無違背Android的單線程模型的原則。

2.3 AsyncTask

雖然藉助消息隊列已經能夠較爲完美的實現了天氣預報的功能,可是你仍是不得不本身管理子線程,尤爲當你的須要有一些複雜的邏輯以及須要頻繁的更新UI的時候,這樣的方式使得你的代碼難以閱讀和理解。

幸運的是Android另外提供了一個工具類:AsyncTask。它使得UI thread的使用變得異常簡單。它使建立須要與用戶界面交互的長時間運行的任務變得更簡單,不須要藉助線程和Handler便可實現。

1)   子類化AsyncTask

2)   實現AsyncTask中定義的下面一個或幾個方法

Ø  onPreExecute(), 該方法將在執行實際的後臺操做前被UI thread調用。能夠在該方法中作一些準備工做,如在界面上顯示一個進度條。

Ø  doInBackground(Params...), 將在onPreExecute 方法執行後立刻執行,該方法運行在後臺線程中。這裏將主要負責執行那些很耗時的後臺計算工做。能夠調用publishProgress方法來更新實時的任務進度。該方法是抽象方法,子類必須實現。

Ø  3. onProgressUpdate(Progress...),在publishProgress方法被調用後,UI thread將調用這個方法從而在界面上展現任務的進展狀況,例如經過一個進度條進行展現。

Ø  4. onPostExecute(Result), 在doInBackground 執行完成後,onPostExecute 方法將被UI thread調用,後臺的計算結果將經過該方法傳遞到UI thread.

爲了正確的使用AsyncTask類,如下是幾條必須遵照的準則:

1)   Task的實例必須在UI thread中建立

2)   execute方法必須在UI thread中調用

3)   不要手動的調用onPreExecute(), onPostExecute(Result),doInBackground(Params...), onProgressUpdate(Progress...)這幾個方法

4)   該task只能被執行一次,不然屢次調用時將會出現異常

下面咱們將經過AsyncTask而且嚴格遵照上面的4條準則來改寫天氣預報的例子:

    public void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.main);

       editText = (EditText) findViewById(R.id.weather_city_edit);

       Button button = (Button) findViewById(R.id.goQuery);

       button.setOnClickListener(this);

    }

    public void onClick(View v) {

       //得到用戶輸入的城市名稱

       String city = editText.getText().toString();

    //必須每次都從新建立一個新的task實例進行查詢,不然將提示以下異常信息

    //the task has already been executed (a task can be executed only once)

       new GetWeatherTask().execute(city);

    }

    class GetWeatherTask extends AsyncTask<String, Integer, String> {

       @Override

       protected String doInBackground(String... params) {

           String city = params[0];

           //調用Google 天氣API查詢指定城市的當日天氣狀況

           return getWetherByCity(city);

       }

       protected void onPostExecute(String result) {

           //把doInBackground處理的結果即天氣信息顯示在title上

           setTitle(result);

       }

    }

注意這行代碼:new GetWeatherTask().execute(city); 值得一提的是必須每次都從新建立一個新的GetWeatherTask來執行後臺任務,不然Android會提示「a task can be executed only once」的錯誤信息。

通過改寫後的程序不只顯得很是的簡潔,並且還減小了代碼量,大大加強了可讀性和可維護性。由於負責更新UI的onPostExecute方法是由UI thread調用,因此沒有違背單線程模型的原則。良好的AsyncTask設計大大下降了咱們犯錯誤的概率。

5綜述

    本文首先大體介紹了Android的單線程模型及其原則。而後經過一個真實案例展現剛接觸Android的開發人員在不理解Android的單線程模型下容易犯的錯誤。最後經過幾種正確的方式實現該案例,進一步認識和理解Android的單線程模型及其原則。因爲更多地關注線程模型,本文或許不足以幫助讀者全面的認識Android技術,關於文中提到的其餘技術細節以及Android的其餘相關技術能夠訪問Android的官方網站進行進一步的瞭解和學習。

相關文章
相關標籤/搜索