Android中Intent概述及使用

孫群發表於2015-09-13

Android中的Intent是一個非常重要且常用的類,可以用來在一個元件中啟動App中的另一個元件或者是啟動另一個App的元件,這裡所說的元件指的是Activity、Service以及Broadcast。


Intent的用途

Intent主要有以下幾種重要用途:
1. 啟動Activity:可以將Intent物件傳遞給startActivity()方法或startActivityForResult()方法以啟動一個Activity,該Intent物件包含了要啟動的Activity的資訊及其他必要的資料。
2. 啟動Service:可以將Intent物件傳遞給startService()方法或bindService()方法以啟動一個Service,該Intent物件包含了要啟動的Service的資訊及其他必要的資料。關於使用startService()方法啟動Service,可以參見博文《Android中startService的使用及Service生命週期》。關於使用bindService()方法啟動Service,可以參見博文《Android中bindService的使用及Service生命週期》
3. 傳送廣播:廣播是一種所有App都可以接收的資訊。Android系統會發布各種型別的廣播,比如釋出開機廣播或手機充電廣播等。我們也可以給其他的App傳送廣播,可以將Intent物件傳遞給sendBroadcast()方法或sendOrderedBroadcast()方法或sendStickyBroadcast()方法以傳送自定義廣播。


Intent的型別

有兩種型別的Intent:explicit(顯式)的和implict(隱式)的。

顯式的Intent:如果Intent中明確包含了要啟動的元件的完整類名(包名及類名),那麼這個Intent就是explict的,即顯式的。使用顯式Intent最典型的情形是在你自己的App中啟動一個元件,因為你自己肯定知道自己的要啟動的元件的類名。比如,為了響應使用者操作通過顯式的Intent在你的App中啟動一個Activity或啟動一個Service下載檔案。

隱式的Intent:如果Intent沒有包含要啟動的元件的完整類名,那麼這個Intent就是implict的,即隱式的。雖然隱式的Intent沒有指定要啟動的元件的類名,但是一般情況下,隱式的Intent都要指定需要執行的action。一般,隱式的Intent只用在當我們想在自己的App中通過Intent啟動另一個App的元件的時候,讓另一個App的元件接收並處理該Intent。例如,你想在地圖上給使用者顯示一個位置,但是你的App又不支援地圖展示,這時候你可以將位置資訊放入到一個Intent中,然後給它指定相應的action,通過這樣隱式的Intent請求其他的地圖型的App(例如Google Map、百度地圖等)來在地圖中展示一個指定的位置。隱式的Intent也體現了Android的一種設計哲學:我自己的App無需包羅永珍所有功能,可以通過與其他App組合起來,給使用者提供很好的使用者體驗。而連線自己的App與其他App的紐帶就是隱式Intent。

當建立了一個顯式Intent去啟動Activity或Service的時候,系統會立即啟動Intent中所指定的元件。

當建立了一個隱式Intent去使用的時候,Android系統會將該隱式Intent所包含的資訊與裝置上其他所有App中manifest檔案中註冊的元件的Intent Filters進行對比過濾,從中找出滿足能夠接收處理該隱式Intent的App和對應的元件。如果有多個App中的某個元件都符合條件,那麼Android會彈出一個對話方塊讓使用者選擇需要啟動哪個App。

Intent Filter,即Intent過濾器,一個元件可以包含0個或多個Intent Filter。Intent Filter是寫在App的manifest檔案中的,其通過設定action或uri資料型別等指明瞭元件能夠處理接收的Intent的型別。如果你給你的Activity設定了Intent Filter,那麼這就使得其他的App有可能通過隱式Intent啟動你的這個Activity。反之,如果你的Activity不包含任何Intent Filter,那麼該Activity只能通過顯式Intent啟動,由於我們一般不會暴露出我們元件的完整類名,所以這種情況下,其他的App基本就不可能通過Intent啟動我們的Activity了(因為他們不知道該Activity的完整類名),只能由我們自己的App通過顯式Intent啟動。

需要注意的是,為了確保App的安全性,我們應該總是使用顯式Intent去啟動Service並且不要為該Service設定任何的Intent Filter。通過隱式的Intent啟動Service是有風險的,因為你不確定最終哪個App中的哪個Service會啟動起來以響應你的隱式Intent,更悲催的是,由於Service沒有UI的在後臺執行,所以使用者也不知道哪個Service執行了。從Android 5.0 (API level 21)開始,用隱式Intent呼叫bindService()方法,Android會丟擲異常,但是也有相應技巧,將一個隱式的Intent轉換為顯式的Intent,然後用顯式的Intent去呼叫bindService()方法就沒有問題了,具體解決辦法可以參見博文《Android中通過Messenger與Service實現程式間雙向通訊》中最後的“注意事項”部分,裡面有相關程式碼的解決方案。


