HandlerThread介紹(譯文)

原文地址: https://blog.nikitaog.me/2014/10/11/android-looper-handler-handlerthread-i/

當有人問你在Android中thread是怎樣使用的?你可能會說“我用過AsyncTask在后臺處理異步任務(wù)”柄粹。很好弦讽,還有其他的嗎涧偷?啊,我還聽說過Handler柏靶,因為我使用Handler來展示toast诈火,或者執(zhí)行延遲任務(wù)魔种。很不錯,但是我在下文將介紹一個新的框架封寞。

從我們熟悉的AsyncTask類開始然评,我相信每個Android開發(fā)者都遇到過它。首先狈究,我們可以從官方文檔得到AsyncTask的一個很好的介紹碗淌。如果你不想浪費精力去學(xué)習如何管理Android threads,那么用AsyncTask來執(zhí)行后臺任務(wù)將是一個不錯的選擇抖锥。比較重要的一點你應(yīng)該了解亿眠,那就是AsyncTask中只有doInBackground()方法運行在非UI線程,其他方法都運行在UI線程磅废。這兒有一個AsyncTask的使用例子:

//MyActivity.java
public class MyActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        MyAsyncTask myTask = new MyAsyncTask(this);
        myTask.execute("http://developer.android.com");
    }
}

//MyAsyncTask.java
public class MyAsyncTask extends AsyncTask<String, Void, Integer> {

    private Context mContext;

    public MyAsyncTask(Context context) {
        mContext = context.getApplicationContext();
    }

    @Override
    protected void onPreExecute() {
        Toast.makeText(mContext, "Let's start!", Toast.LENGTH_LONG).show();
    }

    @Override
    protected Integer doInBackground(String... params) {
        HttpURLConnection connection;
        try {
            connection = (HttpURLConnection) new URL(params[0])
                    .openConnection();
            return connection.getResponseCode();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return -1;
    }

    @Override
    protected void onPostExecute(Integer integer) {
        if (integer != -1) {
            Toast.makeText(mContext, "Got the following code: " + integer, 
                Toast.LENGTH_LONG).show();
        }
    }
}

接下來我們在main布局文件中放一個ProgressBar用來運行我們的demo:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:orientation="vertical"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:gravity="center">
    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/progressBar"/>
</LinearLayout>

從服務(wù)端得到狀態(tài)響應(yīng)碼是一個耗時操作纳像,在這期間我們不想阻塞UI線程,所以我們在這里使用AsyncTask拯勉。當然使用AsyncTask有很多缺點(如果AsyncTask是一個Activity/Fragment的內(nèi)部類竟趾,那么它持有外部類的一個隱式對象憔购,這種是不好的。為什么呢潭兽?因為Activity/Fragment在configuration change的時候會被銷毀倦始,但是AsyncTask存活的時候,他們?nèi)匀粫槐A粼趦?nèi)存中山卦。如果AsyncTask被聲明為獨立的或者靜態(tài)內(nèi)部類鞋邑,然后使用Context的引用去更新UI,更新UI前我們應(yīng)該檢查Context是否為空)账蓉。AsyncTask是一個一次性任務(wù)枚碗,同一個AsyncTask對象,不能通過重新調(diào)用execute來讓任務(wù)重新運行铸本,應(yīng)該重新創(chuàng)建一個AsyncTask對象肮雨。

有意思的是如果你試圖在doInBackground()方法中顯示一個toast,程序會報下面的錯:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:121)
at android.widget.Toast$TN.<init>(Toast.java:322)
at android.widget.Toast.<init>(Toast.java:91)
at android.widget.Toast.makeText(Toast.java:238)
at com.example.testapp.MyActivity$MyAsyncTask.doInBackground(MyActivity.java:25)
at com.example.testapp.MyActivity$MyAsyncTask.doInBackground(MyActivity.java:21)

