Android多線程

這幾天在研究Android的多線程方面的知識,閱讀了許多大牛的文章,發(fā)現(xiàn)Android的多線程方式挺多的曲掰,關(guān)于各種方式的優(yōu)缺點也都各有看法,所以這部分的知識還是很容易令人覺得混亂的胎许,所以自己梳理了相關(guān)知識峻呛,用自己的角度去簡單總結(jié)這些知識,鞏固自己知識的同時也希望幫助到其他人辜窑。
首先钩述,從兩個問題入手:我們?yōu)槭裁葱枰嗑€程機制?什么時候需要到多線程穆碎?
答:1牙勘、因為Android官方明確聲明在多線程編程時有兩大原則:第一、不要阻塞UI線程(即主線程所禀,下文兩個稱呼可互換)方面、第二、不要在UI線程之外訪問UI組件色徘。這個話題是老生常談了恭金,想必很多人都明白個中緣由。
2褂策、我對多線程的使用情況歸結(jié)為主要有兩種情況:第一横腿、將任務(wù)從主線程拋到工作線程,第二種情況是將任務(wù)從工作線程拋到主線程斤寂。這兩種情況其實跟上面兩個原則是對應(yīng)的耿焊。當(dāng)我們有耗時的任務(wù),如果在UI線程中執(zhí)行遍搞,那就會阻塞UI線程了罗侯,必須要拋到工作線程中去執(zhí)行;而當(dāng)我們要更新UI組件時尾抑,就一定得在UI線程里執(zhí)行了歇父,所以就得把在工作線程中執(zhí)行的任務(wù)結(jié)果返回到UI線程中去更新組件了。

一再愈、將任務(wù)從工作線程拋到主線程

我們先從一段代碼開始

protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        button = (Button) findViewById(R.id.button);  
        text = (TextView) findViewById(R.id.text);//耗時任務(wù)完成時在該TextView上顯示文本  

        mRunnable = new Runnable() {  
            @Override  
            public void run() {  
                try {  
                    Thread.sleep(5000);//模擬耗時任務(wù)  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                 text.setText("Task Done!!");//在非UI線程之外去訪問UI組件  
            }  
        };  

        button.setOnClickListener(new View.OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                Thread thread = new Thread(mRunnable);  
                thread.start();  
            }  
        });  

    }  

布局上只定義了一個Button和TextView,Button按下時會開啟一個新線程執(zhí)行耗時任務(wù)榜苫,任務(wù)完成后更新TextView的文本。有點基礎(chǔ)的都能明白這段代碼是有問題的翎冲,因為它在非UI線程之外去訪問UI組件了垂睬。
那這個時候就得想辦法讓text.setText("Task Done!!");這句代碼拋到UI線程中去執(zhí)行了。對此,我們大概有四種方法驹饺,下面分別演示钳枕。
有如下5種方式

1、Handler.sendXXXMessage()等方法

首先是在上面的Activity中定義一個Handler

Handler mHandler = new Handler(){  
        @Override  
        public void handleMessage(Message msg){  
            if(msg.what == 0x123){  
                text.setText("Task Done!!");  
            }  
        }  
    };  

然后將工作線程的代碼改為下面的樣子

