我們都知道,在操作系統中進程是OS分配資源的最小單位,而線程是執行任務的最小單位。一個進程可以擁有多個線程執行任務,這些線程可以共享該進程分配到的資源。當我們的app啟動運行后,在該app沒有其他組件正在運行的前提下,Android系統會啟動一個新Linux進程來運行app,這個進程只包含了一個線程在運行。在默認情況下,app的組件都運行在該進程中,最初就包含的這個線程也被稱為主線程或者UI線程。如果我們啟動該app的時候,系統中已經有一個進程在運行該app的組件,那么該app也會在該進程中運行。當然,我們也可以讓app中不同的組件運行在不同的進程中,也可以在任意進程中新開線程執行任務。
進程
前面提到過,在默認情況下同一app的所有組件都是運行在同一進程中的,而且大多數app並不需要去更改這個設定。但如果我們真的需要指定進程來運行特定組件,那么可以在manifest文件中設置。我們在manifest文件中定義了各個組件,例如activity,service,receiver,provider等等,我們可以設置process屬性來指定一個新線程來運行該組件。通過設置process屬性,我們可以設置每個組件都運行在不同的進程中,也可以指定幾個組件運行在同一個進程中。我們甚至可以設置不同來自app的組件運行在同一個進程中(通過指定相同的process屬性並共享相同的user ID)。
之前的文章中提到,Android系統啟動后會載入通用的framework的代碼與資源之后,啟動一個Zygote進程。為了啟動一個新的程序進程,系統會fork Zygote進程生成一個新的進程,然后在新的進程中加載並運行應用程序的代碼。這就使得大多數的RAM pages被用來分配給framework的代碼,同時促使RAM資源能夠在應用的所有進程之間進行共享。
Android系統在內存不足的情況下會殺死一些進程來滿足那些直接和用戶交互的進程,在這些被殺死的進程中運行的組件也會被注銷掉。當這些組件重新運行時, 才會啟動該線程。那么,系統將會如何決定要殺死哪個進程呢?Android系統需要根據進程與用戶的相關重要性來判斷的。例如,與那些正在顯示activity的進程相比,系統更傾向於殺死那些不再顯示的activity所在的進程。
進程生命周期
Android系統會盡可能長時間的維持一個進程的運行,但是最終會回收舊進程的內存空間提供給新的進程或是更重要的進程使用。Android系統采用了基於組件運行的進程以及組件狀態的“重要性層級”的策略,根據重要性逐層清除進程。
重要性層級從高到低共分為5層:
1、前台進程foreground process
當前正在與用戶交互的進程。如果一個進程P滿足如下任意一個條件,則進程P被稱為前台進程:
- 當前正在與用戶交互(調用過resume方法)的activity在進程P中運行
- 某個service與當前正在與用戶交互的activity相互綁定,該service運行於進程P中
- 某個service調用了startForeground()方法,該service運行在進程P中
- 某個service正在執行某個生命周期的回調方法,該service運行在進程P中
- 某個broadcastReceiver正在執行它的onReceive函數,該broadcastReceiver運行在進程P中
通常情況下,某時刻系統只會有很少一部分前台進程存在。它們只會在內存非常低的情況下才會被殺死,在這種情況下,設備達到了“memory paging state”狀態,只有殺死一些前台進程才能保證系統的快速響應。
2、可見進程Visable process
沒有任何前台組件在此進程中運行,但是仍然可以影響到用戶所看到的屏幕。如果進程P滿足以下任意一個條件,則進程P被稱為可見進程:
- 某個activity並不運行於前台,但仍能被用戶所見(調用了pause方法),activity運行於進程P中。例如某activity啟動了一個dialog,仍然可以看到該activity。
- 如果某個service與visible activity或foreground activity綁定,該service運行於進程P中。
可見進程相對而言比較重要,但在某些情況下為了保證前台進程的運行,系統還是會殺死這些可見進程的。
3、服務進程service process
如果一個service通過startservice方法啟動,並且不屬於以上兩種更高級別的情況,那么運行該service的進程被稱為service process。盡管該service並不與任何能被用戶看到的組件綁定,但是它們做的工作是用戶關心的,例如音樂播放,文件下載等等。
4、后台進程background process
某個當前不可見的activity(回調了onStop方法)運行於該線程。此類線程無法直接影響到用戶體驗,系統可能隨時殺死此類進程回收內存供以上三種進程使用。通常情況下,系統中有多個后台進程在運行,所以它們被存於一個LRU列表中,從而最不常被用戶用到的進程會先被殺死。如果一個activity正常回調了它的生命周期的函數並存儲了相應的狀態數據,那么殺死該后台進程是不會影響到用戶體驗的。因為當用戶試圖返回到該activity時,系統會恢復該activity所有的狀態。
5、空進程 empty process
該進程中沒有運行任何組件,保持此類進程存在的唯一原因就是等待任務。一旦有組件需要運行,則可以縮短進程啟動時間。所以系統往往會殺死這些進程用來平衡進程緩存和底層內核緩存之間的系統資源。
Android系統中,一個進程的等級是可以動態提升的,因為其他的進程可能會依賴於該進程。某個進程為其他進程提供服務,那么該進程的等級一定不會低於它所服務的進程。例如,某content provider 運行於進程A中,它為處於進程B中的客戶端B提供服務;或者如果處於進程A的service綁定於位於進程B中的組件,那么A進程的重要等級只會高於或等於進程B。
由於一個運行service的進程等級要高於那些運行處於后台activity的進程,如果我們需要有長時間執行的操作,那么從一個activity中啟動一個service來完成這些操作就比在activity中新開子線程來完成這些操作效果要好(特別是這些子線程的持續時間要比activity長的情況下)。例如,一個activity想要上傳一張圖片給服務器,那么應當開啟一個service在后台來完成上傳操作,即使用戶離開了當前activity,這些操作也能夠在后台完成。使用service可以保證這些操作至少具有service優先級,無論當前activity的狀態是否改變。這也是為什么broadcast receiver應當使用service而不是簡單的把耗時操作放在子線程中的原因。
線程
當應用程序啟動后,系統將會創建一個主線程來運行應用程序。主線程非常重要,它負責為適當的用戶控件分發任務和事件,包括繪制任務等等。同時,主線程也負責UI組件和應用程序的交互,所以我們也稱主線程為UI線程。
系統並不會為每個組件單獨開啟一個線程來運行,所有的組件都會在主線程中初始化並運行運行在同一個進程中,系統通過主線程來調用每個組件。所以,系統回調方法(例如onKeyDown,生命周期回調方法等)通常運行於主線程。
例如,當用戶點擊屏幕上的按鈕,UI線程會將點擊事件分發給控件。控件就會設置自身的按下狀態,並將重繪請求添加到事件請求隊列。UI線程從事件隊列中取出該重繪請求后,通知該控件重繪。
當用戶和app交互頻繁時,單線程的模式可能會導致響應速度慢,用戶體驗不盡人意。如果在主線程中進行網絡或數據庫請求等耗時操作,則會導致線程阻塞,主線程將無法調度分發事件和任務。當超過5s的阻塞會使系統彈出ANR窗口。另外,UI控件都不是線程安全的,所以系統規定只能在UI線程中修改控件。我們需要遵循兩個規則:
- 不要使UI線程阻塞
- 不要在UI線程之外修改控件
worker線程
上面討論了只有UI線程工作的情況。為了提高應用程序UI的響應速度,獲得更好的用戶體驗,我們需要把耗時操作放在子線程中來完成。但是我們需要注意的是,不要在子線程中操作UI控件。我們通常使用Android的Handler機制來解決線程間通信的問題,詳細請參看之前的文章Android線程間異步通信機制源碼分析。同時,Android也提供了async task來完成異步任務。
異步任務ASYNC TASK
async task在子線程中執行耗時任務,然后將結果返回給UI線程,無需自己手動創建handler。關於async task的使用就不在這里介紹了,在使用async task的過程中,我們需要注意的是多線程問題。由於運行配置的問題(例如屏幕橫豎方向改變),會導致子線程任務未經過我們允許就重新啟動執行。
線程安全方法
多數情況下,我們的方法有可能被多個線程所調用,所以我們必須考慮到線程安全的問題。特別是對於那些可以被遠程調用的方法更是如此,例如,綁定service的方法。當我們試圖調用在IBinder中實現的方法,如果調用者和IBinder處於同一個進程,那么方法將會在調用者所在線程中執行。如果調用者與IBinder並不處於同一個進程中,那么系統從所維護的線程池中取出一個線程來執行該方法,該線程池與IBinder運行在同一個進程中(並非在UI線程中執行)。舉個栗子,盡管service的進程的UI線程將會調用service的onBind方法,然而在onBind方法所返回的IBinder對象中實現的那些方法就會被線程池中線程執行。因為一個service可以由多個客戶端訪問,線程池中的多個線程可以在同一時刻調用同一方法。所以IBinder對象中實現的方法需要是線程安全的。
類似的,一個ContentProvider可以接收到來自不同進程的數據請求,雖然CP和CR類中隱藏了進程間通信管理的細節,但是CP中對應的查詢,刪除,修改,插入等請求方法將會被交給CP所在進程的線程池中線程來執行。這些方法可能在同一時刻被多個進程所調用,所以這些方法必須是線程安全的。
進程間通信
Android系統提供了遠程調用RPC機制來完成進程通信IPC,通過RPC機制,應用程序中的組件(例如activity)作為調用者在本地調用某個方法,該方法在遠程(另外一個進程中)執行,然后將結果返回給調用者。這就需要將所調用方法和它的數據解析為操作系統可以理解的程度,然后從本地進程和地址空間傳遞給遠程的進程和地址空間后,再進行重組和執行。