android 進程和線程

來源:互聯網
上載者:User

進程和線程

如果某個應用程式組件是第一次被啟動,且這時應用程式也沒有其他組件在運行,則Android系統會為應用程式建立一個包含單個線程的linux進程。預設情況下,同一個應用程式的所有組件都運行在同一個進程和線程裡(叫做“main”主線程)。如果組件啟動時,已經存在應用程式的進程了(因為應用程式的其它組件已經在運行了),則此組件會在已有的進程和線程中啟動運行。不過,可以指定組件運行在其他進程裡,也可以為任何進程建立額外的線程。

         本文討論進程和線程是如何在Android應用程式中發揮作用的。

 

進程

預設情況下,同一個應用程式內的所有組件都是運行在同一個進程中的,大部分應用程式也不會去改變它。不過,如果需要指定某個特定組件所屬的進程,則可以利用manifest 檔案來達到目的。

manifest檔案中的每種組件元素——<activity>、 <service>、 <receiver>和<provider>——都支援定義android:process屬性,用於指定組件啟動並執行進程。設定此屬性即可實現每個組件在各自的進程中運行,或者某幾個組件共用一個進程而其它組件運行於獨立的進程。設定此屬性也可以讓不同應用程式的組件運行在同一個進程中——實現多個應用程式共用同一個Linux使用者ID、賦予同樣的許可權。

<application>元素也支援android:process屬性,用於指定所有組件的預設進程。

如果記憶體不足,可又有其它為使用者提供更緊急服務的進程需要更多記憶體,Android可能會決定關閉一個進程。在此進程中運行著的應用程式組件也會因此被銷毀。當需要再次工作時,會為這些組件重新建立一個進程。

在決定關閉哪個進程的時候,Android系統會權衡它們相對使用者的重要程度。比如,相對於一個擁有可見activity的進程,更有可能去關閉一個activity已經在螢幕上看不見的進程。也就是說,是否終止一個進程,取決於運行在此進程中組件的狀態。終止進程的判定規則將在後續內容中討論。

 

進程的生命週期

Android系統試圖儘可能長時間地保持應用程式進程,但為了建立或者運行更加重要的進程,總是需要清除過時進程來回收記憶體。為了決定保留或終止哪個進程,根據進程內啟動並執行組件及這些組件的狀態,系統把每個進程都劃入一個“重要性階層”中。重要性最低的進程首先會被清除,然後是下一個最低的,依此類推,這都是回收系統資源所必需的。

重要性階層共有5級,以下列表按照重要程度列出了各類進程(第一類進程是最重要的,將最後一個被終止):

1. 前台進程

使用者當前操作所必須的進程。滿足以下任一條件時,進程被視作處於前台:

o     其中運行著正與使用者互動的Activity(Activity對象的 onResume() 方法已被調用)。

o     其中運行著被正與使用者互動的activity綁定的服務Service。

o     其中運行著“前台”服務Service——服務以startForeground()方式被調用。

o     其中運行著正在執行生命週期回調方法(onCreate()、onStart()或onDestroy())的服務Service。

o     其中運行著正在執行onReceive()方法的BroadcastReceiver。

一般而言,任何時刻前台進程都是為數不多的,只有作為最後的策略——當記憶體不足以維持它們同時運行時——才會被終止。通常,裝置這時候已經到了記憶體分頁狀態(memory paging state)的地步,終止一些前台進程是為了保證使用者介面的及時響應。

 

2. 可見進程

沒有前台組件、但仍會影響使用者在螢幕上所見內容的進程。滿足以下任一條件時,進程被認為是可見的:

o     其中運行著不在前台的Activity,但使用者仍然可見到此activity(onPause()方法被調用了)。比如以下場合就可能發生這種情況:前台activity開啟了一個對話方塊,而之前的activity還允許顯示在後面。

o     其中運行著被可見(或前台)activity綁定的服務Service。

可見進程被認為是非常重要的進程,除非無法維持所有前台進程同時運行了,它們是不會被終止的。

 

3. 服務進程

此進程運行著由startService()方法啟動的服務,它不會升級為上述兩層級。儘管服務進程不直接和使用者所見內容關聯,但他們通常在執行一些使用者關心的操作(比如在背景播放音樂或從網路下載資料)。因此,除非記憶體不足以維持所有前台、可見進程同時運行,系統會保持服務進程的運行。

 

4. 後台進程

包含目前使用者不可見activity(Activity對象的onStop()方法已被調用)的進程。這些進程對使用者體驗沒有直接的影響,系統可能在任意時間終止它們,以回收記憶體供前台進程、可見進程及服務進程使用。通常會有很多後台進程在運行,所以它們被儲存在一個LRU(最近最少使用)列表中,以確保最近被使用者使用的activity最後一個被終止。如果一個activity正確實現了生命週期方法,並儲存了當前的狀態,則終止此類進程不會對使用者體驗產生可見的影響。因為在使用者返回時,activity會恢複所有可見的狀態。關於儲存和恢複狀態的詳細資料,請參閱Activities文檔。

 

