若有錯漏請不吝拍磚指正,轉載請註明出處,很是感謝java
有一個問題,在網上被頻繁的問到,就是爲何自定義的Receiver老是沒法接收到SD卡插拔的事件。android
而此問題大部分狀況下能夠經過增長一句代碼解決: filter.addDataScheme("file"); // filter是IntentFilter對象app
那麼爲何增長這句代碼就能夠解決了呢?這個問題儘管有人問到,可是卻沒有太好的回答。ide
多是由於對於精通IntentFilter策略的高手們來講,這根本算不上問題,是一個再明顯不過的事實而已。函數
而對於不太瞭解IntentFilter策略的咱們初學者來講,這個問題又暫時有點太難以理解吧。學習
所以,本文試着經過對android的事件過濾策略進行介紹和分析,結合示例程序進行驗證,測試
來解答此問題,並淺顯的介紹android事件過濾策略。插件
首先咱們建立一個android工程起名爲SdCardTester,做爲示例程序。日誌
爲了方便在後續步驟中模擬SD卡插拔,建議將目標平臺設定爲2.3版本,使用2.3版本的模擬器。component
此外務必注意,運行此示例程序的AVD模擬器須要增長SD卡功能支持。
而後爲SdCardTester類增長一個BroadcastReceiver類型的成員變量 mReceiver。
在onCreate中,使用匿名類的技巧,爲 mReceiver 賦值一個BroadcastReceiver子類實例。
注意代碼中重寫的onRecevie函數裏只有一句代碼,用於記錄日誌。以證實咱們確實收到了事件。
而後建立一個IntentFilter,用於過濾SD卡插拔事件。
最後把咱們自定義的Receiver和編寫好的IntentFilter註冊到系統中
最後的最後不要忘了在onDestory中註銷咱們的自定義Receiver,
至此完成了示例程序的代碼編寫,SdCardTester的完整代碼以下:
運行SdCardTester,等看到hello World 字樣,說明onCreate完成,也就意味着咱們自定義的Receiver也已經啓動了。
以後,不要用BACK退出,按HOME按鈕返回主屏幕,(也就是要保證咱們的自定義Receiver仍然在運行)
進入手機設定,選擇Storage (若是系統語言選擇爲中文了是「存儲」),
選擇 Unmount SD card / mount SD card 選項,就能夠模擬SD卡插拔的動做了。以下圖所示:
若是咱們的自定義Receiver收到了相關事件,按照代碼實現,就會使用 TAG "myLoger" 輸出日誌到logCat。
所以咱們經過觀察logCat輸出肯定是否有相關事件發生,在cmd窗口中運行 " adb logcat -s myLoger:i " ,便可持續監控日誌輸出。
或者使用Eclipse ADT插件增長的DDMS視圖,在logCat 的View中增長一個filter,以下圖所示:
注意,按照咱們當前的代碼實現,不管咱們如何插拔SD卡,都不會看到日誌窗口監控到事件發生。
OK,咱們如今增長那行神奇的代碼:「 filter.addDataScheme("file"); 」,
並在onCreate中增長一個日誌輸出監控程序進入。修改以後的代碼以下:
修改完成再次運行程序,不要用BACK退出,按HOME按鈕返回主屏幕,進入Setting模擬SD卡插拔,
而後觀察logCat日誌監控窗口,神奇的事情發生了,咱們能夠接收到事件了!以下圖所示:
那麼,爲何加上 filter.addDataScheme("file"); 就能夠了呢?
爲了解決這個問題,咱們要先學習Intent的分類。Intent分爲兩大類,顯式和隱式。
顯式事件,就是指經過 component Name 屬性,明確指定了目標組件的事件。
好比咱們新建一個Intent,指名道姓的說,此事件用於啓動名爲"com.silenceburn.XXXX」的Activity,那麼這就是一個顯式事件。
隱式事件,就是指沒有 component Name 屬性,沒有明確指定目標組件的事件。
好比系統向全部監控通話狀況的程序發送的「來電話了!」的事件,因爲系統不肯定誰會處理這個事件,
所以系統不會明確指定目標組件,也就是說沒有目標組件,那麼這就是個隱式的事件。
此處只是簡介顯式和隱式事件,更精確詳細的描述請查閱SDK文檔,
咱們只須要記住一點,兩種事件的最大區別是 component Name 屬性是否爲空。
系統在傳送顯式事件時很是方便,由於若是把Intent比做一封信,那麼component Name就是一個詳細的收件人地址,
系統能夠精確的把顯式事件送達目標組件。
而傳送隱式事件時,就比較麻煩了。由於這封信的信封上,沒有寫收信地址!
那怎麼辦呢?系統作了一個艱難的決定,就是把信拆開看看。經過信件內容裏面的線索,去尋找合適的收件人。
好比信中的線索描述到:「收信人是男性,快30歲了,未婚,喜歡玩遊戲」,那麼系統就在小區裏面去找這樣的人。
很是值得慶幸的事情是,這個小區的人素質很是高,每戶人家都寫了點自我介紹在門口,
好比張三寫道:「我是男性,90後,未婚,喜歡玩遊戲」,李四寫道:「我是女性,快30歲了,未婚,喜歡逛街」等等等等。
有了每戶人家的自我介紹,系統就能很快的定位真正的收件人了!
上面是一個類比的例子,不過android系統處理隱式事件的策略,基本上就是上述這種模式了。
首先系統會經過觀察Intent的內容(打開信件看內容),取得匹配線索,系統所需的線索是以下三種 :
action
data (both URI and data type)
category
其次,系統中每一個組件,若是想收取隱式事件,則必須聲明本身的IntentFilter(自我介紹,我對什麼樣的信件感興趣)。
至於怎麼寫IntentFilter,已經至關明瞭了,那就是應該是這樣寫:
"我是組件XXXX,我想要接收這樣的隱式事件:它的ACTION必須是 XXX,它的 category 必須是 YYYY ,它包含的data必須是ZZZZ "
若是組件不聲明IntentFilter,那麼全部的隱式事件都不會發送給該組件。(注意,這並不影響向該組件發送顯式事件。)
對於系統中發生的每一個隱式事件,系統都會嘗試將 action, data , category 和系統中各個組件聲明的 IntentFilter 去進行匹配,
以找到合適的接收者。
上述是android系統的事件過濾策略的簡單原理,實際狀況遠比這要複雜,考慮本文的目的,此處再也不展開,SDK文檔中都有詳盡描述。
由上節可知,對於顯式事件,系統能夠精確送達。對於隱式事件,系統分析事件的 action, data , category 內容,
並和各個組件聲明的IntentFilter進行匹配,找出匹配的組件進行送達。
所以SD卡插拔事件可否被咱們自定義的Recevier收到就取決於以下子問題了:
1. SD卡插拔事件是顯式事件,仍是隱式事件
2. SD卡插拔事件的action, data , category 的內容是什麼
3. 咱們自定義的Receiver組件的IntentFilter是如何聲明的
爲了解決上述3個問題,咱們修改一下代碼,將SD卡插拔事件的內容打印到logcat中進行觀察。
修改後的代碼以下:(注意這裏咱們要添加好 filter.addDataScheme("file"); 以確保事件能夠被接收到)
OK,讓咱們再次運行程序,經過Setting模擬插拔SD卡,觀察logCat的輸出狀況。
下圖是掛載SD卡時的日誌輸出狀況:
經過日誌輸出咱們能夠得知掛載SD卡事件的 Componet 是null ,所以它是一個隱式事件。
所以可否送達,須要看事件的 action, data , category 和 IntentFilter是否匹配。
它的ACTION是 android.intent.action.MEDIA_MOUNTED,
和咱們定義的IntentFilter的 filter.addAction(Intent.ACTION_MEDIA_MOUNTED); 語句相匹配。
所以action部分是匹配的。
它的Categories是null,而咱們定義的 IntentFilter 也沒有使用addCategory方法增長category定義,
null == null,所以 categories也是匹配的。
action, data , category 中的兩個要素已經匹配,那麼可否匹配成功的關鍵,就是看data是否匹配了。
首先務必認識到,data是一個相對複雜的要素。 data由URI來描述和定位,URI由三部分組成,
scheme://host:port/path 模式://主機:端口/路徑
例如咱們截獲的掛載SD卡事件,它的data的URI是 file:///mnt/sdcard
其中模式部分是 file , 主機:端口部分是空的, path部分是 mnt/sdcard
此外在事件中,還能夠設置data的MIME類型,做爲事件的datatype屬性。
綜上所述,data是一個較複雜的要素,所以其匹配規則也格外複雜,
首先明確一個匹配原則,就是對於URI的匹配,只比較filter中聲明的部分。
部分匹配原則:只要filter中聲明的部分匹配成功,就認爲整個URI匹配成功。
舉例來講, content://com.silenceburn.SdCardTester:1000/mydata/private/
和filter定義爲 content://com.silenceburn.SdCardTester:1000/ 是能夠匹配的。
注意filter中並無定義path部分,可是依然能夠匹配成功,由於filter不聲明的部分不進行比較。
換句話講,任何符合content://com.silenceburn.SdCardTester:1000/的事件,不管path是什麼,均可以匹配成功。
接下來是真正的data部分的,也就是URI的匹配規則以下:
1. 若是data的URI和datatype爲空,則 filter 的URI和type也必須爲空,才能匹配成功
2. 若是data的URI不爲空,可是datatype爲空,則 filter 必須定義URI並匹配成功,且type爲空,才能匹配成功
3. 若是data的URI爲空,可是datatype不爲空,則 filter 必須URI爲空,定義type並匹配成功,才能匹配成功
4. 若是data的URI和data都不爲空,則 filter 的URI和type都必須定義並匹配成功,才能匹配成功。
對於URI部分,有一個特殊處理,就是即便filter沒有定義URI,content和file兩種URI也做爲既存的URI存在。
(舉個例子,對於 content 和 file 兩種模式的data,只要filter定義的datatype能夠和事件匹配,就認爲匹配成功,
filter不須要顯式的增長 content 和 file 兩種模式,這兩種模式內置支持 )
有了規則,有了對SD卡插拔事件的內容的分析,咱們就能夠按圖索驥照章辦事了。
首先如第6節所述,SD卡插拔是一個隱式事件,並且 action 和 category 部分和咱們的 filter 都可以匹配成功。
其data部分的URI爲 file:///mnt/sdcard ,datatype爲 null ,所以應用第6節比較規則中的 2 號規則:
2. 若是data的URI不爲空,可是datatype爲空,則 filter 必須定義URI並匹配成功,且type爲空,才能匹配成功
咱們的filter中沒有使用 addtype 方法 ,所以 filter 的type爲空, datatype部分匹配成功。
data的URI爲file:///mnt/sdcard ,咱們使用 filter.addDataScheme("file"); 語句定義 schema 爲 file,
根據部分匹配規則,data匹配成功。
至此,整個事件匹配成功,至此,咱們就明白了文初的問題,爲何必須添加 filter.addDataScheme("file"); 語句才能收到事件!
咱們能夠嘗試把 filter.addDataScheme("file"); 後面增長語句
依然能夠匹配成功,收到SD卡插拔事件。由於這樣就組成了一個URI的徹底匹配。
咱們能夠嘗試把給 filter 增長 datatype 屬性,
這樣就沒法匹配成功了。由於SD卡插拔事件的datatype是null,
而咱們定義的filter的datatype是MIME"text/*" 。
至此文初的問題解析完畢。老生常談,事實上android平臺的intentFilter處理機制遠遠複雜於本文所述範圍。
特別是本文對action和category兩種要素的討論很是少幾乎沒有,實際上這兩種要素的處理也是比較複雜的。
本文只是冰山一角的講述了了一些基本原理和基本準則。
IntentFilter機制做爲android平臺的重要基礎知識之一,咱們你們要一塊兒繼續努力學習,和你們共勉 :)