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

Android Intents

簡介

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

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

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

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

iOS的做法是:

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

至于 Android,就簡單得多了:

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

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

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

還是說回正題,Intents。

什么是 Android Intent?

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

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

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

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

如何理解 Activities 和 Fragments

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

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

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

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

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

啟動 Activities

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

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

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

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

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

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

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

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

其中 Intent 的構造方法是這樣的:

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

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

接下來:

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

這之后會觸發(fā)一系列調用,如無意外,最終會成功啟動一個新的 Activity,當前的 Activity 會進入 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); //名稱,默認值

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

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

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

其它的 Activity 也要使用相應的序列化取值方法獲取值:

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

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

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

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

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

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

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

Intents 還有兩個重要職責:

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

啟動服務

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

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

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

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

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

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

在某個 Activity 中啟動服務:

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

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

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

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

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

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

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

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

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

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

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

ACTION_PLAY, ACTION_PAUSE, ACTION_SKIP.

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

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

發(fā)送和接收廣播

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

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

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

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

下面看一些樣例代碼。

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

在 activity(或者一個獨立的 .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 的習慣用法:

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

當然,單單創(chuàng)建這樣的一個對象是不夠的。我們需要在某處進行注冊。第一反應,你可能會認為可以在 ActivityonStart() 方法內注冊。當 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ā)習慣,當 Activity 停止的時候,請注銷相應的注冊信息:

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

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

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

廣播接收處理也是 Android 中非常重要非常強大非常核心的機制。

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

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

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

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

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

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

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

來源:StackOverflow

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

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

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

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

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

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

我只是在 Facebook 的應用里點了一個 Youtube 的鏈接而已。為什么 Android 會知道我點的是 Youtube 的鏈接?這其中有什么玄機

假設我們打開 Youtbube 應用的 AndroidManifest.xml,我們應該能看到類似如下的配置:

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ù)。因為還存在一些受保護的只能用于系統(tǒng)級傳輸?shù)?action。

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

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

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

來源:Android Documentation(官方文檔)

這個點很關鍵,Android 通過它構建了一種機制,允許應用去響應任何的鏈接。利用這個機制你完全可以構建自己的瀏覽器去處理任何 URL 的請求,如果用戶喜歡的話完全可以將你的瀏覽器設置成默認瀏覽器。

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

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

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

這個方案適用于大多數(shù)的應用,但是如果想要采取和 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 方案。

這樣的話,應用可以支持打開諸如:youtube://path.to.video 的鏈接,也可以打開普通的 HTTP 鏈接??傊阆虢o Activity 中配置多少 filters 和 types 都可以。

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

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

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

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

可以通過 Intent 向其他應用分享信息,比如說向社交網(wǎng)網(wǎng)站分享個帖子,向圖片編輯 app 傳遞一張圖片,發(fā)郵件,發(fā)短息,或者通過即時通訊應用傳些資源什么的等等都是再分享數(shù)據(jù)。目前為止,我們介紹了怎么創(chuàng)建 intent filters,還有如何將應用注冊成廣播接收者以便在收到可響應的通知時做出相應的處理。在本文的最后一部分,將要探討一下如何分享內容。再一次:所謂 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 行使用構造方法 public Intent(String action) 根據(jù)指定 action 創(chuàng)建了一個 Intent;

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

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

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

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

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

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

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

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

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

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

再給出一個例子

總結之前,再看個例子。這次會展示如何分享和接收一張圖片。也就是說,當用戶分享圖片時,讓我們的 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。當使用 startActivity() 的時候,會默認添加 category。

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

接下來,在 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    }

有關的處理代碼在第 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"));

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

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

總結

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

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

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

延伸閱讀


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

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

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

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

  5. PackageManager: 是用來從當前安裝在設備上的 package 中獲取各類信息的類。 

上一篇:消息傳遞機制下一篇:響應式視圖