mRunnable = new Runnable() {  
            @Override  
            public void run() {  
                try {  
                    Thread.sleep(5000);//模擬耗時任務(wù)  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                mHandler.sendEmptyMessage(0x123);//關(guān)于發(fā)消息的方法有很多,比如sendMessage(Message msg),sendMessageDelayed(Message msg, long delayMills)等等赏壹,可按具體需求選擇鱼炒,這里不作擴展  

            }  
        };  

這樣程序運行起來后就不會報錯了。
關(guān)于Handler的底層機制網(wǎng)上有非常多文章作了詳細(xì)的描述蝌借,比如有張鴻洋的Android 異步消息處理機制 讓你深入理解 Looper昔瞧、Handler、Message三者關(guān)系菩佑,這里也小小地提一下自晰,為后面的內(nèi)容做一些必要的鋪墊。
一個線程只有一個Looper, 而一個Looper持有一個MessageQueue, 當(dāng)調(diào)用Looper.prepare()時稍坯,Looper就與當(dāng)前線程關(guān)聯(lián)起來了(在Activity里沒有顯示調(diào)用Looper.prepare()是因為系統(tǒng)自動在主線程里幫我們調(diào)用了)酬荞,而Handler是與Looper的線程是綁定的,查看Handler類的源碼可以發(fā)現(xiàn)它幾個構(gòu)造函數(shù)瞧哟,其中有接收一個Looper參數(shù)的混巧,也有不接收Looper參數(shù)的,從上面的代碼上看绢涡,我們沒有為Handler指定Looper牲剃,那么Handler就默認(rèn)更當(dāng)前線程(即主線程)的Looper關(guān)聯(lián)起來了,之所以啰嗦那么多就是因為這決定了Handler.handlerMessage(msg)方法體里的代碼到底在哪個線程里執(zhí)行雄可,我們再梳理一下凿傅,Looper.prepare調(diào)用決定了Looper與哪個線程關(guān)聯(lián),間接決定了與這個Looper相關(guān)聯(lián)的Handler.handlerMessage(msg)方法體里的代碼執(zhí)行的線程数苫。(太啰嗦了)
現(xiàn)在回到上面的代碼聪舒,我們的Handler是在主線程里的定義的,所以也默認(rèn)跟主線程的Looper相關(guān)聯(lián)虐急,即handlerMessage方法的代碼會在UI線程執(zhí)行箱残,因此更新TextView就不會報錯了。下面這張圖是弄清handlerMessage(msg)方法體里的代碼的執(zhí)行線程的思路


圖片發(fā)自簡書App

2止吁、Handler.post(Runnable)

只要將上面代碼中的

mHandler.sendEmptyMessage(0x123);  

改成

mHandler.post(new Runnable() {  
    @Override  
    public void run() {  
        text.setText("Task Done!!");                 
        }  
 });  

就可以了被辑,可能有人看到new了一個Runnable就以為是又開了一個新線程,事實上并沒有開啟任何新線程敬惦,只是使run()方法體的代碼拋到與mHandler相關(guān)聯(lián)的線程中執(zhí)行盼理,經(jīng)過上面的分析我們也知道m(xù)Handler是與主線程關(guān)聯(lián)的,所以更新TextView組件依然發(fā)生在主線程了俄删。

3宏怔、Activity.runOnUIThread(Runnable)

將上面的代碼改成

runOnUiThread(new Runnable() {  
        @Override  
        public void run() {  
            text.setText("Task Done!!");  
        }  
    });  

這個看起來跟上面的方法很像奏路,差別就是這種方法不需要去定義Handler。

4臊诊、View.post(Runnable)

將上面的代碼改為

text.post(new Runnable() {  
    @Override  
    public void run() {  
        text.setText("Task Done!!");  
    }  
});  

這個看起來依舊是跟上面的方法很像,依然不用定義Handler鸽粉。

5、AsyncTask

這種方法要改動上面整個開新線程的代碼抓艳,具體代碼在入門書籍上基本都有触机,這里就不附上了,思路就是在doInBackground(Params…) 方法里執(zhí)行耗時邏輯玷或,然后在onPostExecute(Result) 中將結(jié)果更新回UI組件威兜。

使用哪種大多數(shù)情況我還是根據(jù)代碼風(fēng)格和習(xí)慣來決定,上面這5種方法具體在效率上是否有巨大差異庐椒,我沒有深入研究,這方面有研究的兄弟希望可以在留言里交流一下

二蚂踊、將任務(wù)從主線程拋到工作線程

正如前言所說约谈,耗時任務(wù)不能在主線程去進行,需要另外開一個線程犁钟。分別有下面幾種方法:

1棱诱、Thread、Runnable

這個是最傳統(tǒng)的方法了涝动,相信每個學(xué)過Java基礎(chǔ)的人都知道迈勋。無非就是繼承Thread類覆寫run()然后通過thread.start()或?qū)崿F(xiàn)Runnable接口復(fù)寫run()然后New Thread(Runnable).start(),在上面的例子中就是通過這種最普通的方法去開新線程的醋粟,不過在實際開發(fā)中靡菇,這種開新線程的方法是很不被推薦的,理由如下:1)當(dāng)你有多個耗時任務(wù)時就會開多個新線程米愿,開啟新線程的以及調(diào)度多個線程的開銷是非常大的厦凤,這往往會帶來嚴(yán)重的性能問題,例如育苟,你有100個耗時任務(wù)较鼓,那就開100個線程。2)如果在線程中執(zhí)行循環(huán)任務(wù)违柏,只能通過一個Flag來控制它的停止博烂,如while(!iscancel){//耗時任務(wù)}。

2漱竖、HandlerThread

在正式介紹HandlerThread前禽篱,我們先來看看以下代碼:

protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        button = (Button) findViewById(R.id.button);  
        button.setOnClickListener(new View.OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                mOtherHandler.sendEmptyMessage(0x124);  
            }  
        });  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                Looper.prepare();//在新線程中調(diào)用  
                mOtherHandler = new Handler() { //默認(rèn)關(guān)聯(lián)新線程的Looper  
                    @Override  
                    public void handleMessage(Message msg) {  

                        if (msg.what == 0x124) {  
                            try {  
                                Log.d("HandlerThread", Thread.currentThread().getName());//打印線程名  
                                Thread.sleep(5000);//模擬耗時邏輯  
                            } catch (InterruptedException e) {  
                                e.printStackTrace();  
                            }  
                        }  

                    }  
                };  
                Looper.loop();  
            }  
        }).start();  
    }  