為什么會有這個錯誤呢箱玷?說的簡單點怨规,因為Toast僅僅能再UI線程中展示。說的準確點锡足,因為Toast能在擁有Looper的線程中展示波丰。你可能會問了“什么是Looper啊舶得?”好了掰烟,是時候刨根問底了。AsyncTask是一個不錯的類沐批,但是如果它不能夠滿足你的需求呢纫骑?如果我們深入研究AsyncTask的實現(xiàn),就會發(fā)現(xiàn)九孩,AsyncTask包含了Handler先馆,Runnable,Thread捻撑。我們都清楚Java中的thread磨隘,但是在Android中,你會發(fā)現(xiàn)繼承Thread的另一個類HandlerThread顾患。HandlerThread和Thread唯一比較大的區(qū)別是番捂,HandlerThread包含Looper,Thread以及MessageQueue江解。Looper對當前線程提供了一個MessageQueue设预。那么什么是MessageQueue呢?MessageQueue是一個包含將要執(zhí)行任務(wù)的隊列犁河。Looper遍歷這個隊列鳖枕,然后發(fā)送Message到對應(yīng)的handler來處理魄梯。任何thread有一個獨一無二的Looper,ThreadLocal幫我們做到了這一點宾符。

**你可能會問了酿秸,如果task都被Handler處理,為什么要設(shè)計這么復(fù)雜魏烫?
至少有兩點優(yōu)勢:

  1. 正如之前提到的辣苏,當同時好幾個線程工作時,如果我們想要順序依次執(zhí)行哄褒,HandlerThread幫助我們避免了資源競爭稀蟋。
  2. 當Thread中任務(wù)結(jié)束后,Thread是不能被重用的呐赡,但是因為Looper的存在退客,包含Looper的線程被保存了活動狀態(tài),直到你調(diào)用了quit方法链嘀。所以每次你想運行一個后臺任務(wù)的時候萌狂,你不需要創(chuàng)建一個新的實例對象。**

你也可以自己創(chuàng)建一個帶Looper的Thread怀泊,但是我推薦你使用HandlerThread粥脚。HandlerThread自帶Looper,并且一些初始化的工作都為你做好了包个。那么什么是Handler呢?Handler包含兩個基本功能:post任務(wù)到MessageQueue并且執(zhí)行它們冤留。Handler通過Looper默認與thread關(guān)聯(lián)碧囊,但是你也可以在構(gòu)造函數(shù)中提供Looper,用來關(guān)聯(lián)Handler到其他Thread纤怒。下面來看一個實際的例子糯而,設(shè)想一下我們有一個Activity,我們想post任務(wù)到MessageQueue泊窘,這些任務(wù)被延遲執(zhí)行熄驼。

public class MyActivity extends Activity {

    private Handler mUiHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Thread myThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (i == 2) {
                        mUiHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(MyActivity.this,
                                        "I am at the middle of background task",
                                        Toast.LENGTH_LONG)
                                        .show();
                            }
                        });
                    }
                }
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MyActivity.this,
                                "Background task is completed",
                                Toast.LENGTH_LONG)
                                .show();
                    }
                });
            }
        });
        myThread.start();
    }
}

由于mUiHandler與UI線程相關(guān)聯(lián)(Handler在默認構(gòu)造函數(shù)中,得到UI線程的Looper)烘豹,mUiHandler是一個類成員瓜贾,我們通過匿名內(nèi)部類來使用它,所以可以post任務(wù)到UI線程携悯。在上面的例子中祭芦,我們使用Thread,如果我們要post一個新的任務(wù)憔鬼,我們不能重新使用這個Thread實例龟劲,我們必須創(chuàng)建一個新的Thread對象胃夏。那么有其他辦法嗎?當然昌跌。我們可以使用帶Looper的Thread仰禀。下面有一個簡單的例子,不同的是我們使用了HandlerThread而不是Thread蚕愤,優(yōu)點就是Thread可以被重復(fù)執(zhí)行:

//MyActivity.java
public class MyActivity extends Activity {

