Android實戰技巧:組件間通信—Intent和IntentFilter – Android移動開發技術文章_手機開發 Android移動開發教學課程

 

Understanding Intent and IntentFilter–理解Intent和IntentFilter

Intent(意圖)在Android中是一個十分重要的組件,它是連接不同應用的橋梁和紐帶,也是讓組件級復用(Activity和Service)成為可能的一個重要原因。Intent的使用分為二個方面一個是發出Intent,另一個則是接收Intent用官方的說法就是Intent Resolving。本主將對Intent和IntentFilter進行一些介紹。

Intent和IntentFilter是Android和一種消息通信機制,可以讓系統的組件之間進行通信。信息的載體就是Intent,它可以是一個要完成的動作請求,也可以一般性的消息廣播,它可以由任意一個組件發出。消息,也就是Intent,最終也是要被組件來進行處理和消化。消息由組件發出,通常在消息的裡面也會有會標記有目標組件的相關信息,另外目標組件也需要告訴系統,哪些消息是它所感興趣的,它需要設置一些過濾器,以過濾掉無關的消息。

其實這裡就好比學校裡的廣播,廣播有時會播放通知,但有時也會播放要執行的動作,比如打掃衛生。消息中通常都會說明消息的目標對象,可能是計算機學院,可能是老師,也可能是英語系的人才需要關註。而每個人,或是學院組織,也隻關心與他們有關的消息,當然這裡就要他們自己去判斷哪些是與他們有關的消息瞭。在Android當中消息就是Intent,過濾器就是IntentFilter。發出消息的組件可以在消息中設置目標組件的相關信息,目標組件也可以設置過濾器,然後系統會進行過濾,隻把組件所感興趣的消息,傳遞給組件。這裡假設讀者已經瞭解Intent和IntentFilter的基本使用方法,且並不會進行全面的介紹,如果不瞭解,可以先讀讀官方文檔,這裡重點講講IntentFilter在使用時的一些註意事項。

Intent消息機制通常有二種,一個是顯式Intent(Explicit Intent),另一個是隱式Intent(Implicit Intent)。

顯式Intent–需要在Intent中明確指定目標組件,也就是在Intent中明確寫明目標組件的名稱(Component name),需要指定完整的包名和類名。因為對於本程序以外的其他應用程序,你很難知道它的組件名字,所以顯式的Intent通常用於應用程序內部通信,更確切的說,顯示Intent是用於應用程序內部啟動組件,通常又是Activity或Service。還沒有見用顯式Intent來給BroadcastReceiver發送廣播的。

隱式Intent–也就是不在Intent中指定目標組件,在Intent中不能含有目標的名字。系統是根據其他的信息,比如Data,Type和Category去尋找目標組件。

隱式Intent通常用於與應用程序外部的組件進行通信。應用程序級別的組件復用也主要是靠隱式Intent來完成的。而IntentFilter也是隻有隱式Intent才用的著,顯式Intent都是直接把Intent傳遞給目標組件,根本不會理會組件的IntentFilter。

顯式Intent(Explicit Intent)

顯示Intent使用起來比較簡單,隻需要在Intent中指定目標組件的名字即可,也就是通過Intent的方法設置Component屬性。如前所述,顯式Intent通常用於應用程序內部啟動Activity或Service。事實上,並不完全局限在應用程序內部,對於外部應用的Activity和Service,也可以用顯式Intent來啟動,但你必須知道它們的名字。

設置組件的名字的方法有:

 

public Intent setComponent(ComponentName component); 

public Intent setClass(Context packageContext, Class<?> cls); 

public Intent setClassName (Context packageContext, String className); 

public Intent setClassName (String packageName, String className); 

事實上雖然設置的方法有這麼多,但Intent內部標識目標組件的屬性隻有一個Component,所以這麼設置方法的目的也隻是設置目標組件的Component,事實上有這麼多的方法原因在於ComponentName的構造是多重載瞭的。在解析Intent時,系統也是取得這個Component屬性,然後去啟動它。

ComponentName Intent.getComponent();

對於應用程序內部啟動Activity通常是這樣子設置Intent:

 

Intent i = new Intent();  

// Select one of them 

i.setComponent(new ComponentName(getApplication(), ViewStubDemoActivity.class)); 

i.setComponent(new ComponentName(getApplication(), ViewStubDemoActivity.class.getName())); 

i.setComponent(new ComponentName(getApplication().getPackageName(), ViewStubDemoActivity.class.getName())); 

i.setClass(getApplication(), ViewStubDemoActivity.class); 

i.setClassName(getApplication(), ViewStubDemoActivity.class.getName()); 

i.setClassName(getApplication().getPackageName(), ViewStubDemoActivity.class.getName()); 

startActivity(i); 

因為應用程序內部的組件類,都是可以訪問到的,所以要盡可能少寫字串常量,以減少拼寫錯誤,如果一定要使用包名和類名,也要註意,類名必須是全稱,也就是從包名開始,如“com.hilton.networks.WifiManagerActivity"。

但是對於外部應用程序的Activity,通常隻能通過以下方法:

 

Intent i = new Intent(); 

// select one of them 

i.setComponent(new ComponentName("com.hilton.networks", "com.hilton.networks.WifiManagerActivity")); 

i.setClassName("com.hilton.networks", "com.hilton.networks.WifiManagerActivity"); 

startActivity(i); 

首先,帶有Context為參數的是不能夠用的,因為通常你無法拿到其他應用程序的Context,你隻能拿到你所在應用程序的Context,所以用你所在的應用程序的Context去啟動外部的Activity肯定會報錯的。其次,不參再像上面那樣通過Class.getName()去指定類名,你為你無法導入外部的類,會有編譯錯誤的。另外,千萬要註意不要拼錯,否則會有RuntimeException拋出的。

對於Service組件,也是一樣,Intent的寫法與Activity組件一致,但是對於BroadcastReceiver組件通常都用顯式Intent。

隱式Intent的消息過濾器–IntentFilter

IntentFilter是用來解析隱式Intent(Implicit Intent)的,也就是說告訴系統你的組件(Activity, Service, BroadcastReceiver)能夠處理哪些隱式的Intent。在使用的時候我們通常是這樣子的:

 

<manifest …> 

    <receiver …> 

           <intent-filter> 

              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> 

              <action android:name="android.appwidget.action.APPWIDGET_ENABLED" /> 

              <action android:name="android.appwidget.action.APPWIDGET_DISABLED" /> 

              <action android:name="android.appwidget.action.APPWIDGET_DELETED" /> 

           </intent-filter> 

           <intent-filter> 

              <action android:name="android.intent.action.MEDIA_MOUNTED"/> 

              <action android:name="android.intent.action.MEDIA_UNMOUNTED"/> 

              <action android:name="android.intent.action.MEDIA_SHARED"/> 

              <action android:name="android.intent.action.MEDIA_REMOVED"/> 

              <action android:name="android.intent.action.MEDIA_EJECT"/> 

              <data android:scheme="file" /> 

           </intent-filter> 

           <intent-filter> 

              <action android:name="android.intent.action.PACKAGE_ADDED"/> 

              <action android:name="android.intent.action.PACKAGE_REMOVED"/> 

              <action android:name="android.intent.action.PACKAGE_DATA_CLEARED"/> 

              <data android:scheme="package" /> 

           </intent-filter> 

   </receiver> 

</manifest> 

在Manifest中使用IntentFilter時要註意以下三點:

1. 千萬註意拼寫錯誤

這裡有一個需要十分小心和註意的地方那就是對於IntentFilter裡面的Action和Data字串常量不要寫錯,因為這個在編譯時是不會被檢查,在運行時又不會拋出異常,如果你拼寫錯瞭,比如大小寫拼錯瞭,在編譯時和運行時都不會有錯誤,但是你的程序卻不能正常工作,你的程序無法收到相應的Intent。曾有一個同事在IntentFilter中寫瞭一些Action,但把其中一個的大小寫拼錯瞭,結果花瞭他一個下午的時間來調試,最後還是另外一個同事到他那聊天才發現瞭是大小寫的拼寫錯誤。

這裡也可以發現Android在Manifest文件中的IntentFilter這塊的封裝性很差。如果,僅僅是如果,這些Action常量也可以通過引用的方式來寫,就可以在編譯時做些檢查和匹配,可以大大的減少出錯的機率,同時也加強瞭封裝和信息隱藏。比如,對於上面的可以寫成這樣:

 

<manifest …> 

    <receiver …> 

           <intent-filter> 

              <action android:name="@android:action/AppWidgetManager.APPWIDGET_UPDATE" /> 

              <action android:name="@android:action/AppWidgetManager.APPWIDGET_ENABLED" /> 

              <action android:name="@android:action/AppWidgetManager.APPWIDGET_DISABLED" /> 

              <action android:name="@android:action/AppWidgetManager.APPWIDGET_DELETED" /> 

           </intent-filter> 

   </receiver> 

</manifest> 

雖然這種拼寫錯誤很低級,但是因為它低級所以當程序不能正常工作時沒有人會想到是因為拼寫錯誤,所以這種拼寫錯誤通常會耗費不少的調試時間。另外一種避免此種錯誤的方法就是在代碼中通過Context.registerReceiver(BroadcastReceiver,IntentFilter)來註冊BroadcastReceiver,就可以直接寫入常量,而非具體字串。但這隻能是接收Broadcast的時候,對於那些想作為公開接口的組件,還是需要在Manifest裡面聲明,比如Email,它要能處理Intent.ACTION_SEND_TO,就需要在Manifest中聲明。2. 要註意Data字段除瞭上面討論的之外,對於IntentFilter還有另外的一點需要註意,就是對於某些Action是需要加上Data字段信息,否則有可能接收不到。比如:

 

<manifest …> 

    <receiver …> 

           <intent-filter> 

              <action android:name="android.intent.action.MEDIA_MOUNTED"/> 

              <action android:name="android.intent.action.MEDIA_UNMOUNTED"/> 

              <action android:name="android.intent.action.MEDIA_SHARED"/> 

              <action android:name="android.intent.action.MEDIA_REMOVED"/> 

              <action android:name="android.intent.action.MEDIA_EJECT"/> 

              <data android:scheme="file" /> 

           </intent-filter> 

           <intent-filter> 

              <action android:name="android.intent.action.PACKAGE_ADDED"/> 

              <action android:name="android.intent.action.PACKAGE_REMOVED"/> 

              <action android:name="android.intent.action.PACKAGE_DATA_CLEARED"/> 

              <data android:scheme="package" /> 

           </intent-filter> 

   </receiver> 

</manifest> 

對於手機外部存儲卡的狀態變化的Broadcast,在註冊監聽器的時候就需要加上DataScheme,否則就會接收不到。這個也花費瞭我幾個小時的調試時間,改在代碼中用Context.registerReceiver(BroadcastReceiver,IntentFilter)註冊也不行,最後參考瞭Music中的做法,加上瞭DataScheme才能在onReceive()中接收到Intent。同樣對於後面的Package相關的Broadcast,也是要加上DataScheme否則也是接收不到Broadcast。可悲的是對於像這樣的系統公共的Broadcast

Intent,在Intent的文檔中並沒有說明如何使用,如果沒有參考事例,相信需要一定的時間才能夠找出為什麼接收不到Intent。

除瞭DataScheme還有一個是MimeType,這個對於系統公共接口是必須加上的,比如Email要處理Intent.ACTION_SENTTO,就需要這樣聲明:

 

<manifest …> 

    <activity android:name="EmailComposer"> 

           <intent-filter> 

              <action android:name="android.intent.action.SEND"/> 

              <data android:mimeType="image/*" /> 

           </intent-filter> 

   </activity> 

</manifest> 

3. 同時也要註意Category字段

如果沒有對IntentFilter寫正確的Category字段,也是收不到Intent。比如:

 

<manifest …> 

    <receiver …> 

            <intent-filter> 

                <action android:name="com.hilton.controlpanel.action.BUTTON_ACTION" /> 

                <category android:name="com.hilton.controlpanel.category.SELF_MAINTAIN" /> 

            </intent-filter> 

   </receiver> 

</manifest> 

如果把Category去掉,死活也接收不到Intent,當然這要取決於Intent是如何發出的,如果Intent發出時沒有加Category,那就沒有必須在IntentFilter加上Category。

總之,對於Intent,要保證發出和接收完全一致,否則系統就無法找到相應的匹配,程序也就無法接收Intent。

有關於DEFAULT category,也要註意,如果是針對Activity的Implicit Intent隱式Intent,如果在沒有其他Category的情況下,一定要加上DEFAULT Category。因為系統會在Context.startActivity(Intent)和Context.startActivityForResult(

Intent)時給Intent加上DEFAULT category。而對於Context.sendBroadcast(Intent),Context.sendOrderedBroadcast(Intent),Contxt.sendStickyBroadcast(Intent)和Context.bindService(Intent)Context.startService(Intent)就不會加DEFAULT Category。

另外要註意,盡量把Action進行合並寫進一個IntentFilter中。因為對於每個IntentFilter標簽都會創建一個IntentFilter對象,所以如果寫幾個就會有幾個對象在那,不但耗費資源而且在匹配的時候也會耗費更多的時間,因為在查詢匹配的時候是要一個IntentFilter對象接著一個IntentFilter對象進行檢查的。直到找到最佳匹配或是到所有的IntentFilter都檢查完為止。

IntentFilter的匹配規則

1. 通過Action字段來匹配這個是Intent中比較基本的一個字段,也比較簡單,就是一個字串,如果相等就匹配成功,否則證明還沒找到目標。但要註意,如果IntentFilter沒有指定Action,那麼它不會匹配到任何的隱式Intent,它隻能被顯式的Intent匹配上。反過來,如果Intent自己沒有指定Action,那麼它能匹配上含有任何Action的IntentFilter,但不能匹配上沒有指定Action的IntentFilter。對於Action,平時要註意拼寫錯誤,因為在AndroidManifest文件中聲明Action都是字串,並且在編譯時不會做檢查,運行時,如果Action拼錯瞭導致匹配不上,要麼是程序不能正常工作,要麼會有異常拋出。

 

2. 通過Category字段來匹配對於Activity來講,如果想處理隱式Intent,並且除瞭Intent.ACTION_MAIN以外,必須指定Category為DEFAULT,否則不會被匹配到。因為Context.startActivity()和Context.startActivityForResult()會自動加上DEFAULT Category。其他情況,Service和BroadcastReceiver則不會,對於Service和BroadcastReceiver,如果Intent中沒有指定Category,那麼在其IntentFilter中也不必指定。

 

3. 通過Data字段來匹配這個相對來講比較復雜,通常Data包含uri, scheme(content, file, http)和type(mimeType)對於Intent來講有二個方法:

 

 

Intent.setData(Uri); //一個Uri,Scheme包含在其中 

Intent.setType(String); //指定MimeType,比如'image/jpeg', 'audio/mpeg'等 

Intent.setDataAndType(Uri, String); //上面二個方法的簡便調用方式,一起搞進去 

對於IntentFilter來講,需要設置的是Scheme和Type,Scheme是對Uri的限制,目標需要限制Scheme是因為Scheme能告訴目錄能從哪裡拿到Uri所指向的文件,Type是MimeType對類型的限制。

 

<intent-filter> 

     <action android:name="android.intent.action.SEND" /> 

     <category android:name="android.intent.category.DEFAULT" /> 

     <data android:scheme="content" android:mimeType="image/*" /> 

</intent-filter> 

Data匹配時的規則一共有四條:

a.如果Intent沒有指定Data相關的字段,隻能匹配上沒有指定Data的IntentFilter。也就是說如果一個Intent沒有指定任何的Data(Uri和Type),它隻能匹配到沒有指定任何Data(Scheme和Type)的IntentFilter。

b.如果一個Intent隻指定瞭Uri但是沒有Type(並且Type也不能夠從Uri中分析出)隻能匹配到僅指定瞭相應Scheme且沒有指定Type的IntentFilter。實際的例子有如果一個Intent是想要發郵件,或是打電話,它們的Intent是類似這樣的:"mailto:someone@sb.com"和"tel:1234567"。換句話說,這些Uri本身就是數據,而不再是一個指向數據的地址。比如:Phone中的Dialer就有如下的IntentFilter:

 

 

<intent-filter> 

    <action android:name="android.intent.action.CALL" /> 

    <category android:name="android.intent.category.DEFAULT" /> 

    <data android:scheme="tel" /> 

</intent-filter> 

 

再如,要處理SD狀態變化的IntentFilter:

 

 

<intent-filter> 

      <action android:name="android.intent.action.MEDIA_MOUNTED"/> 

      <action android:name="android.intent.action.MEDIA_UNMOUNTED"/> 

      <action android:name="android.intent.action.MEDIA_SHARED"/> 

      <action android:name="android.intent.action.MEDIA_REMOVED"/> 

      <action android:name="android.intent.action.MEDIA_EJECT"/> 

      <category android:name="android.intent.category.DEFAULT" />        

      <data android:scheme="file" /> 

</intent-filter> 

 

再如,要處理Package狀態變化的IntentFilter:

 

 

<intent-filter> 

      <action android:name="android.intent.action.PACKAGE_ADDED"/> 

      <action android:name="android.intent.action.PACKAGE_REMOVED"/> 

      <action android:name="android.intent.action.PACKAGE_DATA_CLEARED"/> 

      <category android:name="android.intent.category.DEFAULT" /> 

      <data android:scheme="package" /> 

<intent-filter> 

 

但是註意,對於想對數據進行操作的Intent,最好不要隻指定Uri,而不指定類型。因為如果這樣做通常會匹配到一大堆

c. 如果一個Intent隻指定瞭Type,但是沒有指定Uri,它隻能匹配到隻指定瞭相應Type且沒有指定Scheme的IntentFitler

d. 如果一個Intent即有Uri又有Type,那麼它會匹配上:1).Uri和Type都匹配的IntentFilter;2).首先Type要匹配,另外如果Intent的Uri是content:或file:,且IntentFilter沒有指定Scheme的IntentFilter。因為對於Android來講content和file這二種Scheme是系統最常見也是用的最多的,所以就當成缺省值來對待。

另外需要註意,Type,因為是MimeType,所以是允許使用通配符的,比如'image/*',能匹配上所有以'image'為開頭的類型,也說是說能匹配上所有的圖像。

根據Data匹配的例子

假如系統中有四個Activity,A的IntentFilter是這樣子的:

 

 <activity …> 

         <intent-filter> 

             <action android:name="android.intent.action.SEND" /> 

             <category android:name="android.intent.category.DEFAULT" /> 

             <data android:scheme="content" android:mimeType="image/*" /> 

         </intent-filter> 

</activity> 

這表明A可以發送一切圖片類型,並且內容必須是由ContentProvider提供的,也就是Uri必須是以"content://"開頭的

而另外一個Activity B是這樣子聲明的:

 

 <activity …> 

         <intent-filter> 

             <action android:name="android.intent.action.SEND" /> 

             <category android:name="android.intent.category.DEFAULT" /> 

             <data android:scheme="file" android:mimeType="image/*" /> 

         </intent-filter> 

</activity> 

這表明B可以發送一切圖片,但內容必須是單獨的一個文件,也就是Uri必須是由"file://"開頭的

還有一個C是這樣子聲明的:

 

<activity …> 

        <intent-filter> 

            <action android:name="android.intent.action.SEND" /> 

            <category android:name="android.intent.category.DEFAULT" /> 

        </intent-filter> 

lt;/activity> 

這表明C隻能接收那些沒有指定任何Uri和Type的Action是SEND的Intent。

而D是這樣子聲明的:

 

 <activity …> 

         <intent-filter> 

             <action android:name="android.intent.action.SEND" /> 

             <category android:name="android.intent.category.DEFAULT" /> 

             <data android:mimeType="image/*" /> 

         </intent-filter> 

</activity> 

這表明D可以發送一切圖片,無論是數據庫內的(content),還是單獨的文件(file)。

如果一個Intent是這樣寫的:

 

Intent share = new Intent(Intent.ACTION_SEND); 

startActivity(share); 

那麼它隻能匹配C,因為C沒有指定數據和類型,Action是SEND,根據規則a,它隻能匹配Activity A。但如果給Intent加上額外的條件

 

share.setDataAndType(uri,"image/jpeg"); 

那麼如果uri是數據庫內容,它會匹配到A,如果它是一個文件,會匹配到B。但無論是content還是file都會匹配到D,因為它能處理以任何形式存儲的圖片。但始終不會匹配到C,因為C沒有聲明Data字段,所以不會匹配上。

所以,通常想把組件作為系統公用接口時都是這樣子來寫:

 

<activity …> 

        <intent-filter> 

            <!– implement public actions such as View, Edit, Pick or Send –> 

            <action android:name="android.intent.action.SEND" /> 

            <!– never forget default category, otherwise your activity never receives intents –> 

            <category android:name="android.intent.category.DEFAULT" /> 

            <!– specify mimeType to constrain data type, receive data from both content provider and file –> 

            <data android:mimeType="image/*" /> 

            <!– specify scheme to constrain data source, if necessary –> 

            <data android:shceme="http" /> 

        </intent-filter> 

lt;/activity> 

Intent和IntentFilter對於組件Activity來講註意事項比較多,但是對於Service和BroadcastReceiver來說就沒有那麼多的註意事項瞭,因為對於Service和BroadcastReceiver通常都不用設置Category和Data。但也有例外,比如前面所講到的SD相關廣播和應用程序安裝相關廣播。

另外要註意,如果使用Context.startActivity()或Context.startActivityForResult(),Context.bindService()和Context.startService(),如果系統沒有為Intent匹配到目標Activity和Service那麼會有RuntimeException(ActivityNotFoundException)拋出;如果有多個目標同時匹配,會以列表的方式來讓用戶選擇使用哪個。

使用IntentFilter匹配來進行查詢可用的組件

Intent和IntentFilter不但可以用來進行組件復用,還可以用於查詢系統內都有哪裡組件能做哪些事情。比如Launcher上面會列出很多的應用,其實這種說法不準確,應該是上面列出瞭所有的能啟動一個應用的組件(比如,Dialer和Contacts同屬於一個應用程序Contacts中,但是在Launcher裡面卻有二個,一個是Dialer一個是Contacts。那麼Launcher是如何做到的呢?它不可能是去檢查系統文件,看看哪些應用程序文件存在,然後再列出來。它是通過查詢Intent的方式,把所有含有"android.intent.action.MAIN"和"android.intent.category. LAUNCHER"的Activity的相關信息都取出來,然後列出它們的名稱和Icon。同樣,我們也可這樣來獲得具體相應特征的組件,具體的請參考SDK中的一篇文章(Resources->Articles->Can I Use this Intent?),講的很詳細,且有Sample Code。

發佈留言