可以看到這里用的是第一種方法開啟新線程的,但是在新線程里初始化了Looper(因為不是在主線程闲孤,所以要我們自己調(diào)用Looper.prepare()和loop()),還定義了一個Handler ,前面我之所以那么啰嗦谆级,就是為了讓你明白:這個Handler的handlerMessage(msg)方法體的代碼是在新線程(工作線程)中執(zhí)行的烤礁,而不是主線程(忘了的話拉回去看前面的內(nèi)容),所以我們只需要在Button的點擊事件中調(diào)用sendXXXMessage就可以讓耗時任務(wù)在新線程中執(zhí)行了肥照。
有意思的是脚仔,如果我們以非常快的速度連續(xù)點擊兩次Button,你會發(fā)現(xiàn)打印出來的兩條Log是以間隔5秒相繼出現(xiàn)的舆绎。這是因為每點一次按鈕并沒有開啟都開啟一個新線程鲤脏,而只是發(fā)送了一條消息,我們在onCreate()里就已經(jīng)把一個新線程開好了吕朵,然后調(diào)用Looper.loop()使這個線程一直處于循環(huán)狀態(tài)了猎醇,而我們每發(fā)一條消息,消息都會在MessageQueue里排隊努溃×蛩唬總而言之,不管我們點多少次按鈕梧税,都只有一個工作線程沦疾,多個耗時任務(wù)在這個工作線程的隊列中排隊處理。思路如下圖


圖片發(fā)自簡書App

鋪墊了這么多第队,可以把HandlerThread拉出來了哮塞,查看源碼,你會發(fā)現(xiàn)HandlerThread也是Thread的子類凳谦,那豈不是還是跟第一種方法一樣忆畅,說是也是,說不是也不是尸执。其實呢家凯,HandlerThread就是對上面的代碼的一種封裝,我們來看看它是怎么用的

