鍍金池/ 教程/ iOS/ Android Intents
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

Android Intents

簡介

說起 Android,最大的特點(diǎn)莫過于運(yùn)行其平臺上的應(yīng)用可以很容易的啟動別的應(yīng)用以及互相之間分享數(shù)據(jù)?;厥?iOS 1.0 時代,應(yīng)用之間是完全隔離的,無法進(jìn)行通信(至少非 Apple 應(yīng)用之間是這樣的),甚至到了 iOS SDK 面世之時,這種狀況也沒有改變。

iOS 6 之前的系統(tǒng),若要在編寫郵件過程中直接加入照片或視頻是件很麻煩的事。iOS 6 發(fā)布以后,這項功能才得到根本性的改善。但是在 Android 的世界里,自發(fā)布的第一天,這種功能就是天生攜帶的。

類似的系統(tǒng)平臺層面的差異還有許多。比如有這樣一個場景:拍一張照片,然后用某個圖片處理 app 里編輯一下,接著將照片分享到 Instagram。

注意:這里只是列舉個別細(xì)節(jié)。

iOS的做法是:

  1. 打開系統(tǒng)拍照應(yīng)用拍張照片。
  2. 回到主界面,找到圖片編輯應(yīng)用,啟動應(yīng)用,選擇已存在照片,從系統(tǒng)相冊里選取照片,然后編輯。
  3. 如果圖片編輯應(yīng)用恰好支持直接分享 Instagram 又在分享列表中,就此完成任務(wù)。
  4. 如果第 3 點(diǎn)條件不滿足,那就得先把編輯好的照片保存到系統(tǒng)相冊。
  5. 再一次回到主界面,找到 Instagram 然后打開它...
  6. 導(dǎo)入之前編輯保存的照片,然后分享給 Instagram 上的潮友們。;)

至于 Android,就簡單得多了:

  1. 打開拍照應(yīng)用,拍張照片。
  2. 向右滑查看“相冊”,然后點(diǎn)擊分享按鈕。選擇想要使用的圖片編輯應(yīng)用,然后直接編輯。
  3. 如果圖片編輯應(yīng)用支持直接分享(我還從來沒見過哪個圖片處理應(yīng)用不支持直接分享的),點(diǎn)擊分享然后選擇 Instagram。假如這個應(yīng)用不支持分享,直接卸載算了,換個靠譜的應(yīng)用來處理,或者干脆用系統(tǒng)集成的圖片編輯器。KitKat 之后的系統(tǒng)內(nèi)建編輯器已經(jīng)相當(dāng)酷炫。

需要說明的是,對于那些提供分享功能的 iOS 應(yīng)用來說,其處理流程和 Android 基本是一致的。根本性的差別是,如果應(yīng)用本身不支持分享那就斷絕了分享給其他應(yīng)用的道路。與 Facebook 和 Twitter 一樣,Instagram 這類熱門應(yīng)用還好,但是除此之外還有大量的應(yīng)用,基本上沒什么應(yīng)用會集成針對它們的分享服務(wù)。

比如說你想把 Instagram 里的某張照片分享到 Path 上面(我知道,Path 比較小眾,但是...)。如果是 Android 系統(tǒng),直接從 chooser dialog (選擇對話框) 中選擇 Path 即可。就是這么簡單。

還是說回正題,Intents。

什么是 Android Intent?

在英語詞典里 Intent 的定義是:

noun (名詞)
intention or purpose (意圖、目的)

來自 Android 官方文檔的說明是,Intent 對象主要解決 Androi d應(yīng)用各項組件之間的通訊。事實上,Intent 就是對將要執(zhí)行的操作的一種抽象描述。

看起來很簡單,實際上 Intent 的意義遠(yuǎn)不止于此。在 Android 世界中,Intent 幾乎隨處可見,無論你開發(fā)的 app 多么簡單,也離不開 Intent;小到一個 Hello World 應(yīng)用也是要使用 Intent 的。因為 Intent 最基礎(chǔ)最常見的用法就是啟動 Activity。1

如何理解 Activities 和 Fragments

在iOS中,與 Activity 相比較,最相似的東西就是 UIViewController 了。切莫在 Android 中尋找 ApplicationDelegate 的等價物,因為沒有。也許只有 Application 類稍微貼近于 ApplicationDelegate ,但是從架構(gòu)上看,它們有著本質(zhì)的區(qū)別。

由于廠商把手機(jī)屏幕越做越大,一個全新的概念 Fragments2(碎片)隨之而生。最典型的例子就是新聞閱讀類應(yīng)用。在小屏幕的手機(jī)上,一般用戶只能先看到文章列表。選中一篇文章后,才會全屏顯示文章內(nèi)容。

