文章出處

多線程環境下的ui修改

  開發過程中,經常需要開啟新的線程,并且在其它線程中改變ui線程的ui對象的狀態。Android設計出于性能考慮,ui對象為非線程安全的,然后讓ui對象僅能在主線程——也就是ui線程中被修改,以此來保證ui對象的線程安全。以下引出一些跨線程修改ui對象的情形,以及可能的實現方式。

1. 定時更新ui

一些類似定時更新ui的代碼,如動畫控制。

1.1 多線程定時更改ui

具體就是新啟動(不讓ui線程sleep而卡住)一個線程去計時,之后定時來通知ui修改。

1.1.1 新啟動線程定時執行任務

  • Timer + TimerTask
  • 新啟動線程:run方法中:while(true) + Thread.Sleep/SystemClock.Sleep

 

  本質上都是一個新線程在背后計時。由于使用一個新的非ui線程執行計時,需要在時間到達后去通知ui修改。出于性能考慮,安卓的ui控件不是線程安全的,然后谷歌設計只讓ui線程(主線程)能夠直接修改ui控件,其它非ui線程不能來達到ui的線程安全。

1.1.2 非ui線程更新ui控件的方式

  • runOnUiThread
  • Handler
  • View.postDelay

  runOnUiThread從名字上可以看出就是專門供其它線程更改ui使用的。而handler用于不同線程之間的消息傳遞,可以讓線程T1在希望的時刻去通知T2執行某些特定操作。這當然也完全能滿足[非ui線程定時通知ui線程更改ui控件狀態] 的目的。

1.2 Handler定時更新ui

如果只是為了完成“定時執行”,那么不開啟一個新線程,使用handler.sendEmptyMessageDelayed(1,2000)也可以。

在開始定時任務執行的地方調用:
handler.sendEmptyMessage();

主線程中的handler重寫handleMessage:
final int UPDATE_ANIM = 1001;
boolean isAnimRun;
Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        case UPDATE_ANIM:
            if(isAnimRun){
                //更新ui的代碼,代碼中根據情況改變isAnimRun標志。
                handler.sendEmptyMessageDelayed(UPDATE_ANIM, 2000);
            }
            break;
        }
    }
};

  可見,開啟新的線程一般是執行一些非ui但耗時的操作,比如網絡獲取新聞數據。而定時任務這樣的事情Handler也可以搞定。

2.Handler的跨線程通信

2.1原理簡述

為了讓其它線程發消息通知當前線程執行一些任務,當前線程線程可以這樣做:

  • 當前線程執行Looper.prepare方法,這會產生一個MessageQueue和一個Looper實例。兩個實例會被保存到ThreadLocal中,prepare只能執行一次,以保證每個線程僅有唯一的Looper和MessageQueue。
>> MessageQueue用來接收其它線程丟進來的Message,使用先進先出的時間順序維護所有消息。
>> Looper管理MessageQueue,主要就是不斷的(loop)從中取走Message,然后發送給把此Message的target去處理,
   target即為發送此Message的handler。 
  • 當前線程可以創建一或多個Handler對象。Handler對象創建時會記錄當前線程的MessageQueue和Looper實例作為自己的成員變量。。所以,handler在哪個線程中創建,就和哪個線程的Looper、MessageQueue對應起來。

  • 其它線程可以根據需要創建Message對象,調用handler的sendMessage,此實例方法會設置Message的target為方法的handler實例。 然后Message被發送到handler記錄的MessageQueue中。

  • 當前線程執行Looper.loop方法,進入一個死循環。執行方式就是一個生產者消費者模式。消費者就是Looper,loop方法一旦執行,就去MessageQueue中取消息,沒有消息時會阻塞,取到消息就把Message交給Message.target對應的handler去處理。生產者就是其它線程,其它線程創建Message并使用當前線程的handler執行sendMessage方法(會設置方法的實例對象handler為Message的target)來發送消息到handler字段記錄的隊列中。

  通過以上Looper、MessageQueue、Handler的合作,每一個線程都通過Handler來讓其它線程根據需要通知自己執行一些操作。 Handler可以實現不同線程之間的通信,默認主線程已經提供好了Looper和MessageQueue,所以按需要自己寫個handler對象,就可以在新開啟的其它線程中使用handler來讓主線程執行操作,常見的就是更新ui控件。 每個Message對象同時設置了when屬性,這樣可以做到消息通知的立即執行和延遲執行。