handlerThread = new HandlerThread("MyNewThread");//自定義線程名稱  
        handlerThread.start();  
        mOtherHandler = new Handler(handlerThread.getLooper()){  
            @Override  
            public void handleMessage(Message msg){  

                if (msg.what == 0x124){  
                    try {  
                        Log.d("HandlerThread", Thread.currentThread().getName());  
                        Thread.sleep(5000);//模擬耗時任務(wù)  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  

            }  
        };  

這段代碼跟前面那一段代碼是完全等價的如失,HandlerThread的好處是代碼看起來沒前面的版本那么亂肆饶,相對簡潔一點。還有一個好處就是通過handlerThread.quit()或者quitSafely()使線程結(jié)束自己的生命周期岖常。
可能有人問了驯镊,那用以上方式執(zhí)行完耗時任務(wù)后怎么更新UI組件了,很簡單竭鞍,完全照著面前所說的將任務(wù)從工作線程拋到主線程的五種方法去做就可以了板惑。
可能又有人問了,那mOtherHandler.post(new Runnable())里的Runnable在哪個線程運行偎快,還是工作線程冯乘,只不過這樣就避開了handlerMessage的步驟而已,跟前面的分析還是一樣的原理的晒夹。

3裆馒、AsyncTask

沒錯姊氓,又是它。具體的使用代碼就不貼上來了喷好,到處都有翔横。但值得一說的是,上面說過HandlerThread只開一條線程梗搅,任務(wù)都被阻塞在一個隊列中禾唁,那么就會使阻塞的任務(wù)延遲了,而AsyncTask開啟線程的方法asyncTask.execute()默認(rèn)是也是開啟一個線程和一個隊列的无切,不過也可以通過asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 0)開啟一個含有5個新線程的線程池荡短,也就是說有個5個隊列了,假如說你執(zhí)行第6個耗時任務(wù)時哆键,除非前面5個都還沒執(zhí)行完掘托,否則任務(wù)是不會阻塞的,這樣就可以大大減少耗時任務(wù)延遲的可能性籍嘹,這也是它的優(yōu)點所在烫映,當(dāng)你想多個耗時任務(wù)并發(fā)的執(zhí)行,那你更應(yīng)該選擇AsyncTask噩峦。

4、IntentService

最后再小小地提一下IntentService抽兆,相信很多人也不陌生识补,它是Service的子類,用法跟Service也差不多辫红,就是實現(xiàn)的方法名字不一樣凭涂,耗時邏輯應(yīng)放在onHandleIntent(Intent intent)的方法體里,它同樣有著退出啟動它的Activity后不會被系統(tǒng)殺死的特點贴妻,而且當(dāng)任務(wù)執(zhí)行完后會自動停止切油,無須手動8去終止它。例如在APP里我們要實現(xiàn)一個下載功能名惩,當(dāng)退出頁面后下載不會被中斷澎胡,那么這時候IntentService就是一個不錯的選擇了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市娩鹉,隨后出現(xiàn)的幾起案子攻谁,更是在濱河造成了極大的恐慌,老刑警劉巖弯予,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戚宦,死亡現(xiàn)場離奇詭異孩灯,居然都是意外死亡搬葬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來槽棍,“玉大人,你說我怎么就攤上這事赠法≌鹗” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵骚灸,是天一觀的道長糟趾。 經(jīng)常有香客問我,道長甚牲,這世上最難降的妖魔是什么义郑? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮丈钙,結(jié)果婚禮上非驮,老公的妹妹穿的比我還像新娘。我一直安慰自己雏赦,他們只是感情好劫笙,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著星岗,像睡著了一般填大。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俏橘,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天允华,我揣著相機與錄音,去河邊找鬼寥掐。 笑死靴寂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的召耘。 我是一名探鬼主播百炬,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼污它!你這毒婦竟也來了剖踊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤衫贬,失蹤者是張志新(化名)和其女友劉穎蜜宪,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祥山,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡圃验,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缝呕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澳窑。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡斧散,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出摊聋,到底是詐尸還是另有隱情鸡捐,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布箍镜,位于F島的核電站,受9級特大地震影響煎源,放射性物質(zhì)發(fā)生泄漏色迂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一手销、第九天 我趴在偏房一處隱蔽的房頂上張望歇僧。 院中可真熱鬧,春花似錦锋拖、人聲如沸诈悍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侥钳。三九已至,卻和暖如春柄错,著一層夾襖步出監(jiān)牢的瞬間舷夺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工鄙陡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人躏啰。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓趁矾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親给僵。 傳聞我的和親對象是個殘疾皇子毫捣,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

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

  • 簡介 1. 線程分類 主線程(UI線程) : 處理和界面相關(guān)的事情. 子線程 : 處理耗時操作. Android中...
    王世軍Steven閱讀 917評論 0 2
  • 消息機制 處理消息的手段--Handler,Looper與MessageQueue =tips:= 子線程無法更新...
    hloong閱讀 848評論 1 2
  • Android 多線程的可以歸納為兩種情況:1帝际、將任務(wù)從工作線程拋到主線程蔓同;2、將任務(wù)從主線程拋到工作線程蹲诀; 一斑粱、...
    秀花123閱讀 1,336評論 1 7
  • AsyncTaskHandlerThreadIntentServiceandroid中的線程池 android 中...
  • 什么是多線程? 線程是cpu調(diào)度的最小單位脯爪。java中则北,一個虛擬機實例對應(yīng)著一個進程矿微,進程中有n個線程,java為...
    某昆閱讀 891評論 0 20