沒有 Fragments 的時候,開發(fā)者需要創(chuàng)建兩個 activities(一個用于展示文章列表,另一個用于全屏展示文章詳情),然后在兩者來回切換。

在出現(xiàn)大屏幕的平板之前,這么都做沒什么問題。因為原則上,同一時間只有一個 activity 對用戶可見,但自從 Android 團(tuán)隊引入了 Fragments,一個宿主 Activity 就可以同時展示多個 Fragments 了。

現(xiàn)在,完全可以用一個 Activity 嵌入兩個 Fragments 的方式來替代先前使用兩個不同 Activities 的做法。一個 Fragment 用來展示文章列表,另一個用來展示詳情。對于小屏幕的手機(jī),可以用兩個 Fragments 交替顯示文章列表和詳情。如果是平板設(shè)備,宿主 Activity 會同時顯示兩個 Fragments 的內(nèi)容。類似的東西可以想像一下 iPad 中的郵件應(yīng)用,在同一屏中,左邊是收件箱,右邊是郵件列表。

啟動 Activities

Intents 最常見的用法就是用來啟動 activities(以及在 activities 之間傳遞數(shù)據(jù))。Intent 通過定義兩個 activities 之間將要執(zhí)行的動作從而將它們粘合起來。

然而啟動一個 Activity 并不簡單。Android 中有一個叫做 ActivityManager (活動管理器)的系統(tǒng)組建負(fù)責(zé)創(chuàng)建、銷毀和管理 activities。這里不去過多探討 ActivityManager 的細(xì)節(jié),但是需要指出的是它承擔(dān)全程監(jiān)視已啟動的 activities 以及在系統(tǒng)內(nèi)發(fā)送廣播通知的職責(zé),比如說,啟動過程結(jié)束這件事就是由 ActivityManager 來向安卓系統(tǒng)的其他部分發(fā)放通知的。

ActivityManager 是安卓系統(tǒng)的一個極重要的部分,同時它依靠 Intents 來完成大部分工作。

那么 Android 系統(tǒng)到底是如何利用 Intent 來啟動 Activity 的呢?

如果你仔細(xì)挖掘一下 Activity 的類結(jié)構(gòu)就會發(fā)現(xiàn):它繼承自 Context,里面恰好有個抽象方法 startActivity(),其定義如下:

public abstract void startActivity(Intent intent, Bundle options);

Activity 實現(xiàn)了這個抽象方法。也就是說只要傳遞了正確的 Intent,可以對任意一個 Activity 執(zhí)行啟動操作。

比如說我們要啟動一個名為 ImageActivityActivity。

其中 Intent 的構(gòu)造方法是這樣的:

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

需要傳遞參數(shù) Context(注意,可以認(rèn)為每一個 Activity 都是一個有效的 Context)和 Class 類。

接下來:

Intent i = new Intent(this, ImageActivity.class);
startActivity(i);

這之后會觸發(fā)一系列調(diào)用,如無意外,最終會成功啟動一個新的 Activity,當(dāng)前的 Activity 會進(jìn)入 paused(暫停)或者 stopped(停止)狀態(tài)。

Intents 還可以用來在 Activities 之間傳遞數(shù)據(jù),比如我們將信息放入 Extras 來傳遞:

Intent i = new Intent(this, ImageActivity.class);
i.putExtra("A_BOOLEAN_EXTRA", true); //boolean extra
i.putExtra("AN_INTEGER_EXTRA", 3); //integer extra
i.putExtra("A_STRING_EXTRA", "three"); //integer extra
startActivity(i);

extras 存儲在 Android 的 Bundle3中,Bundle 在這里可以被看做是一個可序列化的容器。

這樣 ImageActivity 就可以通過 Intent 來接收信息,可以通過如下方式將信息取出:

 int value = getIntent().getIntExtra("AN_INTEGER_EXTRA", 0); //名稱,默認(rèn)值

上面就是如何在 Activities 之間傳簡單值。當(dāng)然也可以傳序列化對象。

假如一個對象已實現(xiàn)序列化接口 Serializable。接下來可以這么做:

YourComplexObject obj = new YourComplexObject();
Intent i = new Intent(this, ImageActivity.class);
i.putSerializable("SOME_FANCY_NAME", obj); //使用接收序列化對象的方法
startActivity(i);

其它的 Activity 也要使用相應(yīng)的序列化取值方法獲取值:

YourComplexObject obj = (YourComplexObject) getIntent().getSerializableExtra("SOME_FANCY_NAME");

特別說明,從 Intent 取值的時候請記得判空

if (getIntent() != null ) {
         //確認(rèn)Intent非空后,可以進(jìn)行諸如從extras取值什么的…
}

在 Java 的世界中對空指針很敏感。所以要多加防范。;)

使用 startActivity() 啟動了新的 activity 后,當(dāng)前的 activity 會依次進(jìn)入 paused 和 stopped 狀態(tài),然后進(jìn)入任務(wù)堆棧,當(dāng)用戶點(diǎn)擊 back 按鈕后,activity 會再次恢復(fù)激活。正常情況下,這一系列流程沒什么問題,不過還是可以通過向 Intent 傳遞一些 Flags(標(biāo)識)來通知 ActivityManager 去改變既定行為。

由于這是一個很大很復(fù)雜的話題,此處就不做過多的展開了??梢詤⒁娢臋n 任務(wù)和返回棧的官方文檔來了解 Intent Flags。

下面看看 Intents 除了啟動 Activity 還能做些什么。

Intents 還有兩個重要職責(zé):

  • 啟動 Service4(或向其發(fā)送指令)。
  • 發(fā)Broadcast(廣播)。

啟動服務(wù)

由于 Activities 不能在后臺運(yùn)行(因為在后臺它們會進(jìn)入 paused 態(tài),stopped 態(tài),甚至是 destroyed 銷毀狀態(tài)),如果想要執(zhí)行的后臺進(jìn)程不需要 UI,可以使用 Service (服務(wù))作為替代方案。Services 本身也是個很大的話題,簡單的說它就是:沒有界面或 UI 不可見的運(yùn)行在后臺的任務(wù)。

由于 Services 如無特殊處理是運(yùn)行在UI線程上的,所以當(dāng)系統(tǒng)內(nèi)存緊張時,Services 極有可能被銷毀。也就是說,如果 Services 所要執(zhí)行的是一個耗時操作,那么就應(yīng)該為 Services 開辟單獨(dú)的線程,一般都是通過 AsyncTask 來創(chuàng)建。比如一個 Service 要執(zhí)行媒體播放任務(wù),可以通過申請 Foreground(前臺)服務(wù)狀態(tài)來強(qiáng)制在通知欄中一直顯示一個通知,給用戶展示當(dāng)前服務(wù)在做些什么。應(yīng)用也可以取消前臺狀態(tài)(通知欄上的相應(yīng)狀態(tài)通知也會隨之消失),但是這么做的話 Service 就失去了較高的狀態(tài)優(yōu)先級。

Services 機(jī)制是非常強(qiáng)大的,它也是 Android “多任務(wù)”處理的基礎(chǔ),而在早前,它被認(rèn)為是影響電池用量的關(guān)鍵因素。其實早在 iOS 還未支持多任務(wù)的時代,Android 已經(jīng)在自如的操縱多任務(wù)處理了。使用正確的話,Services 是平臺必不可少的重要組成部分。

在以前,有一個很爭議的問題,就是 Service 可以在沒有任何通知的情況下轉(zhuǎn)入前臺運(yùn)行。也就是說在用戶不知情的情況下,后臺可能會啟動大量的服務(wù)來執(zhí)行各種各樣的任務(wù)。自 Android 4.0 (Ice Cream Sandwich) 之后,Google終于修復(fù)了這個“隱形”通知的問題,讓無法殺掉進(jìn)程且在后臺靜默運(yùn)行的應(yīng)用程序在通知欄上“顯形”,用戶甚至可以從通知欄中切換到應(yīng)用內(nèi)(然后殺掉應(yīng)用)。雖然現(xiàn)在 Android 設(shè)備的續(xù)航還遠(yuǎn)不及 iOS 產(chǎn)品,但是至少后臺靜默 Services 已經(jīng)不再是耗電的主因了。;)

IntentsServices 是怎么協(xié)作的呢?

首先需要一個 Intent 來啟動 Service。而 Service 啟動后,只要其處于非 stopped 狀態(tài),就可以持續(xù)地向它發(fā)送指令,直到它被停止(在這種情況下它將會重新啟動)。

在某個 Activity 中啟動服務(wù):

Intent i = new Intent(this, YourService.class);
i.setAction("SOME_COMMAND");
startService(i);

接下來程序執(zhí)行情況取決于當(dāng)下是否第一次啟動服務(wù)。如果是,那么服務(wù)就會自然啟動(首先執(zhí)行構(gòu)造方法和 onCreate() 方法)。如果該服務(wù)已經(jīng)啟動過,將會直接調(diào)用 onStartCommand() 方法。

方法的具體定義:public int onStartCommand(Intent intent, int flags, int startId);

此處重點(diǎn)關(guān)注 Intent。由于 flagsstartId 與我們要探討的話題相關(guān)性不大,這里直接忽略不贅述。

之前我們通過 setAction("SOME_COMMAND") 設(shè)置了一個 Action。Service 可以通過 onStartCommand() 來獲取該 action。拿上面的例子來說,可以這么做:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    String action = intent.getAction();
    if (action.equals("SOME_COMMAND")) {
        // SOME_COMMAND 具體事件內(nèi)容)
    }
    return START_NOT_STICKY; // 如果服務(wù)已被殺掉,不要重新啟動服務(wù)
}

如果對 START_NOT_STICKY 感興趣,請參見此安卓文檔有很詳盡的描述。

簡而言之:如果 Service 已經(jīng)被殺掉,不需要重啟。與之相反的是 START_STICKY,這個表示應(yīng)當(dāng)執(zhí)行重啟。

從上面的代碼段可知,能夠從 Intent 中獲取 Action。這就是比較常見的與 Services 的通訊方式。

假設(shè)我們要開發(fā)一個應(yīng)用,將 Youtube 的視頻以流式輸送給 Chromecast (雖然現(xiàn)有的Youtube應(yīng)用已經(jīng)具備這個功能了,但既然是 Android,我們還是希望自己做一個)。

通過一個 Service來實現(xiàn)流式播放,這樣當(dāng)用戶在播放視頻的過程中切換到其它應(yīng)用的時候,播放也不會停止。定義幾種 actions:

ACTION_PLAY, ACTION_PAUSE, ACTION_SKIP.

onStartCommand() 內(nèi),可以通過 switch 或者 if 條件判斷,然后針對每一種情況做相應(yīng)的處理。

理論上,服務(wù)可以隨意命名,但通常情況下會使用常量(稍后會舉例)來命名,良好的命名可以避免和其它應(yīng)用的服務(wù)名產(chǎn)生沖突,比如說使用完整的包名 'com.yourapp.somepackage.yourservice.SOME_ACTION_NAME'。如果將服務(wù)名設(shè)為私有,那么服務(wù)只能和自己的應(yīng)用通訊,否則要是想和其它應(yīng)用通訊則需要將服務(wù)名公開。

發(fā)送和接收廣播

Android 平臺的強(qiáng)大特性之一就是:任何一個應(yīng)用都可以廣播一個 Intent,同時,任意應(yīng)用可以通過定義一個 BroadcastReceiver(廣播接收者)來接收廣播。事實上,Android 本身就是采用這個機(jī)制來向應(yīng)用和系統(tǒng)來發(fā)送事件通知的。比如說,網(wǎng)絡(luò)突然變成不可用狀態(tài),Android 組件就會廣播一個 Intent。如果對此感興趣,可以創(chuàng)建一個 BroadcastReceiver,設(shè)置相應(yīng)的filter(過濾器)來截獲廣播并作出適當(dāng)?shù)奶幚怼?/p>

可以將這個過程解為訂閱一個全局的頻道,并且根據(jù)自己的喜好配置過濾條件,接下來會接收符合條件的廣播信息。另外,若只是想要自己的應(yīng)用接收廣播,需要定義成私有。

繼續(xù)前面的 Youtube 播放服務(wù)的例子,如果在播放的過程中出現(xiàn)了問題,服務(wù)可以發(fā)送一個 Intent 廣播來發(fā)布信息,比如“播放遇到問題,將要停止播放”。

應(yīng)用可以注冊一個 BroadcastReceiver 來監(jiān)聽 Service,以便對收到的廣播做出處理。

下面看一些樣例代碼。

基于上面的例子,你可能會定義一個 Activity 用來展示和播放有關(guān)的信息和操作,比如當(dāng)前的播放進(jìn)度和媒體控制按鈕(播放,暫停,停止等等)。你可能會非常關(guān)注當(dāng)前服務(wù)的狀態(tài);一旦有錯誤發(fā)生,你需要及時知曉(可以向用戶展示錯誤提示信息等等)。

在 activity(或者一個獨(dú)立的 .java 文件)中可以創(chuàng)建一個廣播接收器:

private final class ServiceReceiver extends BroadcastReceiver {
    public IntentFilter intentFilter;
    public ServiceReceiver() {
        super();
        intentFilter = new IntentFilter();
        intentFilter.addAction("ACTION_PLAY");
        intentFilter.addAction("ACTION_STOP");
        intentFilter.addAction("ACTION_ERROR");
    }
    @Override
    public void onReceive(final Context context, final Intent intent) {
        if (intent.getAction().equals("ACTION_ERROR")) {
           // 由于有錯誤發(fā)生,播放停止
        } else if (intent.getAction().equals("ACTION_PLAY")){
           // 播放視頻
        }
        // 等等…
    }
 }

receiver 的實現(xiàn)大概如此。這里需要注意下我們向 IntentFilter 中添加的 Actions。它們分別為 ACTION_PLAY(播放), ACTION_STOP(停止), 和 ACTION_ERROR(錯誤)。

由于我們使用的是 Java,列舉一下 Android 的習(xí)慣用法:

private ServiceReceiver mServiceReceiver; 可以用此法將其定義為 Activity 的成員變量。然后在 onCreate() 方法中對其進(jìn)行實例化,比如:mServiceReceiver = new ServiceReciver();。

當(dāng)然,單單創(chuàng)建這樣的一個對象是不夠的。我們需要在某處進(jìn)行注冊。第一反應(yīng),你可能會認(rèn)為可以在 ActivityonStart() 方法內(nèi)注冊。當(dāng) onStart() 執(zhí)行的時候,意味著用戶可以看到這個 Activity 了。

注冊方法詳情如下(定義在 Context 中):

public abstract Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);

由于 ActivitiesServices 都是 Contexts,所以它們本身都實現(xiàn)了這個方法。這表示它們都可以注冊一個或多個 BroadcastReceivers。

此方法需要參數(shù) BroadcastReceiverIntentFilter。之前已經(jīng)創(chuàng)建好,可直接傳參:

@Override
public void onStart() {
    onStart();
      registerReceiver(mServiceReceiver, mServiceReceiver.intentFilter);
}

請養(yǎng)成良好的 Java / Android 開發(fā)習(xí)慣,當(dāng) Activity 停止的時候,請注銷相應(yīng)的注冊信息:

@Override
public void onStop() {
    super.onStop();
    unregisterReceiver(mServiceReceiver);
}

這種處理本身沒什么問題,但是要提醒大家,一旦用戶離開了當(dāng)前應(yīng)用,將不會再收到廣播。這是由于 Activity即將停止,此處在 onStop() 這里注銷了廣播接收。所以當(dāng)你設(shè)計 BroadcastReceivers 的時候,需要考慮清楚,這種處理方式是否適用。畢竟還有其它不依賴于 Activity 的實現(xiàn)方式可供選擇。

每當(dāng) Service 偵測到錯誤發(fā)生,它都會發(fā)起一個廣播,這樣 BroadcastReceiver 可以在 onReceive() 方法中接收廣播信息。

廣播接收處理也是 Android 中非常重要非常強(qiáng)大非常核心的機(jī)制。

讀到這里,愛思考的讀者們可能會問這些廣播到底可以 全局到什么程度?如何將廣播設(shè)置為私有以及如何限制它們只和其所屬應(yīng)用通訊?

事實上 Intents 有兩類:顯式的 (explicit) 隱式的 (implicit)

所謂顯式 Intent 就是明確指出了目標(biāo)組件名稱的 Intent,由于不清楚其它應(yīng)用的組件名稱,顯式 Intent 一般用于啟動自己應(yīng)用內(nèi)部的組件。隱式 Intent 則表示不清楚目標(biāo)組件的名稱,通過給出一些對想要執(zhí)行的動作的描述來尋找與之匹配的組件(通過定義過濾器來羅列一些條件用于匹配組件),隱式 Intent 常用于啟動其它應(yīng)用的組件。

鑒于之前給出的例子中使用的就是顯式 Intents,這里將重點(diǎn)討論一下隱式 Intents。

我們通過一個簡單的例子來看看隱式 Intents的強(qiáng)大之處。定義過濾器 (filter) 有兩種方式。第一種與 iOS 的自定義 URI 機(jī)制很類似,比如:yourapp://some.example.com。

如果你的設(shè)計是想 Android 和 iOS 都通用,那么別無他法只能使用 URI 策略。如果只是針對 Android 平臺的話,建議盡量使用標(biāo)準(zhǔn) URL 的方式(比如 http://your.domain.com/yourparams)。之所以這么說是因為這與如何看待自定義 URI 方案的利與弊有關(guān),對這個問題不做具體的展開了,總而言之(下文引自 stackoverflow):

為了避免不同實體之間的命名沖突,web 標(biāo)準(zhǔn)要求應(yīng)嚴(yán)格要控制 URI 的命名。而使用自定義 URI 方案與其 web 標(biāo)準(zhǔn)相違背。一旦將自定義 URI 方案部署到互聯(lián)網(wǎng)上,等于直接將方案名稱投入到整個互聯(lián)網(wǎng)的命名空間中去(會又很大的命名沖突可能性),所以應(yīng)嚴(yán)格遵守相應(yīng)的標(biāo)準(zhǔn)。

來源:StackOverflow

上述問題暫且放一邊,下面看兩個例子,一個是用標(biāo)準(zhǔn) URL 來實現(xiàn)之前 YouTube app,另一個是在我們自己的 app 中采用自定義 URI方案。

因為每個Android都有配置文件AndroidManifest.xml,可以在其中定義Activities,Services,BroadcastReceivers,versions(版本信息),Intent filter等描述信息,所以實現(xiàn)起來比較簡單。詳情見文檔

Intent 過濾器的本質(zhì)是系統(tǒng)依照過濾條件檢索當(dāng)前已安裝的所有應(yīng)用,看看有哪些應(yīng)用可以處理指定的 URI。

如果某個 app 剛好匹配且是唯一能夠匹配的 app,就會自動打開這個 app。否則的話,可以看到類似這樣的一個選擇對話框:

http://wiki.jikexueyuan.com/project/objc/images/11-9.jpg" alt="" />

為什么 Youtube 的官方應(yīng)用會出現(xiàn)在清單上呢?

我只是在 Facebook 的應(yīng)用里點(diǎn)了一個 Youtube 的鏈接而已。為什么 Android 會知道我點(diǎn)的是 Youtube 的鏈接?這其中有什么玄機(jī)?

假設(shè)我們打開 Youtbube 應(yīng)用的 AndroidManifest.xml,我們應(yīng)該能看到類似如下的配置:

1 <activity android:name=".YouTubeActivity">
2     <intent-filter>
3        <action android:name="android.intent.action.VIEW" />
4       <category android:name="android.intent.category.DEFAULT" />
5         <category android:name="android.intent.category.BROWSABLE" />
6       <data
7        android:scheme="http"
8        android:host="www.youtube.com"
9        android:pathPrefix="/" />
10   </intent-filter>
11 </activity>

接下來我們會逐行解釋一下這段XML信息。

第 1 行是聲明 activity(Android 中的每個 activity 都必須在配置文件中聲明,而過濾器則不是必須的)。

第 2 行聲明了 action。此處的 VIEW 是最常用的action,它表示會向用戶展示數(shù)據(jù)。因為還存在一些受保護(hù)的只能用于系統(tǒng)級傳輸?shù)?action。

第 4-5 行聲明了類別 (categories)。隱式 Intents 要求至少有一個 action 和一個 category。categories 里主要定義 Intent 所要執(zhí)行的 Action 的更多細(xì)節(jié)。在解析 Intent 的時候,只有滿足 categories 中全部描述條件的 activities 才會被使用。Android 把所有傳給 startActivity() 的隱式 Intent 當(dāng)作它們包含至少一個 category android.intent.category.DEFAULT (CATEGORY_DEFAULT 常量),想要接收隱式 Intent 的 Activity 必須在它們的 Intent Filter 中配置 android.intent.category.DEFAULT。

android.intent.category.BROWSABLE 是另一個敏感配置:

能通過瀏覽器安全調(diào)用的 Activity 必須支持這個 category。比如,用戶從正在瀏覽的網(wǎng)頁或者文本中點(diǎn)擊了一個 e-mail 鏈接,接下來生成執(zhí)行這個 link 的 Intent 會含有 BROWSABLE categroy 描述,所以只有支持這個 category 的 activities 才會有可能被匹配到。一旦承諾支持這個 category,在被 Intent 匹配調(diào)用后,必須保證沒有惡意的行為或內(nèi)容(至少是在用戶不知情的情況下不可以有)。

來源:Android Documentation(官方文檔)

這個點(diǎn)很關(guān)鍵,Android 通過它構(gòu)建了一種機(jī)制,允許應(yīng)用去響應(yīng)任何的鏈接。利用這個機(jī)制你完全可以構(gòu)建自己的瀏覽器去處理任何 URL 的請求,如果用戶喜歡的話完全可以將你的瀏覽器設(shè)置成默認(rèn)瀏覽器。

第 6-9 行聲明了所要操作的數(shù)據(jù)類型。在本例中,我們使用 scheme(方案/策略)和 host(主機(jī))來進(jìn)行過濾,所以任何以 http://www.youtube.com/ 開頭的鏈接均可處理,哪怕是在 web 瀏覽器里點(diǎn)擊鏈接。

在 Youtube 應(yīng)用內(nèi)的 AndroidManifest.xml 里配置以上信息后,每當(dāng) Intent 解析的時候,Android 都會在系統(tǒng)已安裝的應(yīng)用中根據(jù) <intent-filter> 內(nèi)定義的信息來過濾和匹配 Intent(或者像我們的例子一樣,從通過代碼注冊的 BroadcastReceivers 中尋找)。

Android PackageManager5 會根據(jù) Intent 信息(action,type 和 category)來尋找符合條件的組件來處理 Intent。如果找到唯一合適的組件,會自動調(diào)用,否則會像上面例子里那樣彈出一個選擇對話框,這樣用戶可以自行選擇應(yīng)用(或者根據(jù)默認(rèn)設(shè)置中指定的應(yīng)用)來處理 Intent 動作。

這個方案適用于大多數(shù)的應(yīng)用,但是如果想要采取和 iOS 一樣的 link 就只能使用自定義 URI。不過在 Android 中,兩種方案是都支持的,而且還可以對同樣的 activity 增加多種過濾條件。還是以 YoutubeActivity 為例,我們假定一個 Youtube URI 方案配置上去:

1 <activity android:name=".YouTubeActivity">
2     <intent-filter>
3        <action android:name="android.intent.action.VIEW" />
4       <category android:name="android.intent.category.DEFAULT" />
5         <category android:name="android.intent.category.BROWSABLE" />
6       <data
7        android:scheme="http"
8        android:host="www.youtube.com"
9        android:pathPrefix="/" />
10      <data android:scheme="youtube" android:host="path" />
11   </intent-filter>
12 </activity>

這個 filter 和先前配置的基本一致,除了在第 10 行增配了自定義的 URI 方案。

這樣的話,應(yīng)用可以支持打開諸如:youtube://path.to.video 的鏈接,也可以打開普通的 HTTP 鏈接??傊?,你想給 Activity 中配置多少 filters 和 types 都可以。

使用自定義 URI 方案到底有什么負(fù)面影響?

自定義 URI 方案的問題是它不符合 W3C 針對 URIs 制定的各項標(biāo)準(zhǔn)。當(dāng)然這個問題也并不絕對,如果只是在應(yīng)用包內(nèi)使用自定義 URI 是 OK 的。但像前文所說,若公開自定義 URI 則會存在命名沖突的風(fēng)險。假如定義一個 URI 為 myapp://,誰也不能保證別的應(yīng)用不會定義同樣的東西,這就會有問題。反過來說,使用域名就不存在這種沖突的隱患。拿我們之前構(gòu)建了自己的 Youtube 播放 app 來說,Android 會提供選擇是啟用自己的 Youtube 播放器還是使用官方 app。

同時,瀏覽器可能無法解析某些自定義URL,比如 yourapp://some.data,極有可能報 404。這就是違背規(guī)則和不遵守標(biāo)準(zhǔn)的風(fēng)險。

數(shù)據(jù)分享

可以通過 Intent 向其他應(yīng)用分享信息,比如說向社交網(wǎng)網(wǎng)站分享個帖子,向圖片編輯 app 傳遞一張圖片,發(fā)郵件,發(fā)短息,或者通過即時通訊應(yīng)用傳些資源什么的等等都是再分享數(shù)據(jù)。目前為止,我們介紹了怎么創(chuàng)建 intent filters,還有如何將應(yīng)用注冊成廣播接收者以便在收到可響應(yīng)的通知時做出相應(yīng)的處理。在本文的最后一部分,將要探討一下如何分享內(nèi)容。再一次:所謂 Intent 就是對將要執(zhí)行的動作的一種抽象描述。

分享到社交網(wǎng)站

在下面的例子中,我們會分享一個文本信息并且讓用戶做出最終的選擇:

1  Intent shareIntent = new Intent(Intent.ACTION_SEND);
2  shareIntent.setType("text/plain");
3  shareIntent.putExtra(Intent.EXTRA_TEXT, "Super Awesome Text!");
4  startActivity(Intent.createChooser(shareIntent, "Share this text using…"));

第 1 行使用構(gòu)造方法 public Intent(String action) 根據(jù)指定 action 創(chuàng)建了一個 Intent;

ACTION_SEND 表示會向別的應(yīng)用發(fā)送數(shù)據(jù)。在本例中,要傳遞的信息是 “Super Awesome Text!”。但是目前為止還不知道要傳給誰。最終,這將由用戶決定。

第 2 行設(shè)置 MIME 數(shù)據(jù)的類型為 text/plain。

第 3 行將要傳遞的數(shù)據(jù)通過 exstra 放到 Intent 中去。

第 4 行會觸發(fā)本例的用戶選擇功能。其中 Intent.createChooser 是將 Intent 重新封裝,將其 action 指定為 ACTION_CHOOSER

這里面沒什么特別復(fù)雜的東西。這個 action 就是用來彈出選擇界面的,也就是說讓用戶自己選擇處理方式。某些場景下,你可能會設(shè)計呈現(xiàn)更加具體的選擇(比如用戶正在發(fā)送 email,可以直接給用戶提供統(tǒng)默認(rèn)的郵件客戶端),但是就本例而言,任何能夠處理我們要分享的文本的應(yīng)用都會被納入選擇清單。

具體的運(yùn)行效果(選擇列表太長了,得滾動著來看)如下:

http://wiki.jikexueyuan.com/project/objc/images/11-10.gif" alt="" />

而后我選擇了用 Google Translate 來處理文本,結(jié)果如下:

http://wiki.jikexueyuan.com/project/objc/images/11-11.jpg" alt="" />

Google Translate 將剛剛的文本翻譯成了意大利文。

再給出一個例子

總結(jié)之前,再看個例子。這次會展示如何分享和接收一張圖片。也就是說,當(dāng)用戶分享圖片時,讓我們的 app 出現(xiàn)在用戶的分享選擇列表中。

AndroidManifest 做如下配置:

1    <activity android:name="ImageActivity">
2        <intent-filter>
3            <action android:name="android.intent.action.SEND"/>
4            <category android:name="android.intent.category.DEFAULT"/>
5            <data android:mimeType="image/*"/>
6        </intent-filter>
7    </activity>

注意,至少要配置一個 action 和一個 category。

第 3 行將 action 配置為 SEND,表示可以配置 SEND 類型的 actions。

第 4 行聲明 category 為 DEFAULT。當(dāng)使用 startActivity() 的時候,會默認(rèn)添加 category。

第 5 行很重要,是將 MIME 類型設(shè)置為任何類型的圖片。

接下來,在 ImageActivity 中對Intent的處理如下:

1    @Override
2    protected void onCreate(Bundle savedInstanceState) {
3        super.onCreate(savedInstanceState);
4        setContentView(R.layout.main);
5        
6        // 處理intent(如果有intent)
7        Intent intent = getIntent();
8        if ( intent != null ) {
9            if (intent.getType().indexOf("image/") != -1) {
10                 Uri data = intent.getData();
11                 //  處理image…
12            } 
13        }
14    }

有關(guān)的處理代碼在第 9 行,在檢查 Intent 中是否包含圖片數(shù)據(jù)。

接下來來看看分享圖片的處理代碼:

1    Uri imageUri = Uri.parse("/path/to/image.png");
2    Intent intent = new Intent(Intent.ACTION_SEND);
3    intent.setType("image/png");    
4    intent.putExtra(Intent.EXTRA_STREAM, imageUri);
5    startActivity(Intent.createChooser(intent , "Share"));

關(guān)鍵代碼在第 3 行,定義了 MIME 類型(只有 IntentFilters 匹配到的應(yīng)用才會出現(xiàn)在選擇列表中),第 4 行是將要分享的數(shù)據(jù)放入 Intent 中。

最后,第 5 行創(chuàng)建了之前看到過的選擇對話框,其中只有能夠處理 image/png 的應(yīng)用才會出現(xiàn)在選擇對話框的列表中。

總結(jié)

我們從大體上介紹了什么 Intent,它能做些什么,以及如何在 Android 中分享信息,但是還有很多內(nèi)容本文沒有涵蓋。Intent 這種機(jī)制非常強(qiáng)大,相比較于 iOS 設(shè)備而言,Android 這個特性提供了非常便捷的用戶體驗。iOS 用戶(包括我在內(nèi))會覺得頻繁的返回主界面或者操作任務(wù)切換是非常低效的。

當(dāng)然,這也并不意味著在應(yīng)用之間分享數(shù)據(jù)這方面 Android 的技術(shù)就是更好的或者說其實現(xiàn)方式更高級。歸根結(jié)底,這是個人喜好問題,就像有些 iOS 用戶就不喜歡 Android 設(shè)備的返回鍵而 Android 用戶卻特別中意。理由是這些 Android 用戶覺得返回鍵標(biāo)準(zhǔn)、高效且位置固定,總是在 home 鍵旁。

我記得我在西班牙生活的時候,曾經(jīng)聽過一個很棒的諺語: “Colors were created so we can all have different tastes”(“各花入各眼,存在即合理”)。

延伸閱讀


  1. Activities 是在你的應(yīng)用中提供單個屏幕的用戶界面的組件。 

  2. Fragment 代表了一個 activity 中的行為或者一部分用戶界面。 

  3. 由字符串到 一組 Parcelable 類型的映射。 

  4. Service 是這樣一種應(yīng)用組件:當(dāng)用戶與應(yīng)用無交互時,它還可以執(zhí)行長時間運(yùn)行的操作,或者為其他應(yīng)用提供某種功能。 

  5. PackageManager: 是用來從當(dāng)前安裝在設(shè)備上的 package 中獲取各類信息的類。