? ? ? ? ? ? 記得在我上學的時候,iPHone是屬于少數人才擁有的稀有物品,Android甚至還沒面世,那個時候全球的手機市場是有諾基亞統治著的,當時Symbian操作系統做的特別出色,因為比起一般手機,它可以支持后臺功能。那個時候能夠一邊打著電話、聽音樂、一般在后臺掛著qq是件非常酷的事情。所以我也曾經單純的認為,支持后臺的手機就是智能手機。
? ? ? ? ? ? 而如今,Symbian早已風光不再,Android和ios占據了大部分智能市場份額,Windows Phone也占據了一小部分,目前已是三分天下的局面。在這三大操作系統中iOs和windows Phone一開始是不支持后臺的,后來逐漸意思到后臺這個功能的重要性,才加入了后臺的功能。而Android則是沿用了Symbian的老習慣,從一開始就支持后臺功能,這使得應用程序即使在關閉的情況下仍然可以在后臺繼續運行。不管怎么說,后臺功能屬于四大組件之一,其重要程序不言而喻。
10.1 服務是什么?
? ? ? ? ? ? ?服務(Service)是Android中實現程序后臺運行的解決方案,他非常適合去執行那些不需要和用戶交互而且還需要長期運行的任務。服務的運行不依賴于任何用戶界面,即使程序被切換到后臺,或者用戶打開了另一個應用程序,服務仍然能夠保持正常運行。
? ? ? ? ? ? ?不過需要注意的是,服務并不是運行在一個獨立的進程當中,而是依賴于創建服務時所在的應程序進程。當某個應用程序進程被殺掉時,所有依賴于該進程的服務也會停止運行。
? ? ? ? ? ? ?另外,也不要被服務的后臺概念所迷惑,實際上服務并不會自動開啟現程,所有的代碼都是默認運行在主線程當中的。也就是說,我們需要在服務的內部手動創建子線程,并在這里執行具體的任務,否則就會有可能出現主線程被阻塞的情況。那么本章的第一堂課,我們就先來學習一下關于Android多線程編程的知識。
10.2 Android多線程編程
? ? ? ? ? ?熟悉Java的你,對多線程編程一定不會陌生吧。當我們需要執行一些耗時操作,比如說法起一條網絡請求時,考慮到網速等其他原因,服務器未必會立刻響應我們的請求,如果不將這類操作放在子線程里去運行,就會導致主線程被阻塞,從而引向用戶對軟件的正常使用,那么就讓我們從縣城的基本用法開始學習吧!
10.2.1 ?線程的基本用法
? ? ? ? ? Android多線程變成其實并不比Java多線程編程特殊,基本都是使用相同的語法。比如說,定義一個需要新建一個類繼承自Thread,然后重寫父類的run()方法,并在里面編寫耗時邏輯即可,如下所示:
? ? ? ? ? ? 那么該如何啟動這個線程啦?其實也很簡單,只需要New出MyThread的實例,然后調用它的start()方法,這樣run方法中的代碼就會在子線程當中運行了,如下所示:
當然,使用繼承的方式耦合性有點高,更多的時候我們都會選擇實現Runnable接口的方式來定義一個線程,如下所示:
? ? ? ? ? ?如果使用了這種寫法,啟動線程的方法也需要進行相應的改變
? ? ? ? ? 可以看到,Thread的構造函數接受一個Runable參數,而我們new出的MyThread正是一個實現了Runable接口的對象,所以可以直接將它傳入到Thead的構造函數里。接著調用Thread的start()方法,run()方法中的代碼就會在子線程中運行了。
當然你不想專門在定義一個類趨勢線Runnable接口,也可以使用匿名類的方式,這種寫法更為常見,如圖所示:
? ? ? ? ? ? ?以上幾種線程的使用方式相信你都不會感到陌生,因為在Java中創建和啟動線程也是使用同樣的方式。了解了線程的基本用法后,下面我們來看一下Android多線程與Java多線程編程有什么不同的地方。
10.2.2 在子線程中更新UI
? ? ? ? ? ? ? 和許多其他的GUI庫一樣,Android的UI也是線程不安全的。也就是說,如果想要更新應用程序的UI元素,則必須在主線程里進行,否則就會出現異常。
? ? ? ? ? ? 眼見為實,讓我們通過一個具體的例子來驗證一下吧。新建一個AndroidThreadTest項目,然后修改activity_main.xml中的代碼,如此所示:
? ? ? ? ? 布局文件中定義了兩個控件,TextView用于在屏幕的鄭重顯示一個Hello World 字符串,Button用于改變吧TextView中顯示的內容,我們希望再點擊Button后可以把TextView中顯示的字符串改成Nice to meet you。
? ? ? ? ? ?接下來修改MainActivity中的代碼。如下所示:
? ? ? ? ? 可以看到,我們在Change Text按鈕的點擊事件里面開啟了一個子線程,然后在子線程中調用TextView的setText()方法將現實的字符串改成Nice to meet you。代碼的邏輯非常簡單,只不過我們是在子線程中更新UI的。現在運行以下程序,并點擊Change Text按鈕,你會發現程序茍然崩潰了,如圖10.1所示:
然后觀察logCat中的的錯誤日志,可以看到由于在子線程中更新UI所導致的如圖10.2所示:
由此證實了,Android確實是不允許在子線程中進行UI操作的問題。本小節中我們先來學習一下一步消息處理的使用方法,下一小節中再去分析它的原理。
修改MainActivity中的代碼,如下所示:
? ? ? ? ? 這里我們先是定義了一個整形常量UPDATE_TEXT,用于表示更新TextView這個動作。然后新增一個Handle對象,并重寫了父類的handleMessage()方法,在這里對具體的Message進行了處理。如果發現Message的what字段的值等于UPDATE_TEXT,就將TextView現實的內容改成Nice to meet you。
? ? ? ? ? 下面再來看一下Change Text按鈕的點擊事件中的代碼。可以看到,這次我們并沒有在子線程里直接進行UI操作,而是創建了Message(android.os.Message)對象,并將它的what字段的值指定為UPDATE_TEXT,然后調用Handle的endMessage()方法將這條Message放棄愛送出去。很快,Handle救護收到這條Message,并在handleMessage()方法中對它進行處理。注意此時handleMessage()方法中的代碼就是在主線程運行的了,所以我們可以放心的在這里進行UI操作。接下來對Message攜帶的what字段的值進行判斷,如果等去UPDATE_TEXT,就像TextView顯示的內容改成Nice to meet you。如圖10.3所示:
? ? ? ? ? ?這樣你就已經掌握了Android異步消息處理的基本用法,使用這種機制就可以出色的解決掉在子線程中更新UI的問題。不過恐怕你對他的工作原理還不是很清楚,下面我們就來分析一下Android異步消息處理機制到底是如何工作的。
10.2.3 ? ? ? ?解析 消息異步處理機制
? ? ? ? ? ? ? ? ?Android中的異步消息處理主要是由4個部分組成:Message、Handle、MessageQueue和Looper。其中Message和Handle在上一小節中我們已經接觸過了,而MessageQueue和Looper對于你來說還是全新的概念,下面我就來對這4部分進行以下簡要的介紹:
1 Message
? ? ? ? ? ? ? ?Message是在線程之間傳遞的消息,它可以在內部攜帶少量的信息,用于在不同線程之間交換數據。上一小節中我們使用到了Message的what字段,除此之外還可以使用argl和argl2字段來攜帶一些整型數據,使用Object對象。
2 ?Handler
? ? ? ? ? ? Handler顧名思義也就是處理者的意思,他主要是用于發送和處理消息的。發送消息一般是使用Handle的sendMessage()方法,而發出的消息經過一系列的輾轉處理后,最終會傳遞到Handler的handleMessage()方法中。
3 MessageQueue
? ? ? ? ? ?MessageQueue是消息隊列的意思,他主要用于存放所有通過Handle發送的消息。這部分消息會一直存在于消息隊列中,等待被處理。每個線程中只會有一個MessageQueue對象。
4 Looper
? ? ? ? ? ?Looper是每個線程中的MessageQueue的管家,調用Looper的loop()方法后,就會進入到一個無限循環當中,然后每當發現messageQueue中存在一條消息,就會將它取出,并傳遞到Hnadle的handleMessage方法中、每個線程中也只會有一個Looper對象。
? ? ? ? ? 了解了Message、Handler、MessageQueue、以及Looper的基本蓋簾后,我們再來把異步消息 處理的整個流程梳理一遍。首先需要在主線程中創建一個Handle對象,并重寫handleMesage()方法。然后當子線程中需要進行UI操作時,就創建一個Message對象,并通過Handle將這條消息發送出去。之后這條消息會被添加到MessageQueue的隊列中等待被處理,而Looper則會一直嘗試從MessageQueue中取出待處理的消息,最后分發回Hnadle的HnadleMessage()方法中。由于Hnadle是在主線程中創建的,所以此時handleMessage()中的代碼也會在主線程中運行,于是我們在這里就可以安心的進行UI操作了。整個異步消息處理的流程示意圖如圖10.4所示:
? ? ? ? ? ?一條Message經過這樣一個流程的輾轉調用之后,也就從子線程進入到了主線程,從不能更新UI變成了可以更新UI,整個異步消息處理的核心思想就是如此。
? ? ? ? ? ?而我們在9.2.1小節中用到的runOnUiThread()方法其實就是一個異步消息處理機制的接口封裝,他雖然上看起來用法更簡單,但其實背后的實現原理和圖10.4中描述的是一模一樣的。
10.2.4 ? ? ? 使用AsyncTack
? ? ? ? ? ?不過為了更方便我們在子線程中對UI進行操作,Android還提供了另一些好用的工具,比如AsyncTask。借助AsyncTask,即使你對異步消息處理機制完全不了解,也可以十分簡單的從子線程切換到主線程。當然AsyncTask背后的實現原理也是基于異步消息處理機制的,只是Android幫我們做了很好地封裝而已。
? ? ? ? ? ?首先看一下AsyncTask的基本用法,由于AsyncTask是一個抽象類,所以如果我們想使用它,就必須要創建一個類去繼承他。在繼承使我們可以為AsyncTask類指定3個泛型慘呼是,這三個參數的用途如下。
? ? ? Params: 在執行AsyncTask時需要傳入的參數,可用在后臺任務中使用。
? ? ? Progress: 后臺任務執行時,如果需要在界面上顯示當前的進度,則使用這里指定的泛型作為進度單位。
? ? ? Result: 當任務執行完畢后,如果需要對結果進行返回,則使用這里指定的泛型作為返回值類型。
? ? ? 因此,一個最簡單的自定義AsyncTask就可以寫成如下方式:
? ? ? ? ? ? ?這里我們把AsyncTask的第一個泛型參數指定為Void,表示在執行AsyncTask的時候不需要傳入參數給后臺任務,。第二個泛型參數指定為Integer,表示使用整形數據作為進度顯示單位。第三個泛型參數指定為Boolean,則表示使用布爾型數據來反饋執行結果。
? ? ? ? ? ? ?當然,目前我們自定義的DownloadTask還是一個空任務,并不能進行任何實際的操作,我們還需要去重寫AsyncTask中的幾個方法才能完成任務的定制。經常需要去重寫的方法還有以下4個。
1 onPreExcute()
? ? ? ? ? ? 這個方法會在后臺開始執行任務之前調用,用于進行一些界面初始化操作,比如顯示一個進度條對話框等
2 doInBackGround(Params)
? ? ? ? ? ?這個方法中的所有代碼都會在子線程中運行,我們應該在這里去處理所有的耗時任務。任務一旦完成就可以通過return語句來將任務的執行結果返回,如果AsyncTask的第三個參數指定的是Void,就可以不返回任務執行結果。注意在這個方法里面是不可以進行UI操作的,如果需要更新UI元素,比如說返回當前任務的執行進度,可以調用publisProgress(Progress...)方法來完成。
3 onProgressUpdata(Progress....)
? ? ? ? ? 當在后臺任務中調用publisProgress(Progress...)方法后,onProgressUpdata(Progress....)就會很快被調用,該方法中攜帶的參數就是后臺任務中傳遞過來的。在這個方法中可以對UI進行操作,利用參數中的數值就可以對界面元素進行相應的更新。
4. OnPostExecute(Result)
? ? ? ? ? 當后臺執行完畢通過return語句進行返回時,這個方法就很快會被調用。反悔的數據作為參數傳遞到此方法中,可以利用反悔的數據進行一些UI操作,比如說提醒任務執行的結果,以及關閉掉進度條對話框等。
? ? ? ? ? 因此,一個比較完整的自定義項目AsyncTask就可以寫成如下方式: