Android開發藝術探索知識回顧——第1章 Activity的生命週期和啓動模式:2、啓動模式

 

1.2 Activity的啓動模式

上一節介紹了 Activity 在標準情況下和異常情況下的生命週期,我們對Activity的生命週期應該有了深入的瞭解。除了 Activity 的生命週期外,Activity 的啓動模式也是一個難點, 原因是形形色色的啓動模式和標誌位實在是太容易被混淆了,但是Activity作爲四大組件之首,它的的確確非常重要,有時候爲了滿足項目的特殊需求,就必須使用 Activity 的啓動模式,所以我們必須要搞清楚它的啓動模式和標誌位,本節將會一一介紹。

 

1.2.1 Activity 的 LaunchMode

首先說一下 Activity 爲什麼需要啓動模式。我們知道,在默認情況下,當我們多次啓動同一個Activity的時候,系統會創建多個實例並把它們一一放入任務棧中,當我們單擊 back 鍵,會發現這些Activity會一一回退。任務棧是一種“後進先出”的棧結構,這個比較好理解,每按一下 back 鍵就會有一個 Activity 出棧,直到棧空爲止,當棧中無任何 Activity 的時候,系統就會回收這個任務棧

關於任務棧的系統工作原理,這裏暫時不做說明,在後續章節會專門介紹任務棧。知道了 Activity 的默認啓動模式以後,我們可能就會發現一個問題:多次啓動同一個 Activity,系統重複創建多個實例,這樣不是很傻嗎?這樣的確有點傻,Android在設計的時候不可能不考慮到這個問題,所以它提供了啓動模式來修改系統的默認行爲。目前有四種啓動模式:standard、singleTop、singleTask 和 singlelnstance,下面先介紹各種啓動模式的含義:

 

(1) standard:標準模式,這也是系統的默認模式。

每次啓動一個 Activity 都會重新創建一個新的實例,不管這個實例是否已經存在。被創建的實例的生命週期符合典型情況下 Activity 的生命週期,如上節描述,它的 onCreate、onStart、onResume 都會被調用。這是一種典型的多實例實現,一個任務棧中可以有多個實例,每個實例也可以屬於不同的任務棧。

在這種模式下,誰啓動了這個 Activity,那麼這個Activity 就運行在啓動它的那個 Activity 所在的棧中。比如 Activity A 啓動了 Activity B (B是標準模式),那麼 B 就會進入到 A 所在的棧中。不知道讀者是否注意到,當我們用 ApplicationContext 去啓動 standard 模式的 Activity 的時候會報錯,錯誤如下:

E/AndroidRuntime(674): android.util.androidruntiomException: Calling startActivity from 
outside of an Activity context requires the FLAG_ACTIVITY_TASK flag . Is this really what are want?

相信這句話讀者一定不陌生,這是因爲 standard 模式的 Activity 默認會進入啓動它的 Activity 所屬的任務棧中,但是由於非 Activity 類型的 Context (如ApplicationContext)  並沒有所謂的任務棧,所以這就有問題了。

解決這個問題的方法是:爲待啓動 Activity 指定 FLAG_ACTIVITY_NEW_TASK 標記位,這樣啓動的時候就會爲它創建一個新的任務棧,這個時候待啓動 Activity實際上是以 singleTask 模式啓動的,讀者可以仔細體會。

 

(2) singleTop:棧頂複用模式。

在這種模式下,如果新 Activity 已經位於任務棧的棧頂,那麼此 Activity 不會被重新創建,同時它的 onNewIntent 方法會被回調通過此方法的參數我們可以取出當前請求的信息。需要注意的是,這個 Activity 的 onCreate、onStart 不會被系統調用,因爲它並沒有發生改變。如果新 Activity 的實例已存在但不是位於棧頂,那麼新 Activity 仍然會重新重建。

舉個例子,假設目前棧內的情況爲 ABCD,其中 ABCD 爲四個Activity,A位於棧底,D位於棧頂,這個時候假設要再次啓動D,如果D的啓動模式爲singleTop,那麼棧內的情況仍然爲ABCD;如果D的啓動模式爲standard,那麼由於D被重新創建,導致棧內的情況就變爲ABCDD。

 

(3) singleTask:棧內複用模式。

這是一種單實例模式,在這種模式下,只要 Activity 在一個棧中存在,那麼多次啓動此 Activity 都不會重新創建實例,和 singleTop 一樣,系統也會回調其onNewIntent。

具體一點,當一個具有 singleTask 模式的 Activity 請求啓動後,比如Activity A,系統首先會尋找是否存在 A 想要的任務棧。如果不存在,就重新創建一個任務棧,然後創建 A 的實例後把 A 放到棧中。

如果存在 A 所需的任務棧,這時要看 A 是否在棧中有實例存在,如果有實例存在,那麼系統就會把 A 調到棧頂並調用它的 onNewIntent 方法。如果實例不存在,就創建 A 的實例並把 A 壓入棧中。舉幾個例子:

  • 比如目前任務棧 S1 中的情況爲 ABC,這個時候 Activity D 以 singleTask 模式請求啓動,其所需要的任務棧爲 S2,由於 S2 和 D 的實例均不存在,所以系統會先創建任務棧 S2,然後再創建 D 的實例並將其入棧到 S2。
  • 另外一種情況,假設 D 所需的任務棧爲 S1,其他情況如上面例子 1 所示,那麼由於 S1 已經存在,所以系統會直接創建 D 的實例並將其入棧到 S1。
  • 如果 D 所需的任務棧爲 S1,並且當前任務棧 S1 的情況爲 ADBC,根據棧內複用的原則,此時 D 不會重新創建,系統會把 D 切換到棧頂並調用其 onNewIntent 方法, 同時由於 singleTask 默認具有 clearTop 的效果,會導致棧內所有在 D 上面的 Activity 全部岀棧,於是最終 S1 中的情況爲 AD。這一點比較特殊,在後面還會對此種情況詳細地分析。

通過上述 3 個例子,讀者應該能比較清晰地理解 singleTask 的含義了。

 

(4) singlelnstance:單實例模式。

這是一種加強的 singleTask 模式,它除了具有 singleTask 模式的所有特性外,還加強了一點,那就是具有此種模式的 Activity 只能單獨地位於一個任務棧中,換句話說,比如 Activity A 是 singlelnstance 模式,當 A 啓動後,系統會爲它創建一個新的任務棧,然後 A 獨自在這個新的任務棧中,由於棧內複用的特性,後續的請求均不會創建新的 Activity,除非這個獨特的任務棧被系統銷燬了。

 

上面介紹了幾種啓動模式,這裏需要指出一種情況:我們假設目前有 2 個任務棧,前臺任務棧的情況爲AB,而後臺任務棧的情況爲CD,這裏假設 CD 的啓動模式均爲singleTask。

現在請求啓動 D,那麼整個後臺任務棧都會被切換到前臺,這個時候整個後退列表變成了 ABCD。當用戶按 back 鍵的時候,列表中的 Activity 會出棧,如圖1-7所示。如果不是請求啓動 D 而是啓動 C,那麼情況就不一樣了,請看圖1-8,具體原因在本節後面會再進行詳細分析。

  

 

什麼是 Activity 所需要的任務棧呢?

另外一個問題是,在 singleTask 啓動模式中,多次提到某個 Activity 所需的任務棧,什麼是 Activity 所需要的任務棧呢?這要從一個參數說起:TaskAffinity,可以翻譯爲任務相關性。這個參數標識了一個Activity所需要的任務棧的名字,默認情況下,所有Activity 所需的任務棧的名字爲應用的包名。

當然,我們可以爲每個Activity都單獨指定 TaskAffinity 屬性,這個屬性值必須不能和包名相同,否則就相當於沒有指定。TaskAffinity 屬性主要和 singleTask 啓動模式或者 allowTaskReparenting 屬性配對使用,在其他情況下沒有意義。

另外,任務棧分爲前臺任務棧和後臺任務棧,後臺任務棧中的 Activity 位於暫停狀態,用戶可以通過切換將後臺任務棧再次調到前臺。當 TaskAffinity 和 singleTask 啓動模式配對使用的時候,它是具有該模式的 Activity 的目前任務棧的名字,待啓動的 Activity 會運行在名字和 TaskAffinity 相同的任務棧中。

當 TaskAffinity 和 allowTaskReparenting 結合的時候,這種情況比較複雜,會產生特殊的效果。當一個應用 A 啓動了應用 B 的某個Activity 後,如果這個 Activity 的 allowTaskReparenting 屬性爲 true 的話,那麼當應用 B 被啓動後,此 Activity 會直接從應用 A 的任務棧轉移到應用 B 的任務棧中。

這還是很抽象,再具體點,比如現在有2個應用 A 和 B,A 啓動了 B 的一個Activity C,然後按Home 鍵回到桌面,然後再單擊 B 的桌面圖標,這個時候並不是啓動了 B 的主 Activity。而是重新顯示了已經被應用 A 啓動的 Activity C,或者說,C 從 A 的任務棧轉移到了 B 的任務棧中。

可以這麼理解,由於 A 啓動了 C,這個時候 C 只能運行在 A 的任務棧中,但是 C 屬於 B 應用,正常情況下,它的 TaskAffinity 值肯定不可能和 A 的任務棧相同(因爲包名不同)。所以,當 B 被啓動後,B 會創建自己的任務棧,這個時候系統發現 C 原本所想要的任務棧已經被創建了,所以就把 C 從 A 的任務棧中轉移過來了。這種情況讀者可以寫個例子測試一下,這裏就不做示例了。

 

如何給Activity指定啓動模式呢?

如何給Activity指定啓動模式呢?有兩種方法,第一種是通過 AndroidMenifest 爲 Activity 指定啓動模式,如下所示。

另一種情況是通過在 Intent 中設置標誌位來爲 Activity 指定啓動模式,比如:

這兩種方式都可以爲 Activity 指定啓動模式,但是二者還是有區別的。首先,優先級上,第二種方式的優先級要高於第一種,當兩種同時存在時,以第二種方式爲準;其次,上述兩種方式在限定範圍上有所不同,比如,第一種方式無法直接爲 Activity 設定 FLAG_ACTIVITY_CLEAR_TOP 標識,而第二種方式無法爲 Activity 指定 singlelnstance 模式。

 

體驗啓動模式——代碼示例

關於 Intent 中爲 Activity 指定的各種標記位,在下面的小節中會繼續介紹。下面通過一個例子來體驗啓動模式的使用效果。還是前面的例子,這裏我們把 MainActivity 的啓動模式設爲 singleTask,然後重複啓動它,看看是否會重複創建,代碼修改如下:

1、把 MainActivity 的啓動模式設爲 singleTask

<activity
   android:name="com.yyh.demo4.launchMode.MainActivity"
   android:configChanges="orientation|screenSize"
   android:label="@string/app_name" 
   android:launchMode="singleTask"
   >
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

 

2、MainActivity裏面添加如下代碼:

findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent();
        intent.setClass(MainActivity.this, MainActivity.class);
        intent.putExtra("time", System.currentTimeMillis());
        startActivity(intent);
    }
});


@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    Log.d(TAG, "onNewIntent, time=" + intent.getLongExtra("time", 0));
}

根據上述修改,我們做如下操作,連續單擊三次按鈕啓動3次 MainActivity,算上原本的 MainActvity 的實例,正常情況下,任務棧中應該有4個 MainActivity 的實例,但是我們爲其指定了 singleTask 模式,現在來看一看到底有何不同。

 

執行 adb shell dumpsys activity 命令分析

mac 上打開 terminal終端,輸入命令 adb shell dumpsys activity,

command + f,搜索關鍵字:ACTIVITY MANAGER ACTIVITIES

具體日誌如下:

從上面導出的 Activity 信息可以看出,儘管啓動了 4 次 MainActivity,但是它始終只有一個實例在任務棧中。從圖1-9的 log 可以看出,Activity 的確沒有重新創建,只是暫停了一 下,然後調用了onNewIntent,接着調用 onResume 就又繼續了。

現在我們去掉singleTask,再來對比一下,還是同樣的操作,單擊三次按鈕啓動 MainActivity 三次。

執行 adb shell dumpsys activity 命令:

mac 上打開 terminal終端,輸入命令 adb shell dumpsys activity,

command + f,搜索關鍵字:ACTIVITY MANAGER ACTIVITIES

導出信息很多,我們可以有選擇地看,比如就看 Running activities (most recent first)這一塊,如下所示。

我們能夠得出目前總共有 2 個任務棧,前臺任務棧的 taskAffinity 值爲 com.ryg.chapter_1,它裏面有 4 個 Activity,後臺任務棧的 taskAffinity 值爲 com.mumu.launcher,它裏面有1個Activity,這個Activity就是桌面。( 注意:後臺任務棧的 taskAffinity 值不是 com.android.launcher,因爲測試用的網易MuMu模擬器,所以是 com.mumu.launcher )

通過這種方式來分析任務棧信息就清晰多了。從上面的導出信息中可以看到,在任務棧中有 4 個MainActivity,這也就驗證了 Activity 的啓動模式的工作方式。

 

 singleTask 特殊情況說明

上述四種啓動模式,standard 和 singleTop 都比較好理解,singlelnstance 由於其特殊性也好理解,但是關於 singleTask 有一種情況需要再說明一下。如圖1-7所示,如果在 Activity B 中請求的不是 D 而是 C,那麼情況如何呢?這裏可以告訴讀者的是,任務棧列表變成了 ABC,是不是很奇怪呢? Activity D 被直接出棧了。

  

 

修改代碼進行說明

下面我們再用實例驗證看看是不是這樣。 首先,還是使用上面的代碼,但是我們做一下修改:

        <activity
            android:name="com.yyh.demo5.singleTask.MainActivity"
            android:configChanges="orientation|screenSize"
            android:label="@string/app_name"
            android:launchMode="standard" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity
            android:name="com.yyh.demo5.singleTask.SecondActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:taskAffinity="com.ryg.task1" 
            android:launchMode="singleTask"/>
        
        <activity
            android:name="com.yyh.demo5.singleTask.ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:taskAffinity="com.ryg.task1" 
            android:launchMode="singleTask"/>

 

我們將 SecondActivity 和 ThirdActivity 都設成 singleTask 並指定它們的 taskAffinity 屬性爲“com.ryg.task1”,注意這個taskAffinity屬性的值爲字符串,且中間必須含有包名分隔符“.”。

然後做如下操作,在 MainActivity 中單擊按鈕啓動 SecondActivity,在 SecondActivity 中單擊按鈕啓動 ThirdActivity,在ThirdActivity 中單擊按鈕又啓動 MainActivity,最後再在 MainActivity 中單擊按鈕啓動 SecondActivity,現在按back鍵,然後看到的是哪個Activity?答案:是回到桌面。是不是有點摸不到頭腦了?沒關係,接下來我們分析這個問題。

 

從理論上分析這個問題

首先,從理論上分析這個問題,先假設 MainActivity 爲ASecondActivity 爲B,ThirdActivity 爲C。我們知道 A 爲standard模式,按照規定,A 的taskAffinity 值繼承自 Application 的 taskAffinity,而 Application 默認 taskAffinity 爲包名,所以 A 的 taskAffinity 爲包名。由於我們在 XML 中爲 B 和 C 指定了 taskAffinity 和啓動模式,所以 B 和 C 是 singleTask 模式且有相同的 taskAffinity 值“com.ryg.task1”。

A 啓動 B 的時候,按照 singleTask 的規則,這個時候需要爲 B 重新創建一個任務棧“com.ryg.task1”。B 再啓動 C,按照singleTask 的規則,由於C所需的任務棧(和B爲同一任務棧)已經被B創建,所以無須再創建新的任務棧,這個時候系統只是創建 C 的實例後將C入棧了。

接着 C 再啓動 A,A 是standard 模式,所以系統會爲它創建一個新的實例,並將它加到啓動它的那個 Activity 的任務棧,由於是 C 啓動了 A,所以 A 會進入 C 的任務棧中並位於棧頂。這個時候己經有兩個任務棧了,一個是名字爲包名的任務棧,裏面只有 A,另一個是名字爲“com.ryg.task1”的任務棧,裏面的 Activity 爲BCA。

接下來,A 再啓動 B,由於 B 是 singleTask,B 需要回到任務棧的棧頂,由於棧的工作模式爲“後進先出”,B想要回到棧頂,只能是CA出棧。所以,到這裏就很好理解了,如果再按back鍵,B 就出棧了,B 所在的任務棧已經不存在了,這個時候只能是回到後臺任務棧並把 A 顯示出來。

注意這個 A 是後臺任務棧的 A,不是 "com.ryg.task1”任務棧的 A,接着再繼續back,就回到桌面了。分析到這裏,我們得岀一條結論,singleTask 模式的 Activity 切換到棧頂會導致在它之上的棧內的 Activity 出棧。

 

釆用dumpsys命令驗證說明

接着我們在實踐中再次驗證這個問題,還是釆用dumpsys命令。我們省略中間的過程,直接看 C 啓動 A 的那個狀態,執行 adb shell dumpsys activity 命令,

mac 上打開 terminal終端,輸入命令 adb shell dumpsys activity,

command + f,搜索關鍵字:ACTIVITY MANAGER ACTIVITIES

查看關鍵信息 Running activities (most recent first)

日誌如下:

可以清楚地看到有 2 個任務棧,第一個(com.ryg.chapter_1)只有 A,第二個(com.ryg.task1) 有BCA,就如同我們上面分析的那樣,然後再從 A 中啓動 B ,再看一下日誌

可以發現在任務棧 com.ryg.task1 中只剩下 B 了,C、A 都已經出棧了。這個時候再按 back 鍵,任務棧 com.ryg.chapter_1 中的 A 就顯示出來了,如果再 back 就回到桌面了。分析到這裏,相信讀者對 Activity 的啓動模式已經有很深入的理解了。

注意:

1、用網易的MuMu模擬器進行測試,在點擊跳轉過程中,會出現黑屏現象,所以改用真機測試了!

2、其他任務棧爲手機系統自帶的任務棧,這塊可以忽略!

下面介紹Activity中常用的標誌位。

 

 

1.2.2 Activity 的 Flags

Activity 的 Flags 有很多,這裏主要分析一些比較常用的標記位。標記位的作用很多,有的標記位可以設定 Activity 的啓動模式,比如FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_SINGLE_TOP等;還有的標記位可以影響 Activity 的運行狀態,比如 FLAG_ACTIVITY_CLEAR_TOP 和 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 等。 下面主要介紹幾個比較常用的標記位,剩下的標記位讀者可以査看官方文檔去了解,大部分情況下,我們不需要爲 Activity 指定標記位,因此,對於標記位理解即可。在使用標記位的時候,要注意有些標記位是系統內部使用的,應用程序不需要去手動設置這些標記位以防出現問題。

FLAG_ACTIVITY_NEW_TASK

這個標記位的作用是爲 Activity 指定 “singleTask” 啓動模式,其效果和在 XML 中指定該啓動模式相同。

FLAG_ACTIVITY_SINGLE_TOP

這個標記位的作用是爲 Activity 指定 “singleTop” 啓動模式,其效果和在 XML 中指定該啓動模式相同。

FLAG_ACTIVITY_CLEAR_TOP

具有此標記位的 Activity ,當它啓動時,在同一個任務棧中所有位於它上面的 Activity 都要出棧。這個模式一般需要和FLAG_ACTIVITY_NEW_TASK 配合使用,在這種情況下,被啓動 Activity 的實例如果已經存在,那麼系統就會調用它的 onNewIntent。如果被啓動的 Activity 釆用 standard 模式啓動,那麼它連同它之上的 Activity 都要出棧,系統會創建新的 Activity 實例並放入棧頂。通過1.2.1節中的分析可以知道,singleTask啓動模式默認就具有此標記位的效果。

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

具有這個標記的 Activity 不會出現在歷史 Activity 的列表中,當某些情況下我們不希望用戶通過歷史列表回到我們的 Activity 的時候這個標記比較有用,它等同於在 XML 中指定 Activity 的屬性 android:excludeFromRecents="true"。

 

1.3 IntentFilter的匹配規則

我們知道,啓動 Activity 分爲兩種,顯式調用和隱式調用。二者的區別這裏就不多說,顯式調用需要明確地指定被啓動對象的組件信息,包括包名和類名,而隱式調用則不需要明確指定組件信息。原則上一個 Intent 不應該既是顯式調用又是隱式調用,如果二者共存的話以顯式調用爲主

顯式調用很簡單,這裏主要介紹一下隱式調用。隱式調用需要 Intent 能夠匹配目標組件的 IntentFilter 中所設置的過濾信息,如果不匹配將無法啓動目標 Activity。IntentFilter 中的過濾信息有 action、category、data,下面是一個過濾規則的示例:

        <activity
            android:name="com.yyh.demo6.IntentFilter.ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:taskAffinity="com.ryg.task1" 
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="com.ryg.charpter_1.c" />
                <action android:name="com.ryg.charpter_1.d" />
                <category android:name="com.ryg.category.c" />
                <category android:name="com.ryg.category.d" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
         	</intent-filter>
         </activity>

爲了匹配過濾列表,需要同時匹配過濾列表中的 action、category、data 信息,否則匹配失敗。一個過濾列表中的 action、category 和data 可以有多個,所有的 action、category、 data 分別構成不同類別,同一類別的信息共同約束當前類別的匹配過程。

只有一個 Intent 同時匹配action類別、category類別、data類別纔算完全匹配,只有完全匹配才能成功啓動目標 Activity。另外一點,一個 Activity 中可以有多個 intent-filter,一個 Intent 只要能匹配任何一組 intent-filter 即可成功啓動對應的 Activity,如下所示。

         <activity android:name="ShareActivity">
            <!-- This activity handlers "SEND" actions with text data-->
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
            <!--This activity also handlers "SEND" and "SEND_MULTIPLE" with media data-->
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <action android:name="android.intent.action.SEND_MULTIPLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="application/vnd.google.panorama360+jpg"/>
                <data android:mimeType="image/*" />
                <data android:mimeType="video/*" />
            </intent-filter>
        </activity>

 

下面詳細分析各種屬性的匹配規則。

1. action的匹配規則

action 是一個字符串,系統預定義了一些 action,同時我們也可以在應用中定義自己的 action。action 的匹配規則是 Intent 中的 action 必須能夠和過濾規則中的 action 匹配,這裏說的匹配是指 action 的字符串值完全一樣。

一個過濾規則中可以有多個 action,那麼只要 Intent 中的 action 能夠和過濾規則中的任何一個 action 相同即可匹配成功。針對上面的過濾規則,只要我們的 Intent 中 action 值爲 “com.ryg.charpter_l.c” 或者 “com.ryg.charpter_ l.d” 都能成功匹配。需要注意的是,Intent中如果沒有指定 action,那麼匹配失敗。

總結一下,action的匹配要求 Intent 中的 action 存在且必須和過濾規則中的其中一個 action 相同,這裏需要注意它和 category 匹配規則的不同。另外,action 區分大小寫,大小寫不同字符串相同的 action 會匹配失敗。

 

2. category的匹配規則

category 是一個字符串,系統預定義了一些 category,同時我們也可以在應用中定義自己的 category。category 的匹配規則和 action 不同,它要求 Intent 中如果含有 category,那麼所有的 category 都必須和過濾規則中的其中一個 category 相同。換句話說,Intent 中如果出現了 category,不管有幾個category,對於每個category來說,它必須是過濾規則中已經定義了的 category。

當然,Intent中可以沒有 category,如果沒有 category 的話,按照上面的描述,這個 Intent 仍然可以匹配成功。這裏要注意下它和 action 匹配過程的不同,action 是要求 Intent 中必須有一個 action 且必須能夠和過濾規則中的某個 action 相同,而 category 要求 Intent 可以沒有category,但是如果你一旦有 category,不管有幾個,每個都要能夠和過濾規則中的任何一個 category 相同。

爲了匹配前面的過濾規則中的 category,我們可以寫出下面的 Intent,intent.addcategory ("com.ryg.category.c") 或者 Intent.addcategory ("com.ryg. category.d")  亦或者不設置 category。

爲什麼不設置 category 也可以匹配呢?

原因是系統在調用 startActivity 或者 startActivityForResult 的時候會默認爲 Intent 加上 " android.intent.category.DEFAULT " 這個 category,所以這個 category 就可以匹配前面的過濾規則中的第三個 category。同時,爲了我們的activity能夠接收隱式調用,就必須在 intent-filter 中指定 "android.intent.category.DEFAULT" 這個 category,原因剛纔已經說明了。

 

3. data的匹配規則

data 的匹配規則和 action 類似,如果過濾規則中定義了 data,那麼 Intent 中必須也要定義可匹配的 data。在介紹 data 的匹配規則之前,我們需要先了解一下 data 的結構,因爲 data 稍微有些複雜。

data的語法如下所示:

    <data 
       android:scheme="string"
       android:host="string"
       android:port="string"
       android:path="string"
       android:pathPattern="string"
       android:pathPrefix="string"
       android:mimeType="string"
        />

data 由兩部分組成,mimeType 和 URI。mimeType指媒體類型,比如 image/jpeg、audio/mpeg4-generic 和 video/* 等,可以表示圖片、文本、視頻等不同的媒體格式,而 URI 中包含的數據就比較多了,下面是 URI 的結構:

    <scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

這裏再給幾個實際的例子就比較好理解了,如下所示。
 

content://com.example.project:200/folder/subfolder/etc 

http://www.baidu.com:80/search/info

看了上面的兩個示例應該就瞬間明白了,沒錯,就是這麼簡單。不過下面還是要介紹 一下每個數據的含義。

Scheme:URI的模式,比如 http、file、content 等,如果URI中沒有指定 scheme。那 麼整個 URI 的其他參數無效,這也意味着 URI 是無效的。

Host:URI的主機名,比如 www.baidu.com,如果 host 未指定,那麼整個URI中的其他參數無效,這也意味着 URI 是無效的。

Port:URI中的端口號,比如80,僅當 URI 中指定了 scheme 和 host 參數的時候 port 參數纔是有意義的。

Path、pathPattem 和 pathPrefix:這三個參數表述路徑信息,其中 path 表示完整的路徑信息;pathPattem 也表示完整的路徑信息,但是它裏面可以包含通配符 "*","*" 表示 0 個或多個任意字符,需要注意的是,由於正則表達式的規範,如果想表示真實的字符串,那麼"*"要寫成 "\\*","\"要寫成"\\\\";pathPrefix表示路徑的前綴信息。

介紹完 data 的數據格式後,我們要說一下 data 的匹配規則了。前面說到,data 的匹配規則和 action 類似,它也要求 Intent 中必須含有data 數據,並且 data 數據能夠完全匹配過濾規則中的某一個 data。這裏的完全匹配是指過濾規則中出現的 data 部分也出現在了 Intent 中的 data 中。下面分情況說明。

(1)如下過濾規則:

       <intent-filter>
          <data android:mimeType="image/*"/>
      </intent-filter>

這種規則指定了媒體類型爲所有類型的圖片,那麼 Intent 中的 mimeType 屬性必須爲 "image/*" 才能匹配,這種情況下雖然過濾規則沒有指定 URI,但是卻有默認值,URI 的默認值爲 content 和 file 。

也就是說,雖然沒有指定 URI,但是 Intent 中的 URI 部分的 schema 必須爲 content 或者 file 才能匹配,這點是需要尤其注意的。爲了匹配 (1) 中規則,我們可以寫出如下示例:

    intent.setDataAndType(Uri.parse("file://abc"),"image/png");

另外,如果要爲 Intent 指定完整的 data,必須要調用 setDataAndType 方法,不能先調用 setData 再調用 setType,因爲這兩個方法彼此會清除對方的值,這個看源碼就很容易理解,比如setData:

    public Intent setData(Uri data) {
        mData = data;
        mType = null;
        return this;
    }

可以發現,setData 會把 mimeType 置爲null,同理 sctType 也會把 URI 置爲 null。

 

(2)如下過濾規則:

      <intent-filter>
           <data android:mimeType="video/mpeg" android:scheme="http" .../>
           <data android:mimeType="audio/mpeg" android:scheme="http" .../>
      </intent-filter>

這種規則指定了兩組 data 規則,且每個 data 都指定了完整的屬性值,既有 URI 又有 mimeType。爲了匹配 (2) 中規則,我們可以寫岀如下示例:

    intent.setDataAndType(Uri.parse("http://abc"),"video/png");

或者

    intent.setDataAndType(Uri.parse("http://abc"),"audio/png"); 

通過上面兩個示例,讀者應該已經明白了 data 的匹配規則,關於 data 還有一個特殊情況需要說明下,這也是它和 action 不同的地方,如下兩種特殊的寫法,它們的作用是一樣的:

  <intent-filter >
     <data android:scheme="file" android:host="www.baidu.com"/>
         ...
 </intent-filter>


 <intent-filter >
       <data android:scheme="file" />
       <data android:host="www.baidu.com"/>
         ...
  </intent-filter>

到這裏我們已經把 IntentFilter 的過濾規則都講解了一遍,還記得本節前面給出的一個 intent-filter 的示例嗎?現在我們給出完全匹配它的Intent:

    Intent intent = new Intent("com.ryg.charpter_1.c");
    intent.addCategory("com.ryg.category.c");
    intent.setDataAndType(Uri.parse("file//abc"),"text/plain");
    startActivity(intent);

還記得 URI 的 schema 是有默認值的嗎?

如果把上面的 intent.setDataAndType(Uri.parse("file://abc"), text/plain") 這句成 intent.setDataAndType(Uri.parse("http://abc"), "text/plain")打開 Activity 的時候就會報錯,提示無法找到 Activity,如圖1-10所示。

另外 一點,Intent-filter的匹配規則對於Service和BroadcastReceiver 也是同樣的道理,不過系統對於 Service 的建議是儘量使用顯式調用方式來啓動服務。

 

最後,當我們通過隱式方式啓動一個 Activity 的時候,可以做一下判斷,看是否有 Activity 能夠匹配我們的隱式 Intent,如果不做判斷就有可能出現上述的錯誤了。判斷方法有兩種:釆用 PackageManager 的 resolveActivity 方法或者 Intent 的 resoIveActivity 方法,如果它們找不到匹配的 Activity 就會返回 null,我們通過判斷返回值就可以規避上述錯誤了。

另外,PackageManager 還提供了 querylntentActivities 方法,這個方法和 resolveActivity 方法不同的是:它不是返回最佳匹配的 Activity信息而是返回所有成功匹配的 Activity 信息。 我們看一下 querylntentActivities 和 resolveActivity 的方法原型:

    public abstract List<ResolveInfo>queryIntentActivities(Intent intent,int fladgs);
    public abstract ResolveInfo resolveActivity(Intent intent,int flags);

上述兩個方法的第一個參數比較好理解,第二個參數需要注意,我們要使用 MATCH_ DEFAULT_ONLY 這個標記位,這個標記位的含義是僅僅匹配那些在 intent-filter 中聲明瞭 <category android:name="android.intent.category.DEFAULT"/> 這個 category 的 Activity。

使用這個標記位的意義在於,只要上述兩個方法不返回null,那麼 startActivity 一定可以成功。 如果不用這個標記位,就可以把intent-filter中 category 不含 DEFAULT 的那些 Activity 給匹配出來,從而導致 startActivity 可能失敗。因爲不含有 DEFAULT 這個 category 的 Activity 是無法接收隱式 Intent 的。在 action 和 category 中,有一類 action 和 category 比較重要,它們是:

    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />

這二者共同作用是用來標明這是一個入口 Activity 並且會出現在系統的應用列表中,少了任何一個都沒有實際意義,也無法出現在系統的應用列表中,也就是二者缺一不可。 另外,針對 Service 和 BroadcastReceiver,PackageManager 同樣提供了類似的方法去獲取成功匹配的組件信息。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章