2.2 讓自己的線程開始接收消息

其它線程默認Looper和MessageQueue是沒有準備好的,可以在run方法里通過以下幾步配置好:

  • 調用Looper.prepare()方法,這會建立Looper實例和對應的MessageQueue。

  • 設計好自己的Handler對象,主要是處理消息,之后供外界用它來向MessageQueue發送消息。

  • 最后調用Looper.loop()方法開啟Looper對消息隊列的監測。表示可以讓其它線程來發送通知了。

2.3 Toast.show的調用

本質上對ui控件的修改,最終都是ui線程執行的。比如我們的線程里需要設置某個TextView的Text屬性,那么只能是使用ui線程的handler去發送消息給ui線程去執行。或者使用runOnUiThread這樣的簡便方法。

一個特殊的例子就是Toast.show,它僅要求當前線程Looper和MessageQueue準備好即可:
void run(){
    Looper.prepare();
    Toast.makeText(DemoActivity.this,"Wow......",0).show();
    Looper.loop();
}

3.多線程更新ListView

另一個常見“跨線程改變ui”的例子就是網絡數據加載,比如加載新聞列表到ListView,啟動新的線程是為了避免主線程阻塞而卡ui。相比啟動一個線程去達到計時器的目的,使用非ui線程去執行耗時操作等就劃算得多了。一般的套路是:

  • 界面上需要新的數據時,啟動一個線程去從網絡或本地獲取一批數據,通常是分頁獲得一個合理的數據集合。界面上顯示進度條,并且使得一部分界面不可交互。

  • 獲取數據完畢后,調用adapter的notifyDataSetChanged()它是一個ui操作,需要使用“非ui線程執行ui操作”的技巧去完成。

  ListAdapter的要點就是:復用屏幕不可見的View對象,并且使用ViewHolder來避免findViewById的開銷。

4.AsyncTask

AsyncTask是圍繞Thread和Handler構建的一個簡單包裹類,可以完成一些后臺執行任務后更新UI的操作,api中指出操作不宜過長——a few seconds at most,如果是更為耗時的任務,就需要自己使用java.util.concurrent 包下的諸如Executor, ThreadPoolExecutor 和 FutureTask等類。

4.1 基本要點

  • new AsyncTask<Prams,Progress,Result>

  • onPreExecute中執行一些耗時操作的預備動作,可以是ui操作,如顯示進度條。

  • doInBackground中執行耗時任務,調用publishProgress來更新進度。

  • onPostExecute中使用結果數據,更新ui,如dismiss掉進度條。

  • 應該在ui線程中創建AsyncTask的實例,并調用其execute方法。

4.2 原理簡述

AsyncTask擁有一個靜態的Handler成員:

private static final InternalHandler sHandler = new InternalHandler();

  InternalHandler繼承自Handler,它接收處理兩個消息:

public void handleMessage(Message msg) {
        AsyncTaskResult result = (AsyncTaskResult) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }

  可以知道,更新進度結束2個回調方法的執行是涉及到ui操作的,因此必須確保sHandler對象是屬于ui線程的: The AsyncTask class must be loaded on the UI thread. This is done automatically as of JELLY_BEAN.

AsyncTask的構造方法使用創建好了一個Callable和一個FutureTask來實現線程的創建。

api要求AsyncTask的創建和execute方法的調用必須在ui線程中執行,實際上重點是execute方法,它里面調用了onPreExecute()方法,此方法會涉及ui操作,而且沒有使用handler機制,就必須被在ui線程中執行。execute只能執行一次,我們通常會寫new MyAsyncTask().execute() 這樣的代碼,所以為了確保在ui線程中執行execute,我們最好是在ui線程中執行AsyncTask的創建——當然了,在非ui線程中創建AsyncTask實例通常也沒多大意義。

  • 第1個關鍵點就是InternalHandler,它保證updateProgress和postExecute在ui線程中執行。sHandler是類級別的,它結合AsyncTaskResult完成了在UI線程中調用指定AsyncTask的updateProgress和postExecute:
AsyncTaskResult result = (AsyncTaskResult) msg.obj; 
result.mTask.finish(result.mData[0]);
result.mTask.onProgressUpdate(result.mData);
  • 第2個關鍵點就線程創建部分:Executor,最終是使用ThreadPoolExecutor完成對Future的啟動執行:
//在executeOnExecutor中
exec.execute(mFuture);

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()