說起 Android,最大的特點(diǎn)莫過于運(yùn)行其平臺(tái)上的應(yīng)用可以很容易的啟動(dòng)別的應(yīng)用以及互相之間分享數(shù)據(jù)?;厥?iOS 1.0 時(shí)代,應(yīng)用之間是完全隔離的,無(wú)法進(jìn)行通信(至少非 Apple 應(yīng)用之間是這樣的),甚至到了 iOS SDK 面世之時(shí),這種狀況也沒有改變。
iOS 6 之前的系統(tǒng),若要在編寫郵件過程中直接加入照片或視頻是件很麻煩的事。iOS 6 發(fā)布以后,這項(xiàng)功能才得到根本性的改善。但是在 Android 的世界里,自發(fā)布的第一天,這種功能就是天生攜帶的。
類似的系統(tǒng)平臺(tái)層面的差異還有許多。比如有這樣一個(gè)場(chǎng)景:拍一張照片,然后用某個(gè)圖片處理 app 里編輯一下,接著將照片分享到 Instagram。
注意:這里只是列舉個(gè)別細(xì)節(jié)。
iOS的做法是:
至于 Android,就簡(jiǎn)單得多了:
需要說明的是,對(duì)于那些提供分享功能的 iOS 應(yīng)用來(lái)說,其處理流程和 Android 基本是一致的。根本性的差別是,如果應(yīng)用本身不支持分享那就斷絕了分享給其他應(yīng)用的道路。與 Facebook 和 Twitter 一樣,Instagram 這類熱門應(yīng)用還好,但是除此之外還有大量的應(yīng)用,基本上沒什么應(yīng)用會(huì)集成針對(duì)它們的分享服務(wù)。
比如說你想把 Instagram 里的某張照片分享到 Path 上面(我知道,Path 比較小眾,但是...)。如果是 Android 系統(tǒng),直接從 chooser dialog (選擇對(duì)話框) 中選擇 Path 即可。就是這么簡(jiǎn)單。
還是說回正題,Intents。
在英語(yǔ)詞典里 Intent 的定義是:
noun (名詞)
intention or purpose (意圖、目的)
來(lái)自 Android 官方文檔的說明是,Intent 對(duì)象主要解決 Androi d應(yīng)用各項(xiàng)組件之間的通訊。事實(shí)上,Intent 就是對(duì)將要執(zhí)行的操作的一種抽象描述。
看起來(lái)很簡(jiǎn)單,實(shí)際上 Intent 的意義遠(yuǎn)不止于此。在 Android 世界中,Intent 幾乎隨處可見,無(wú)論你開發(fā)的 app 多么簡(jiǎn)單,也離不開 Intent;小到一個(gè) Hello World 應(yīng)用也是要使用 Intent 的。因?yàn)?Intent 最基礎(chǔ)最常見的用法就是啟動(dòng) Activity。1
在iOS中,與 Activity 相比較,最相似的東西就是 UIViewController 了。切莫在 Android 中尋找 ApplicationDelegate 的等價(jià)物,因?yàn)闆]有。也許只有 Application 類稍微貼近于 ApplicationDelegate ,但是從架構(gòu)上看,它們有著本質(zhì)的區(qū)別。
由于廠商把手機(jī)屏幕越做越大,一個(gè)全新的概念 Fragments2(碎片)隨之而生。最典型的例子就是新聞閱讀類應(yīng)用。在小屏幕的手機(jī)上,一般用戶只能先看到文章列表。選中一篇文章后,才會(huì)全屏顯示文章內(nèi)容。
沒有 Fragments 的時(shí)候,開發(fā)者需要?jiǎng)?chuàng)建兩個(gè) activities(一個(gè)用于展示文章列表,另一個(gè)用于全屏展示文章詳情),然后在兩者來(lái)回切換。
在出現(xiàn)大屏幕的平板之前,這么都做沒什么問題。因?yàn)樵瓌t上,同一時(shí)間只有一個(gè) activity 對(duì)用戶可見,但自從 Android 團(tuán)隊(duì)引入了 Fragments,一個(gè)宿主 Activity 就可以同時(shí)展示多個(gè) Fragments 了。
現(xiàn)在,完全可以用一個(gè) Activity 嵌入兩個(gè) Fragments 的方式來(lái)替代先前使用兩個(gè)不同 Activities 的做法。一個(gè) Fragment 用來(lái)展示文章列表,另一個(gè)用來(lái)展示詳情。對(duì)于小屏幕的手機(jī),可以用兩個(gè) Fragments 交替顯示文章列表和詳情。如果是平板設(shè)備,宿主 Activity 會(huì)同時(shí)顯示兩個(gè) Fragments 的內(nèi)容。類似的東西可以想像一下 iPad 中的郵件應(yīng)用,在同一屏中,左邊是收件箱,右邊是郵件列表。
Intents 最常見的用法就是用來(lái)啟動(dòng) activities(以及在 activities 之間傳遞數(shù)據(jù))。Intent 通過定義兩個(gè) activities 之間將要執(zhí)行的動(dòng)作從而將它們粘合起來(lái)。
然而啟動(dòng)一個(gè) Activity 并不簡(jiǎn)單。Android 中有一個(gè)叫做 ActivityManager (活動(dòng)管理器)的系統(tǒng)組建負(fù)責(zé)創(chuàng)建、銷毀和管理 activities。這里不去過多探討 ActivityManager 的細(xì)節(jié),但是需要指出的是它承擔(dān)全程監(jiān)視已啟動(dòng)的 activities 以及在系統(tǒng)內(nèi)發(fā)送廣播通知的職責(zé),比如說,啟動(dòng)過程結(jié)束這件事就是由 ActivityManager 來(lái)向安卓系統(tǒng)的其他部分發(fā)放通知的。
ActivityManager 是安卓系統(tǒng)的一個(gè)極重要的部分,同時(shí)它依靠 Intents 來(lái)完成大部分工作。
那么 Android 系統(tǒng)到底是如何利用 Intent 來(lái)啟動(dòng) Activity 的呢?
如果你仔細(xì)挖掘一下 Activity 的類結(jié)構(gòu)就會(huì)發(fā)現(xiàn):它繼承自 Context,里面恰好有個(gè)抽象方法 startActivity(),其定義如下:
public abstract void startActivity(Intent intent, Bundle options);
Activity 實(shí)現(xiàn)了這個(gè)抽象方法。也就是說只要傳遞了正確的 Intent,可以對(duì)任意一個(gè) Activity 執(zhí)行啟動(dòng)操作。
比如說我們要啟動(dòng)一個(gè)名為 ImageActivity 的 Activity。
其中 Intent 的構(gòu)造方法是這樣的:
public Intent(Context packageContext, Class<?> cls)
需要傳遞參數(shù) Context(注意,可以認(rèn)為每一個(gè) Activity 都是一個(gè)有效的 Context)和 Class 類。
接下來(lái):
Intent i = new Intent(this, ImageActivity.class);
startActivity(i);
這之后會(huì)觸發(fā)一系列調(diào)用,如無(wú)意外,最終會(huì)成功啟動(dòng)一個(gè)新的 Activity,當(dāng)前的 Activity 會(huì)進(jìn)入 paused(暫停)或者 stopped(停止)狀態(tài)。
Intents 還可以用來(lái)在 Activities 之間傳遞數(shù)據(jù),比如我們將信息放入 Extras 來(lái)傳遞:
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 存儲(chǔ)在 Android 的 Bundle3中,Bundle 在這里可以被看做是一個(gè)可序列化的容器。
這樣 ImageActivity 就可以通過 Intent 來(lái)接收信息,可以通過如下方式將信息取出:
int value = getIntent().getIntExtra("AN_INTEGER_EXTRA", 0); //名稱,默認(rèn)值
上面就是如何在 Activities 之間傳簡(jiǎn)單值。當(dāng)然也可以傳序列化對(duì)象。
假如一個(gè)對(duì)象已實(shí)現(xiàn)序列化接口 Serializable。接下來(lái)可以這么做:
YourComplexObject obj = new YourComplexObject();
Intent i = new Intent(this, ImageActivity.class);
i.putSerializable("SOME_FANCY_NAME", obj); //使用接收序列化對(duì)象的方法
startActivity(i);
其它的 Activity 也要使用相應(yīng)的序列化取值方法獲取值:
YourComplexObject obj = (YourComplexObject) getIntent().getSerializableExtra("SOME_FANCY_NAME");
特別說明,從 Intent 取值的時(shí)候請(qǐng)記得判空:
if (getIntent() != null ) {
//確認(rèn)Intent非空后,可以進(jìn)行諸如從extras取值什么的…
}
在 Java 的世界中對(duì)空指針很敏感。所以要多加防范。;)
使用 startActivity() 啟動(dòng)了新的 activity 后,當(dāng)前的 activity 會(huì)依次進(jìn)入 paused 和 stopped 狀態(tài),然后進(jìn)入任務(wù)堆棧,當(dāng)用戶點(diǎn)擊 back 按鈕后,activity 會(huì)再次恢復(fù)激活。正常情況下,這一系列流程沒什么問題,不過還是可以通過向 Intent 傳遞一些 Flags(標(biāo)識(shí))來(lái)通知 ActivityManager 去改變既定行為。
由于這是一個(gè)很大很復(fù)雜的話題,此處就不做過多的展開了??梢詤⒁娢臋n 任務(wù)和返回棧的官方文檔來(lái)了解 Intent Flags。
下面看看 Intents 除了啟動(dòng) Activity 還能做些什么。
Intents 還有兩個(gè)重要職責(zé):
Service4(或向其發(fā)送指令)。Broadcast(廣播)。由于 Activities 不能在后臺(tái)運(yùn)行(因?yàn)樵诤笈_(tái)它們會(huì)進(jìn)入 paused 態(tài),stopped 態(tài),甚至是 destroyed 銷毀狀態(tài)),如果想要執(zhí)行的后臺(tái)進(jìn)程不需要 UI,可以使用 Service (服務(wù))作為替代方案。Services 本身也是個(gè)很大的話題,簡(jiǎn)單的說它就是:沒有界面或 UI 不可見的運(yùn)行在后臺(tái)的任務(wù)。
由于 Services 如無(wú)特殊處理是運(yùn)行在UI線程上的,所以當(dāng)系統(tǒng)內(nèi)存緊張時(shí),Services 極有可能被銷毀。也就是說,如果 Services 所要執(zhí)行的是一個(gè)耗時(shí)操作,那么就應(yīng)該為 Services 開辟單獨(dú)的線程,一般都是通過 AsyncTask 來(lái)創(chuàng)建。比如一個(gè) Service 要執(zhí)行媒體播放任務(wù),可以通過申請(qǐng) Foreground(前臺(tái))服務(wù)狀態(tài)來(lái)強(qiáng)制在通知欄中一直顯示一個(gè)通知,給用戶展示當(dāng)前服務(wù)在做些什么。應(yīng)用也可以取消前臺(tái)狀態(tài)(通知欄上的相應(yīng)狀態(tài)通知也會(huì)隨之消失),但是這么做的話 Service 就失去了較高的狀態(tài)優(yōu)先級(jí)。
Services 機(jī)制是非常強(qiáng)大的,它也是 Android “多任務(wù)”處理的基礎(chǔ),而在早前,它被認(rèn)為是影響電池用量的關(guān)鍵因素。其實(shí)早在 iOS 還未支持多任務(wù)的時(shí)代,Android 已經(jīng)在自如的操縱多任務(wù)處理了。使用正確的話,Services 是平臺(tái)必不可少的重要組成部分。
在以前,有一個(gè)很爭(zhēng)議的問題,就是 Service 可以在沒有任何通知的情況下轉(zhuǎn)入前臺(tái)運(yùn)行。也就是說在用戶不知情的情況下,后臺(tái)可能會(huì)啟動(dòng)大量的服務(wù)來(lái)執(zhí)行各種各樣的任務(wù)。自 Android 4.0 (Ice Cream Sandwich) 之后,Google終于修復(fù)了這個(gè)“隱形”通知的問題,讓無(wú)法殺掉進(jìn)程且在后臺(tái)靜默運(yùn)行的應(yīng)用程序在通知欄上“顯形”,用戶甚至可以從通知欄中切換到應(yīng)用內(nèi)(然后殺掉應(yīng)用)。雖然現(xiàn)在 Android 設(shè)備的續(xù)航還遠(yuǎn)不及 iOS 產(chǎn)品,但是至少后臺(tái)靜默 Services 已經(jīng)不再是耗電的主因了。;)
Intents 和 Services 是怎么協(xié)作的呢?
首先需要一個(gè) Intent 來(lái)啟動(dòng) Service。而 Service 啟動(dòng)后,只要其處于非 stopped 狀態(tài),就可以持續(xù)地向它發(fā)送指令,直到它被停止(在這種情況下它將會(huì)重新啟動(dòng))。
在某個(gè) Activity 中啟動(dòng)服務(wù):
Intent i = new Intent(this, YourService.class);
i.setAction("SOME_COMMAND");
startService(i);
接下來(lái)程序執(zhí)行情況取決于當(dāng)下是否第一次啟動(dòng)服務(wù)。如果是,那么服務(wù)就會(huì)自然啟動(dòng)(首先執(zhí)行構(gòu)造方法和 onCreate() 方法)。如果該服務(wù)已經(jīng)啟動(dòng)過,將會(huì)直接調(diào)用 onStartCommand() 方法。
方法的具體定義:public int onStartCommand(Intent intent, int flags, int startId);
此處重點(diǎn)關(guān)注 Intent。由于 flags 和 startId 與我們要探討的話題相關(guān)性不大,這里直接忽略不贅述。
之前我們通過 setAction("SOME_COMMAND") 設(shè)置了一個(gè) Action。Service 可以通過 onStartCommand() 來(lái)獲取該 action。拿上面的例子來(lái)說,可以這么做:
@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ù)已被殺掉,不要重新啟動(dòng)服務(wù)
}
如果對(duì) START_NOT_STICKY 感興趣,請(qǐng)參見此安卓文檔有很詳盡的描述。
簡(jiǎn)而言之:如果 Service 已經(jīng)被殺掉,不需要重啟。與之相反的是 START_STICKY,這個(gè)表示應(yīng)當(dāng)執(zhí)行重啟。
從上面的代碼段可知,能夠從 Intent 中獲取 Action。這就是比較常見的與 Services 的通訊方式。
假設(shè)我們要開發(fā)一個(gè)應(yīng)用,將 Youtube 的視頻以流式輸送給 Chromecast (雖然現(xiàn)有的Youtube應(yīng)用已經(jīng)具備這個(gè)功能了,但既然是 Android,我們還是希望自己做一個(gè))。
通過一個(gè) Service來(lái)實(shí)現(xiàn)流式播放,這樣當(dāng)用戶在播放視頻的過程中切換到其它應(yīng)用的時(shí)候,播放也不會(huì)停止。定義幾種 actions:
ACTION_PLAY, ACTION_PAUSE, ACTION_SKIP.
在 onStartCommand() 內(nèi),可以通過 switch 或者 if 條件判斷,然后針對(duì)每一種情況做相應(yīng)的處理。
理論上,服務(wù)可以隨意命名,但通常情況下會(huì)使用常量(稍后會(huì)舉例)來(lái)命名,良好的命名可以避免和其它應(yīng)用的服務(wù)名產(chǎn)生沖突,比如說使用完整的包名 'com.yourapp.somepackage.yourservice.SOME_ACTION_NAME'。如果將服務(wù)名設(shè)為私有,那么服務(wù)只能和自己的應(yīng)用通訊,否則要是想和其它應(yīng)用通訊則需要將服務(wù)名公開。
Android 平臺(tái)的強(qiáng)大特性之一就是:任何一個(gè)應(yīng)用都可以廣播一個(gè) Intent,同時(shí),任意應(yīng)用可以通過定義一個(gè) BroadcastReceiver(廣播接收者)來(lái)接收廣播。事實(shí)上,Android 本身就是采用這個(gè)機(jī)制來(lái)向應(yīng)用和系統(tǒng)來(lái)發(fā)送事件通知的。比如說,網(wǎng)絡(luò)突然變成不可用狀態(tài),Android 組件就會(huì)廣播一個(gè) Intent。如果對(duì)此感興趣,可以創(chuàng)建一個(gè) BroadcastReceiver,設(shè)置相應(yīng)的filter(過濾器)來(lái)截獲廣播并作出適當(dāng)?shù)奶幚怼?/p>
可以將這個(gè)過程解為訂閱一個(gè)全局的頻道,并且根據(jù)自己的喜好配置過濾條件,接下來(lái)會(huì)接收符合條件的廣播信息。另外,若只是想要自己的應(yīng)用接收廣播,需要定義成私有。
繼續(xù)前面的 Youtube 播放服務(wù)的例子,如果在播放的過程中出現(xiàn)了問題,服務(wù)可以發(fā)送一個(gè) Intent 廣播來(lái)發(fā)布信息,比如“播放遇到問題,將要停止播放”。
應(yīng)用可以注冊(cè)一個(gè) BroadcastReceiver 來(lái)監(jiān)聽 Service,以便對(duì)收到的廣播做出處理。
下面看一些樣例代碼。
基于上面的例子,你可能會(huì)定義一個(gè) Activity 用來(lái)展示和播放有關(guān)的信息和操作,比如當(dāng)前的播放進(jìn)度和媒體控制按鈕(播放,暫停,停止等等)。你可能會(huì)非常關(guān)注當(dāng)前服務(wù)的狀態(tài);一旦有錯(cuò)誤發(fā)生,你需要及時(shí)知曉(可以向用戶展示錯(cuò)誤提示信息等等)。
在 activity(或者一個(gè)獨(dú)立的 .java 文件)中可以創(chuàng)建一個(gè)廣播接收器:
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")) {
// 由于有錯(cuò)誤發(fā)生,播放停止
} else if (intent.getAction().equals("ACTION_PLAY")){
// 播放視頻
}
// 等等…
}
}
receiver 的實(shí)現(xiàn)大概如此。這里需要注意下我們向 IntentFilter 中添加的 Actions。它們分別為 ACTION_PLAY(播放), ACTION_STOP(停止), 和 ACTION_ERROR(錯(cuò)誤)。
由于我們使用的是 Java,列舉一下 Android 的習(xí)慣用法:
private ServiceReceiver mServiceReceiver; 可以用此法將其定義為 Activity 的成員變量。然后在 onCreate() 方法中對(duì)其進(jìn)行實(shí)例化,比如:mServiceReceiver = new ServiceReciver();。
當(dāng)然,單單創(chuàng)建這樣的一個(gè)對(duì)象是不夠的。我們需要在某處進(jìn)行注冊(cè)。第一反應(yīng),你可能會(huì)認(rèn)為可以在 Activity 的 onStart() 方法內(nèi)注冊(cè)。當(dāng) onStart() 執(zhí)行的時(shí)候,意味著用戶可以看到這個(gè) Activity 了。
注冊(cè)方法詳情如下(定義在 Context 中):
public abstract Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);
由于 Activities 和 Services 都是 Contexts,所以它們本身都實(shí)現(xiàn)了這個(gè)方法。這表示它們都可以注冊(cè)一個(gè)或多個(gè) BroadcastReceivers。
此方法需要參數(shù) BroadcastReceiver 和 IntentFilter。之前已經(jīng)創(chuàng)建好,可直接傳參:
@Override
public void onStart() {
onStart();
registerReceiver(mServiceReceiver, mServiceReceiver.intentFilter);
}
請(qǐng)養(yǎng)成良好的 Java / Android 開發(fā)習(xí)慣,當(dāng) Activity 停止的時(shí)候,請(qǐng)注銷相應(yīng)的注冊(cè)信息:
@Override
public void onStop() {
super.onStop();
unregisterReceiver(mServiceReceiver);
}
這種處理本身沒什么問題,但是要提醒大家,一旦用戶離開了當(dāng)前應(yīng)用,將不會(huì)再收到廣播。這是由于 Activity即將停止,此處在 onStop() 這里注銷了廣播接收。所以當(dāng)你設(shè)計(jì) BroadcastReceivers 的時(shí)候,需要考慮清楚,這種處理方式是否適用。畢竟還有其它不依賴于 Activity 的實(shí)現(xiàn)方式可供選擇。
每當(dāng) Service 偵測(cè)到錯(cuò)誤發(fā)生,它都會(huì)發(fā)起一個(gè)廣播,這樣 BroadcastReceiver 可以在 onReceive() 方法中接收廣播信息。
廣播接收處理也是 Android 中非常重要非常強(qiáng)大非常核心的機(jī)制。
讀到這里,愛思考的讀者們可能會(huì)問這些廣播到底可以 全局到什么程度?如何將廣播設(shè)置為私有以及如何限制它們只和其所屬應(yīng)用通訊?
事實(shí)上 Intents 有兩類:顯式的 (explicit) 和隱式的 (implicit)。
所謂顯式 Intent 就是明確指出了目標(biāo)組件名稱的 Intent,由于不清楚其它應(yīng)用的組件名稱,顯式 Intent 一般用于啟動(dòng)自己應(yīng)用內(nèi)部的組件。隱式 Intent 則表示不清楚目標(biāo)組件的名稱,通過給出一些對(duì)想要執(zhí)行的動(dòng)作的描述來(lái)尋找與之匹配的組件(通過定義過濾器來(lái)羅列一些條件用于匹配組件),隱式 Intent 常用于啟動(dòng)其它應(yīng)用的組件。
鑒于之前給出的例子中使用的就是顯式 Intents,這里將重點(diǎn)討論一下隱式 Intents。
我們通過一個(gè)簡(jiǎn)單的例子來(lái)看看隱式 Intents的強(qiáng)大之處。定義過濾器 (filter) 有兩種方式。第一種與 iOS 的自定義 URI 機(jī)制很類似,比如:yourapp://some.example.com。
如果你的設(shè)計(jì)是想 Android 和 iOS 都通用,那么別無(wú)他法只能使用 URI 策略。如果只是針對(duì) Android 平臺(tái)的話,建議盡量使用標(biāo)準(zhǔn) URL 的方式(比如 http://your.domain.com/yourparams)。之所以這么說是因?yàn)檫@與如何看待自定義 URI 方案的利與弊有關(guān),對(duì)這個(gè)問題不做具體的展開了,總而言之(下文引自 stackoverflow):
為了避免不同實(shí)體之間的命名沖突,web 標(biāo)準(zhǔn)要求應(yīng)嚴(yán)格要控制 URI 的命名。而使用自定義 URI 方案與其 web 標(biāo)準(zhǔn)相違背。一旦將自定義 URI 方案部署到互聯(lián)網(wǎng)上,等于直接將方案名稱投入到整個(gè)互聯(lián)網(wǎng)的命名空間中去(會(huì)又很大的命名沖突可能性),所以應(yīng)嚴(yán)格遵守相應(yīng)的標(biāo)準(zhǔn)。
來(lái)源:StackOverflow
上述問題暫且放一邊,下面看兩個(gè)例子,一個(gè)是用標(biāo)準(zhǔn) URL 來(lái)實(shí)現(xiàn)之前 YouTube app,另一個(gè)是在我們自己的 app 中采用自定義 URI方案。
因?yàn)槊總€(gè)Android都有配置文件AndroidManifest.xml,可以在其中定義Activities,Services,BroadcastReceivers,versions(版本信息),Intent filter等描述信息,所以實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單。詳情見文檔。
Intent 過濾器的本質(zhì)是系統(tǒng)依照過濾條件檢索當(dāng)前已安裝的所有應(yīng)用,看看有哪些應(yīng)用可以處理指定的 URI。
如果某個(gè) app 剛好匹配且是唯一能夠匹配的 app,就會(huì)自動(dòng)打開這個(gè) app。否則的話,可以看到類似這樣的一個(gè)選擇對(duì)話框:
http://wiki.jikexueyuan.com/project/objc/images/11-9.jpg" alt="" />
為什么 Youtube 的官方應(yīng)用會(huì)出現(xiàn)在清單上呢?
我只是在 Facebook 的應(yīng)用里點(diǎn)了一個(gè) Youtube 的鏈接而已。為什么 Android 會(huì)知道我點(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>
接下來(lái)我們會(huì)逐行解釋一下這段XML信息。
第 1 行是聲明 activity(Android 中的每個(gè) activity 都必須在配置文件中聲明,而過濾器則不是必須的)。
第 2 行聲明了 action。此處的 VIEW 是最常用的action,它表示會(huì)向用戶展示數(shù)據(jù)。因?yàn)檫€存在一些受保護(hù)的只能用于系統(tǒng)級(jí)傳輸?shù)?action。
第 4-5 行聲明了類別 (categories)。隱式 Intents 要求至少有一個(gè) action 和一個(gè) category。categories 里主要定義 Intent 所要執(zhí)行的 Action 的更多細(xì)節(jié)。在解析 Intent 的時(shí)候,只有滿足 categories 中全部描述條件的 activities 才會(huì)被使用。Android 把所有傳給 startActivity() 的隱式 Intent 當(dāng)作它們包含至少一個(gè) category android.intent.category.DEFAULT (CATEGORY_DEFAULT 常量),想要接收隱式 Intent 的 Activity 必須在它們的 Intent Filter 中配置 android.intent.category.DEFAULT。
android.intent.category.BROWSABLE 是另一個(gè)敏感配置:
能通過瀏覽器安全調(diào)用的 Activity 必須支持這個(gè) category。比如,用戶從正在瀏覽的網(wǎng)頁(yè)或者文本中點(diǎn)擊了一個(gè) e-mail 鏈接,接下來(lái)生成執(zhí)行這個(gè) link 的 Intent 會(huì)含有 BROWSABLE categroy 描述,所以只有支持這個(gè) category 的 activities 才會(huì)有可能被匹配到。一旦承諾支持這個(gè) category,在被 Intent 匹配調(diào)用后,必須保證沒有惡意的行為或內(nèi)容(至少是在用戶不知情的情況下不可以有)。
來(lái)源:Android Documentation(官方文檔)
這個(gè)點(diǎn)很關(guān)鍵,Android 通過它構(gòu)建了一種機(jī)制,允許應(yīng)用去響應(yīng)任何的鏈接。利用這個(gè)機(jī)制你完全可以構(gòu)建自己的瀏覽器去處理任何 URL 的請(qǐng)求,如果用戶喜歡的話完全可以將你的瀏覽器設(shè)置成默認(rèn)瀏覽器。
第 6-9 行聲明了所要操作的數(shù)據(jù)類型。在本例中,我們使用 scheme(方案/策略)和 host(主機(jī))來(lái)進(jìn)行過濾,所以任何以 http://www.youtube.com/ 開頭的鏈接均可處理,哪怕是在 web 瀏覽器里點(diǎn)擊鏈接。
在 Youtube 應(yīng)用內(nèi)的 AndroidManifest.xml 里配置以上信息后,每當(dāng) Intent 解析的時(shí)候,Android 都會(huì)在系統(tǒng)已安裝的應(yīng)用中根據(jù) <intent-filter> 內(nèi)定義的信息來(lái)過濾和匹配 Intent(或者像我們的例子一樣,從通過代碼注冊(cè)的 BroadcastReceivers 中尋找)。
Android PackageManager5 會(huì)根據(jù) Intent 信息(action,type 和 category)來(lái)尋找符合條件的組件來(lái)處理 Intent。如果找到唯一合適的組件,會(huì)自動(dòng)調(diào)用,否則會(huì)像上面例子里那樣彈出一個(gè)選擇對(duì)話框,這樣用戶可以自行選擇應(yīng)用(或者根據(jù)默認(rèn)設(shè)置中指定的應(yīng)用)來(lái)處理 Intent 動(dòng)作。
這個(gè)方案適用于大多數(shù)的應(yīng)用,但是如果想要采取和 iOS 一樣的 link 就只能使用自定義 URI。不過在 Android 中,兩種方案是都支持的,而且還可以對(duì)同樣的 activity 增加多種過濾條件。還是以 YoutubeActivity 為例,我們假定一個(gè) 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>
這個(gè) filter 和先前配置的基本一致,除了在第 10 行增配了自定義的 URI 方案。
這樣的話,應(yīng)用可以支持打開諸如:youtube://path.to.video 的鏈接,也可以打開普通的 HTTP 鏈接??傊阆虢o Activity 中配置多少 filters 和 types 都可以。
自定義 URI 方案的問題是它不符合 W3C 針對(duì) URIs 制定的各項(xiàng)標(biāo)準(zhǔn)。當(dāng)然這個(gè)問題也并不絕對(duì),如果只是在應(yīng)用包內(nèi)使用自定義 URI 是 OK 的。但像前文所說,若公開自定義 URI 則會(huì)存在命名沖突的風(fēng)險(xiǎn)。假如定義一個(gè) URI 為 myapp://,誰(shuí)也不能保證別的應(yīng)用不會(huì)定義同樣的東西,這就會(huì)有問題。反過來(lái)說,使用域名就不存在這種沖突的隱患。拿我們之前構(gòu)建了自己的 Youtube 播放 app 來(lái)說,Android 會(huì)提供選擇是啟用自己的 Youtube 播放器還是使用官方 app。
同時(shí),瀏覽器可能無(wú)法解析某些自定義URL,比如 yourapp://some.data,極有可能報(bào) 404。這就是違背規(guī)則和不遵守標(biāo)準(zhǔn)的風(fēng)險(xiǎn)。
可以通過 Intent 向其他應(yīng)用分享信息,比如說向社交網(wǎng)網(wǎng)站分享個(gè)帖子,向圖片編輯 app 傳遞一張圖片,發(fā)郵件,發(fā)短息,或者通過即時(shí)通訊應(yīng)用傳些資源什么的等等都是再分享數(shù)據(jù)。目前為止,我們介紹了怎么創(chuàng)建 intent filters,還有如何將應(yīng)用注冊(cè)成廣播接收者以便在收到可響應(yīng)的通知時(shí)做出相應(yīng)的處理。在本文的最后一部分,將要探討一下如何分享內(nèi)容。再一次:所謂 Intent 就是對(duì)將要執(zhí)行的動(dòng)作的一種抽象描述。
在下面的例子中,我們會(huì)分享一個(gè)文本信息并且讓用戶做出最終的選擇:
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)建了一個(gè) Intent;
ACTION_SEND 表示會(huì)向別的應(yīng)用發(fā)送數(shù)據(jù)。在本例中,要傳遞的信息是 “Super Awesome Text!”。但是目前為止還不知道要傳給誰(shuí)。最終,這將由用戶決定。
第 2 行設(shè)置 MIME 數(shù)據(jù)的類型為 text/plain。
第 3 行將要傳遞的數(shù)據(jù)通過 exstra 放到 Intent 中去。
第 4 行會(huì)觸發(fā)本例的用戶選擇功能。其中 Intent.createChooser 是將 Intent 重新封裝,將其 action 指定為 ACTION_CHOOSER。
這里面沒什么特別復(fù)雜的東西。這個(gè) action 就是用來(lái)彈出選擇界面的,也就是說讓用戶自己選擇處理方式。某些場(chǎng)景下,你可能會(huì)設(shè)計(jì)呈現(xiàn)更加具體的選擇(比如用戶正在發(fā)送 email,可以直接給用戶提供統(tǒng)默認(rèn)的郵件客戶端),但是就本例而言,任何能夠處理我們要分享的文本的應(yīng)用都會(huì)被納入選擇清單。
具體的運(yùn)行效果(選擇列表太長(zhǎng)了,得滾動(dòng)著來(lái)看)如下:
http://wiki.jikexueyuan.com/project/objc/images/11-10.gif" alt="" />
而后我選擇了用 Google Translate 來(lái)處理文本,結(jié)果如下:
http://wiki.jikexueyuan.com/project/objc/images/11-11.jpg" alt="" />
Google Translate 將剛剛的文本翻譯成了意大利文。
總結(jié)之前,再看個(gè)例子。這次會(huì)展示如何分享和接收一張圖片。也就是說,當(dāng)用戶分享圖片時(shí),讓我們的 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>
注意,至少要配置一個(gè) action 和一個(gè) category。
第 3 行將 action 配置為 SEND,表示可以配置 SEND 類型的 actions。
第 4 行聲明 category 為 DEFAULT。當(dāng)使用 startActivity() 的時(shí)候,會(huì)默認(rèn)添加 category。
第 5 行很重要,是將 MIME 類型設(shè)置為任何類型的圖片。
接下來(lái),在 ImageActivity 中對(duì)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ù)。
接下來(lái)來(lái)看看分享圖片的處理代碼:
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)用才會(huì)出現(xiàn)在選擇列表中),第 4 行是將要分享的數(shù)據(jù)放入 Intent 中。
最后,第 5 行創(chuàng)建了之前看到過的選擇對(duì)話框,其中只有能夠處理 image/png 的應(yīng)用才會(huì)出現(xiàn)在選擇對(duì)話框的列表中。
我們從大體上介紹了什么 Intent,它能做些什么,以及如何在 Android 中分享信息,但是還有很多內(nèi)容本文沒有涵蓋。Intent 這種機(jī)制非常強(qiáng)大,相比較于 iOS 設(shè)備而言,Android 這個(gè)特性提供了非常便捷的用戶體驗(yàn)。iOS 用戶(包括我在內(nèi))會(huì)覺得頻繁的返回主界面或者操作任務(wù)切換是非常低效的。
當(dāng)然,這也并不意味著在應(yīng)用之間分享數(shù)據(jù)這方面 Android 的技術(shù)就是更好的或者說其實(shí)現(xiàn)方式更高級(jí)。歸根結(jié)底,這是個(gè)人喜好問題,就像有些 iOS 用戶就不喜歡 Android 設(shè)備的返回鍵而 Android 用戶卻特別中意。理由是這些 Android 用戶覺得返回鍵標(biāo)準(zhǔn)、高效且位置固定,總是在 home 鍵旁。
我記得我在西班牙生活的時(shí)候,曾經(jīng)聽過一個(gè)很棒的諺語(yǔ): “Colors were created so we can all have different tastes”(“各花入各眼,存在即合理”)。
Activities 是在你的應(yīng)用中提供單個(gè)屏幕的用戶界面的組件。 ↩
Fragment 代表了一個(gè) activity 中的行為或者一部分用戶界面。 ↩
由字符串到 一組 Parcelable 類型的映射。 ↩
Service 是這樣一種應(yīng)用組件:當(dāng)用戶與應(yīng)用無(wú)交互時(shí),它還可以執(zhí)行長(zhǎng)時(shí)間運(yùn)行的操作,或者為其他應(yīng)用提供某種功能。 ↩
PackageManager: 是用來(lái)從當(dāng)前安裝在設(shè)備上的 package 中獲取各類信息的類。 ↩