5. 空進程

不含任何活動應用程式組件的進程。保留這種進程的唯一目的就是用作緩衝,以改善下次在此進程中運行組件的啟動時間。為了在進程緩衝和核心緩衝間平衡系統整體資源,系統經常會終止這種進程。

 

依據進程中目前活躍組件的重要程度,Android會給進程評估一個儘可能高的層級。例如:如果一個進程中運行著一個服務和一個使用者可見的activity,則此進程會被評定為可見進程,而不是服務進程。

此外,一個進程的層級可能會由於其它進程的依賴而被提高——為其它進程提供服務的進程層級永遠不會低於使用此服務的進程。比如:如果A進程中的content provider為進程B中的用戶端提供服務,或進程A中的服務被進程B中的組件所調用,則A進程至少被視為與進程B同樣重要。

因為運行服務的進程層級是高於後台activity進程的,所以,如果activity需要啟動一個長時間啟動並執行操作,則為其啟動一個服務service會比簡單地建立一個背景工作執行緒更好些——尤其是在此操作時間比activity本身存在時間還要長久的情況下。比如,一個activity要把圖片上傳至Web網站,就應該建立一個服務來執行之,即使使用者離開了此activity,上傳還是會在後台繼續運行。不論activity發生什麼情況,使用服務可以保證操作至少擁有“服務進程”的優先順序。同理,上一篇中的廣播接收器broadcast receiver也是使用服務而非線程來處理耗時任務的。

 

 

線程

應用程式啟動時,系統會為它建立一個名為“main”的主線程。主線程非常重要,因為它負責把事件分發給相應的使用者介面widget——包括螢幕繪圖事件。它也是應用程式與Android UI組件包(來自android.widget和android.view包)進行互動的線程。因此,主線程有時也被叫做UI線程。

系統並不會為每個組件的執行個體都建立單獨的線程。運行於同一個進程中的所有組件都是在UI線程中執行個體化的,對每個組件的系統調用也都是由UI線程分發的。因此,對系統回調進行響應的方法(比如報告使用者操作的onKeyDown()或生命週期回調方法)總是運行在UI線程中。

舉個例子,當使用者觸控螢幕幕上的按鈕時,應用程式的UI線程把觸摸事件分發給widget,widget先把自己置為按下狀態,再發送一個顯示地區已失效(invalidate)的請求到事件隊列中。UI線程從隊列中取出此請求,並通知widget重繪自己。

如果應用程式在與使用者互動的同時需要執行繁重的任務,單線程模式可能會導致運行效能很低下,除非應用程式的執行時機剛好很合適。如果UI線程需要處理每一件事情,那些耗時很長的操作——諸如訪問網路或查詢資料庫等——將會阻塞整個UI(線程)。一旦線程被阻塞,所有事件都不能被分發,包括螢幕繪圖事件。從使用者的角度看來,應用程式看上去像是掛起了。更糟糕的是,如果UI線程被阻塞超過一定時間(目前大約是5秒鐘),使用者就會被提示那個可惡的“應用程式沒有響應”(ANR)對話方塊。如果引起使用者不滿,他可能就會決定退出並刪除這個應用程式。

此外,Andoid的UI組件包並不是安全執行緒的。因此不允許從背景工作執行緒中操作UI——只能從UI線程中操作使用者介面。於是,Andoid的單線程模式必須遵守兩個規則:

1.         不要阻塞UI線程。

2.         不要在UI線程之外訪問Andoid的UI組件包。

 

背景工作執行緒

根據對以上單線程模式的描述,要想保證程式介面的響應能力,關鍵是不能阻塞UI線程。如果操作不能很快完成,應該讓它們在單獨的線程中運行(“後台”或“工作”線程)。

例如:以下響應滑鼠點擊的代碼實現了在單獨線程中下載圖片並在ImageView顯示:

public void onClick(View v) {

    new Thread(new Runnable() {

        public void run() {

            Bitmap b = loadImageFromNetwork("http://example.com/image.png");

            mImageView.setImageBitmap(b);

        }

    }).start();

}

乍看起來,這段代碼似乎能運行得很好,因為建立了一個新的線程來處理訪問網路的操作。可是它違反了單線程模式的第二條規則:不要在UI線程之外訪問Andoid的UI組件包——以上例子在背景工作執行緒裡而不是UI線程裡修改了ImageView。這可能導致不明確、不可預見的後果,要跟蹤這種情況也是很困難很耗時間的。

為瞭解決以上問題,Android提供了幾種途徑來從其它線程中訪問UI線程。下面列出了有助於解決問題的幾種方法:

·       Activity.runOnUiThread(Runnable)

·       View.post(Runnable)

·       View.postDelayed(Runnable, long)

 

比如說,可以使用View.post(Runnable)方法來修正上面的代碼:

public void onClick(View v) {

    new Thread(new Runnable() {

        public void run() {

            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");

            mImageView.post(new Runnable() {

                public void run() {

                    mImageView.setImageBitmap(bitmap);

                }

            });

        }

    }).start();

}

以上代碼的執行現在是安全執行緒的了:網路相關的操作在單獨的線程裡完成,而ImageView是在UI線程裡操縱的。

不過,隨著操作變得越來越複雜,這類代碼也會變得很複雜很難維護。為了用背景工作執行緒完成更加複雜的互動處理,可以考慮在背景工作執行緒中用Handler來處理UI線程分發過來的訊息。當然,最好的解決方案也許就是繼承使用非同步任務類AsyncTask,此類簡化了一些背景工作執行緒和UI互動的操作。

 

使用非同步任務

非同步任務AsyncTask 允許以非同步方式對使用者介面進行操作。它先阻塞背景工作執行緒,再在UI線程中呈現結果,在此過程中不需要對線程和handler進行人工幹預。

要使用非同步任務,必須繼承AsyncTask類並實現doInBackground()回調方法,該對象將運行於一個後台線程池中。要更新UI時,須實現onPostExecute()方法來分發doInBackground()返回的結果,由於此方法運行在UI線程中,所以就能安全地更新UI了。然後就可以在UI線程中調用execute()來執行任務了。

例如,可以利用AsyncTask來實現上面的那個例子:

public void onClick(View v) {

    new DownloadImageTask().execute("http://example.com/image.png");

}

 

private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {

    /** The system calls this to perform work in a worker thread and

      * delivers it the parameters given to AsyncTask.execute() */

    protected Bitmap doInBackground(String... urls) {

        return loadImageFromNetwork(urls[]);

    }

    

    /** The system calls this to perform work in the UI thread and delivers

      * the result from doInBackground() */

    protected void onPostExecute(Bitmap result) {

        mImageView.setImageBitmap(result);

    }

}

現在UI是安全的,代碼也得到簡化,因為任務分解成了背景工作執行緒內完成的部分和UI線程內完成的部分。

要全面理解這個類的使用,須閱讀AsyncTask的參考文檔。以下是關於其工作方式的概述:

·       可以用generics來指定參數、進度值和任務最終值的類型。

·       背景工作執行緒中的doInBackground()方法會自動執行。

·       onPreExecute()、onPostExecute()和onProgressUpdate()方法都在UI線程中調用。

·       doInBackground()的傳回值會傳給onPostExecute()。

·       在doInBackground()內的任何時刻,都可以調用publishProgress()來執行UI線程中的onProgressUpdate()。

·       可以在任何時刻、任何線程內取消任務。

注意:在使用背景工作執行緒時,可能遇到的另一個問題是由於回合組態的改變(比如使用者改變了螢幕方向)導致activity意外重啟,這可能會銷毀該背景工作執行緒。要瞭解如何在這種情況下維持任務執行、以及如何在activity被銷毀時正確地取消任務,請參見Shelves常式的原始碼。

 

安全執行緒的方法

在某些場合,方法可能會從不止一個線程中被調用,因此這些方法必須是寫成安全執行緒的。

對於能被遠程調用的方法——比如綁定服務(bound service)中的方法,這是理所當然的。如果對IBinder所實現方法的調用發起於IBinder所在進程的內部,那麼這個方法是執行在調用者的線程中的。但是,如果調用發起於其他進程,那麼這個方法將運行於線程池中選出的某個線程中(而不是運行於進程的UI線程中),該線程池由系統維護且位於IBinder所在的進程中。例如,即使一個服務的onBind()方法是從服務所在進程的UI線程中調用的,實現了onBind()的方法對象(比如,實現了RPC方法的一個子類)仍會從線程池中的線程被調用。因為一個服務可以有不止一個用戶端,所以同時可以有多個線程池與同一個IBinder方法相關聯。因此IBinder方法必須實現為安全執行緒的。

類似地,內容提供者(content provider)也能接收來自其它進程的資料請求。儘管ContentResolver類、ContentProvider類隱藏了進程間通訊管理的細節,ContentProvider中響應請求的方法——query()、insert()、delete()、update()和getType()方法——是從ContentProvider所在進程的線程池中調用的,而不是進程的UI線程。因為這些方法可能會從很多線程同時調用,它們也必須實現為安全執行緒的。

 

 

進程間通訊

Android利用遠端程序呼叫(remote procedure call,RPC)提供了一種處理序間通訊(IPC)機制,通過這種機制,被activity或其他應用程式組件調用的方法將(在其他進程中)被遠程執行,而所有的結果將被返回給調用者。這就要求把方法調用及其資料分解到作業系統可以理解的程度,並將其從本地的進程和地址空間傳輸至遠端進程和地址空間,然後在遠程進程中重新組裝並執行這個調用。執行後的傳回值將被反向傳輸回來。Android提供了執行IPC事務所需的全部代碼,因此只要把注意力放在定義和實現RPC編程介面上即可。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.