Intent的組成

Android可以根據Intent所攜帶的資訊去查詢要啟動的元件,Intent還攜帶了一些資料資訊以便要啟動的元件根據Intent中的這些資料做相應的處理。

Intent由6部分資訊組成:Component Name、Action、Data、Category、Extras、Flags。根據資訊的作用用於,又可分為三類:
a. Component Name、Action、Data、Category為一類,這4中資訊決定了Android會啟動哪個元件,其中Component Name用於在顯式Intent中使用,Action、Data、Category、Extras、Flags用於在隱式Intent中使用。
b. Extras為一類,裡面包含了具體的用於元件實際處理的資料資訊。
c. Flags為一類,其是Intent的後設資料,決定了Android對其操作的一些行為,下面會介紹。

Component name
要啟動的元件的名稱。如果你想使用顯式的Intent,那麼你就必須指定該引數,一旦設定了component name,Android會直接將Intent傳遞給元件名所指定的元件去啟動它。如果沒有設定component name,那麼該Intent就是隱式的,Android系統會根據其他的Intent的資訊(例如下面要介紹到的action、data、category等)做一些比較判斷決定最終要啟動哪個元件。所以,如果你啟動一個你自己App中的元件,你應該通過指定component name通過顯式Intent去啟動它(因為你知道該元件的完整類名)。

需要注意的是,當啟動Service的時候,你應該總是指定Component Name。否則,你不確定最終哪個App的哪個元件被啟動了,並且使用者也看不到哪個Service啟動了。

component name在Intent中對應的field是ComponentName物件,你可以通過要啟動的元件的完整類名(包括應用的包名)指定該值,例如com.example.ExampleActivity。你可以通過Intent的setComponent()方法、setClass()方法、setClassName()方法或Intent的建構函式指定component name。

Action
是表示了要執行操作的字串,比如檢視或選擇,其對應著Intent Filter中的action標籤<action />

你可以指定你獨有的action以便於你的App中的Intent的使用或其他App中通過Intent呼叫你的App中的元件。Intent類和Android中其他framework級別的一些類也提供了許多已經定義好的具有一定通用意義的action。以下是一些用於啟動Activity的常見的action:
Intent.ACTION_VIEW 其值為 “android.intent.action.VIEW”,當你有一些資訊想讓通過其他Activity展示給使用者的時候,你就可以將Intent的action指定為ACTION_VIEW,比如在一個圖片應用中檢視一張圖片,或者在一個地圖應用中展現一個位置。
Intent.ACTION_SEND 其值為”android.intent.action.SEND”,該action常用來做“分享”使用,當你有一些資料想通過其他的App(例如QQ、微信、百度雲等)分享出去的時候,就可以使用此action構建Intent物件,並將其傳遞給startActivity()方法,由於手機上可能有多個App的Activity均支援ACTION_SEND這一action,所以很有可能會出現如下的圖片所示的情形讓使用者具體選擇要通過哪個App分享你的資料:

這裡寫圖片描述

可以通過檢視Intent類瞭解更多的Intent預定義的一些常見的action。Android中framework級別的一些類也定義了一些action,例如Settings中定義了一些action用以分別開啟系統中“設定”這個應用的不同介面以完成對指定配置(如WLAN設定、語言設定等)。

你可以通過呼叫intent物件的setAction()方法或在Intent的建構函式中指定intent的action。

如果你定義了你自己的action,請務必將你的App的包名作為該action的字首,這是一種良好的程式設計習慣,避免造成混淆,例如:

static final String ACTION_TIMETRAVEL = "com.example.action.TIMETRAVEL";

Data
此處所說的Intent中的data指的是Uri物件和資料的MIME型別,其對應著Intent Filter中的data標籤<data />
一個完整的Uri由scheme、host、port、path組成,格式是<scheme>://<host>:<port>/<path>,例如content://com.example.project:200/folder/subfolder/etc。Uri就像一個資料連結,元件可以根據此Uri獲得最終的資料來源。通常將Uri和action結合使用,比如我們將action設定為ACTION_VIEW,我們應該提供將要被編輯修改的文件的Uri。

當建立了一個Intent物件的時候,除了指定Uri之外,指定資料的MIME型別也很重要。例如,一個Activity能夠顯示圖片,但是不能夠播放視訊,顯示圖片的Uri和播放視訊的Uri可能很類似,為了不讓Android誤將一個含有視訊Uri的Intent物件傳遞給一個只能顯示圖片的Activity,我們需要在該Activity的Intent Filter中指定MIME型別為圖片(例如<data android:mimeType="image/*" ... />)並且還要給Intent物件設定對應的圖片型別的MIME,這樣Android就會基於Uri和MIME型別將Intent傳遞給符合條件的元件。然後有個特例,如果Uri使用的是content:協議,那麼這就說明Uri所提供的資料將來自於本地裝置,即資料由ContentProvider提供,這種情況下Android會根據Uri自動推斷出MIME型別,此種情況我們無需再自己指定MIME型別。

如果只設定資料的Uri,需要呼叫Intent物件的setData()方法;如果只設定資料的MIME型別,需要呼叫Intent物件的setType()方法;如果要同時設定資料的Uri和MIME型別,需要呼叫Intent物件的setDataAndType()方法。

需要注意的是,如果你想要同時設定資料的Uri和MIME型別,不要先後呼叫Intent物件的setData()方法和setType()方法,因為setData()方法和setType()是互斥的,即如果呼叫了setData()方法,會將Intent中已經通過setType()方法設定的MIME型別重置為空。如果呼叫了setType()方法,會將Intent中已經通過setData()方法設定的Uri重置為空。所以在需要同時設定資料的Uri和MIME型別的時候,一定要呼叫Intent物件的setDataAndType()方法,而不是分別呼叫setData()方法和setType()方法。

Category
category包含了關於元件如何處理Intent的一些其他資訊,雖然可以在Intent中加入任意數量的category,但是大多數的Intent其實不需要category。
以下是一些常見的category:

CATEGORY_BROWSABLE 目標元件會允許自己通過一個連結被一個Web瀏覽器啟動,該連結可能是一個圖片連結或e-mail資訊等。

CATEGORY_LAUNCHER 用於標識Activity是某個App的入口Activity。

你可以在Intent類中查詢到更多預定義的category。

Extras
extras,顧名思義,就是額外的資料資訊,Intent中有一個Bundle物件儲存著各種鍵值對,接收該Intent的元件可以從中讀取出所需要的資訊以便完成相應的工作。有的Intent需要靠Uri攜帶資料,有的Intent是靠extras攜帶資料資訊。

你可以通過呼叫Intent物件的各種過載的putExtra(key, value)方法向Intent中加入各種鍵值對形式的額外資料。你也可以直接建立一個Bundle物件,向該Bundle物件傳入很多鍵值對,然後通過呼叫Intent物件的putExtras(Bundle)方法將其一塊設定給Intent物件中去。

例如,你建立了一個action為ACTION_SEND的Intent物件,然後想用它啟動e-mail傳送郵件,那麼你需要給該Intent物件設定兩個extra的值:
Intent.EXTRA_EMAIL 作為key值設定收件方,用Intent.EXTRA_SUBJECT 作為key值設定郵件標題。

Intent類裡面也指定了很多預定義的EXTRA_*形式的extra,例如上面我們提到的(Intent.EXTRA_EMAILIntent.EXTRA_SUBJECT)。如果你想要宣告你自己自定義的extra,請確保將你的App的包名作為你的extra的字首,例如:

static final String EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS";

Flags
flag就是標記的意思,Intent類中定義的flag能夠起到作為Intent物件的後設資料的作用。這些flag會告知Android系統如何啟動Activity(例如,新啟動的Activity屬於哪個task)以及在該Activity啟動後如何對待它(比如)。更多資訊可參見Intent的setFlags()方法。


顯式Intent使用示例

Intent intent = new Intent(this, ActivityB.class);
startActivity(intent);

上面的程式碼在Intent的建構函式中指定了要啟動的元件的ComponentName是ActivityB,該intent物件是顯式的,呼叫startActivity(intent)時,Android系統會立即啟動ActivityB。


隱式Intent使用示例

之前提到過,在使用隱式Intent的時候需要指定其action。如果你的App不能完成某個功能,但是其他的App可能完成該功能,那麼你就可以用隱式Intent啟動其他的App去完成相應的功能。例如,你有一段文字資訊,想通過其他App分享出去,那麼隱式Intent物件去啟動潛在的支援分享的App,示例程式碼如下:

Intent sendIntent = new Intent();
// 設定action, action對隱式Intent來說是非常重要的
sendIntent.setAction(Intent.ACTION_SEND);
// 設定資料的MIME型別為純文字型別
sendIntent.setType("text/plain");
// 設定額外的資料
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);

// 獲取包管理器
PackageManager pm = getPackageManager();
// 先判斷系統中有沒有潛在的App的Activity支援對該sendIntent的接收與處理
if (pm.resolveActivity(sendIntent, 0) != null) {
    startActivity(sendIntent);
}

上面的程式碼中,我們構建了一個Intent物件,並沒有給其設定component name,所以該Intent是一個隱式的Intent物件。我們首先給intent設定了action的值為Intent.ACTION_SEND,action對隱式Intent來說是非常重要的。然後我們將intent的資料的MIME型別設定為純文字型別(“text/plain”),告知Android我們的Intent持有的是文字型別的資料。最後我們將實際的文字資料通過putExtra()方法作為額外資料設定進去。
需要注意的是,在構建好了Intent物件之後,我們沒有立即執行startActivity(sendIntent)方法,而是將sendIntent作為引數傳遞給了PackageManager的resolveActivity()方法中,該方法會讓Android根據該sendIntent找到潛在的適合啟動的元件的資訊,並以ResolveInfo類的物件的形式返回結果,如果返回null,表示當前系統中沒有任何元件可以接收並處理該sendIntent。如果返回不是null,就表明系統中至少存在一個元件可以接收並處理該sendIntent,只有在這種情況下,我們才會執行程式碼startActivity(sendIntent),在通過intent啟動元件之前先判斷要啟動的元件存不存在是個良好的程式設計習慣,因為如果系統中不存在支援你的intent的元件,那麼當你呼叫startActivity()、startService()、bindService()等方法的時候,Android就會丟擲異常。


強制使用者使用App Chooser

在上文中我們已經提到,如果我們的Intent是隱式的,當我們通過startActivity(intent)嘗試啟動元件的時候,可能Android系統會顯示上面的截圖檔案詢問使用者要啟動哪個App,有時候使用者會將某一個App設定為預設的App,這樣下次我們再執行程式碼startActivity(intent)的時候就有可能不會再出現選擇App的介面,而是直接執行上次使用者設定為預設App的應用。這對於使用者選擇一個預設瀏覽器開啟網頁這種情形是有好處的,因為一般一個使用者習慣於用一個自己喜歡的瀏覽器。

但是如果使用者不想每次都用同一個預設App處理這樣的情形怎麼辦呢?這時候我們可以在程式碼中明確地使用App選擇對話方塊,比如黨我們的App執行一個action為ACTION_SEND的分享功能時,我們想讓使用者分享自己資料的程式碼,但是我們不確定使用者想通過哪個App去分享,我們想每次都彈出App選擇對話方塊讓使用者決定想通過哪個App分享,示例程式碼如下所示:

Intent sendIntent = new Intent(Intent.ACTION_SEND);
...

String title = "請選擇想通過哪個App分享資料";

// 驗證是否有App能夠接收並處理sendIntent
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    // 根據sendIntent建立一個需要顯示App選擇對話方塊的intent物件
    Intent chooserIntent = Intent.createChooser(sendIntent, title);
    // 我們使用chooserIntent作為startActivity()方法的引數,而非sendIntent
    startActivity(chooserIntent);
}

首先我們建立了我們原始的sendIntent,並對其設定action等相關資訊,然後我們將sendIntent傳遞給了Intent.createChooser()方法中,建立了另一個chooserIntent。後面我們通過呼叫Intent.resolveActivity(PackageManager)方法判斷系統中是否有App能夠接收並處理sendIntent,該方法與上面之前提到過的PackageManager的resolveActivity()方法是等價的。最後我們使用chooserIntent作為startActivity()方法的引數,而非sendIntent,chooserIntent會讓Android系統強制顯示使用者選擇App處理Intent的介面。

本文大部分參考了Android中對Intent部分的Develop Guide的描述,希望本文對大家更好地使用Intent物件有所幫助。

如果有疑問,歡迎大家在評論中給我留言,看到會及時回覆。

相關閱讀:
Android中Intent物件與Intent Filter過濾匹配過程詳解
Android中常見Intent習慣用法-上篇(附原始碼下載)

相關文章