    private Handler mUiHandler = new Handler();
    private MyWorkerThread mWorkerThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mWorkerThread = new MyWorkerThread("myWorkerThread");
        Runnable task = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (i == 2) {
                        mUiHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(MyActivity.this,
                                        "I am at the middle of background task",
                                        Toast.LENGTH_LONG)
                                        .show();
                            }
                        });
                    }
                }
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MyActivity.this,
                                "Background task is completed",
                                Toast.LENGTH_LONG)
                                .show();
                    }
                });
            }
        };
        mWorkerThread.start();
        mWorkerThread.prepareHandler();
        mWorkerThread.postTask(task);
        mWorkerThread.postTask(task);
    }

    @Override
    protected void onDestroy() {
        mWorkerThread.quit();
        super.onDestroy();
    }
}

//MyWorkerThread.java
public class MyWorkerThread extends HandlerThread {

    private Handler mWorkerHandler;

    public MyWorkerThread(String name) {
        super(name);
    }

    public void postTask(Runnable task){
        mWorkerHandler.post(task);
    }

    public void prepareHandler(){
        mWorkerHandler = new Handler(getLooper());
    }
}

在這個例子中答恶,我使用了HandlerThread,因為我不想自己去管理Looper审胸,HandlerThread已經(jīng)幫我們實現(xiàn)了它亥宿。一旦我們開始了HandlerThread,在任何時間我們可以post任務(wù)砂沛,當你想停止HandlerThread的時候別忘了調(diào)用quit方法烫扼。mWorkerHandler通過myWorkerThread的Looper與myWorkerThread相綁定。你不能在HandlerThread構(gòu)造函數(shù)中初始化mWorkerHandler碍庵,因為在thread不是活動狀態(tài)的時候getLooper會返回null映企。你或許會發(fā)現(xiàn)下面這種handler的初始化方式:

private class MyWorkerThread extends HandlerThread {

    private Handler mWorkerHandler;

    public MyWorkerThread(String name) {
        super(name);
    }

    @Override
    protected void onLooperPrepared() {
        mWorkerHandler = new Handler(getLooper());
    }

    public void postTask(Runnable task){
        mWorkerHandler.post(task);
    }
}

有時它會起作用,有時在調(diào)用postTask的時候静浴,會出現(xiàn)NullPointerException堰氓,mWorkerHandler為null。臥槽苹享?

[站外圖片上傳中...(image-19d4aa-1531388491240)]

為什么呢双絮?那是因為在新線程創(chuàng)建的時候需要native調(diào)用。如果我們看onLooperPrepared被調(diào)用的代碼得问,就會在HandlerThread中發(fā)現(xiàn)如下的代碼片段:

public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

僅僅在新線程被創(chuàng)建并且啟動的情況下才會run方法才會被調(diào)用囤攀。出現(xiàn)NullPointerException異常的原因在于,run()方法在postTask()之后調(diào)用宫纬。所以就是被兩個線程(主線程和后臺線程)之間的競爭條件害慘了焚挠。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市漓骚,隨后出現(xiàn)的幾起案子蝌衔,更是在濱河造成了極大的恐慌,老刑警劉巖蝌蹂,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件噩斟,死亡現(xiàn)場離奇詭異,居然都是意外死亡孤个,警方通過查閱死者的電腦和手機亩冬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人硅急,你說我怎么就攤上這事覆享。” “怎么了营袜?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵撒顿,是天一觀的道長。 經(jīng)常有香客問我荚板,道長凤壁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任跪另,我火速辦了婚禮拧抖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘免绿。我一直安慰自己唧席,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布嘲驾。 她就那樣靜靜地躺著淌哟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辽故。 梳的紋絲不亂的頭發(fā)上徒仓,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音誊垢,去河邊找鬼掉弛。 笑死,一個胖子當著我的面吹牛喂走,可吹牛的內(nèi)容都是我干的狰晚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼缴啡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瓷们?” 一聲冷哼從身側(cè)響起业栅,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谬晕,沒想到半個月后碘裕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡攒钳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年帮孔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡文兢,死狀恐怖晤斩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情姆坚,我是刑警寧澤澳泵,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站兼呵,受9級特大地震影響兔辅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜击喂,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一维苔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧懂昂,春花似錦介时、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饿序,卻和暖如春勉失,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背原探。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工乱凿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咽弦。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓徒蟆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親型型。 傳聞我的和親對象是個殘疾皇子段审,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容