全域性大喇叭,詳解廣播機制
記得在上學的時候,每個班級的教室裡都會裝有一個喇叭,這些喇叭都是接入到學校的廣播室的,一旦有什麼重要的通知,就會播放一條廣播來告知全消的師生。類似的工作機制其實在計算機領域也有很廣泛的應用,如果你瞭解網路通訊原理應該會知道,在一個 IP 網路範圍中最大的 IP 地址是被保留作為廣播地址來使用的。比如某個網路的 IP 範圍是 192.168.0.XXX,子網掩碼是 255.255.255.0,那麼這個網路的廣播地址就是 192.168.0.255。廣播資料包會被髮送到同一網路上的所有埠,這樣在該網路中的每臺主機都將會收到這條廣播。
為了方便於進行系統級別的訊息通知,
1. 廣播機制簡介
為什麼說 Android 中的廣播機制更加靈活呢?這是因為 Android 中的每個應用程式都可以對自己感興趣的廣播進行註冊,這樣該程式就只會接收到自己所關心的廣播內容,這些廣播可能是來自於系統的,也可能是來自於其他應用程式的。Android 提供了一套完整的 API,允許應用程式自由地傳送和接收廣播。傳送廣播的方法其實之前稍微有提到過一下,如果你記性好的話可能還會有印象,就是藉助我們學過的 Intent。而接收廣播的方法則需要引入一個新的概念,廣播接收器
先來了解一下廣播的型別。Android 中的廣播主要可以分為兩種型別,標準廣播和有序廣播。
標準廣播(Normal broadcasts)是一種完全非同步執行的廣播,在廣播發出之後,所有廣播接收器幾乎都會在同一時刻接收到這條廣播訊息,因此它們之間沒有任何先後順序可言。這種廣播的效率會比較高,但同時也意味著它是無法被截斷的。標準廣播的工作流程如圖 5.1 所示。
有序廣播(Ordered broacasts)則是一種同步執行的廣播,在廣播發出之後,同一時刻只會有一個廣播接收器能夠收到這條廣播訊息,當這個廣播接收器中的邏輯執行完畢後,廣播才會繼續傳遞。所以此時的廣播接收器是有先後順序的,優先順序高的廣播接收器就可以先收到廣播訊息,並且前面的廣播接收器還可以截斷正在傳遞的廣播,這樣後面的廣播接收器就無法收到廣播訊息了。有序廣播的工作流程如圖 5.2 所示。
掌握了這些基本概念後,我們就可以來嘗試一下廣播的用法了,首先就從接收系統廣播開始吧。
2. 接收系統廣播
Android 內建了很多系統級別的廣播,我們可以在應用程式中通過監聽這些廣播來得到各種系統的狀態資訊。比如手機開機完成後會發出一條廣播,電池的電路發生變化會發出一條廣播,時間或時區發生改變也會發出一條廣播等等。如果想要接收到這些廣播,就需要使用廣播接收器,下面我們就來看一下它的具體用法。
2.1 動態註冊監聽網路變化
廣播接收器可以自由地對自己感興趣的廣播進行註冊,這樣當有相應的廣播發出時,廣播接收器就能夠收到該廣播,並在內部處理相應的邏輯。註冊廣播的方式一般有兩種,在程式碼中註冊和在 AndroidMainifest.xml 中註冊,其中前者也被稱為動態註冊,後者也被稱為靜態註冊。
那麼該如何建立一個廣播接收器呢?其實只需要新建一個類,讓它繼承自 BroadcastReceiver,並重寫父類的 onReceive() 方法就行了。這樣當有廣播到來時,onReceive() 方法就會得到執行,具體的邏輯就可以在這個方法中處理。
那我們就先通過動態註冊的方式編寫一個能夠監聽網路變化的程式,藉此學習一下廣播接收器的基本用法吧。新建一個 BroadcastTest 專案,然後修改 MainActivity 中的程式碼,如下所示:
public class MainActivity extends Activity {
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangeReceiver = new NetworkChangeReceiver();
registerReceiver(networkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangeReceiver);
}
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "network changes",
Toast.LENGTH_SHORT).show();
}
}
}
可以看到,我們在 MainActivity 中定義了一個內部類 NetworkChangeReceiver,這個類是繼承自 BroadcastReceiver 的,並重寫了父類的 onReceive() 方法。這樣每當網路狀態傳送變化時,onReceive() 方法就會得到執行,這裡只是簡單地使用 Toast 提示了一段文字資訊。
然後觀察 onCreate() 方法,首先我們建立了一個 IntentFilter 的例項,並給它添加了一個值為 android.NET.conn.CONNECTIVITY_CHANGE 的 action,為什麼要新增這個值呢?因為當網路狀態傳送變化時,系統發出的正是這一條值為 android.net.conn.CONNECTIVITY_CHANGE 的廣播,也就是說我們的廣播接收器想要監聽什麼廣播,就在這裡新增相應的 action 就行了。接下來建立了一個 NetworkChangeReceiver 的例項,然後呼叫 registerReceiver() 方法進行註冊,將 NetworkChangeReceiver 的例項和 IntentFilter 的例項都傳了進去,這樣 NetworkChangeReceiver 就會收到所有值為 android.net.conn.CONNECTIVITY_CHANGE 的廣播,也就實現了監聽網路變化的功能。
最後要記得,動態註冊的廣播接收器一定都要取消註冊才行,這裡我們是在 onDestroy() 方法中通過呼叫 unregisterReceiver() 方法來實現的。
整體來說,程式碼還是非常簡單的,現在執行一下程式。首先你會在註冊完成的時候收到一條廣播,然後按下 Home 鍵回到主介面(注意不能按 Back 鍵,否則 onDestroy() 方法會執行),接著按下 Menu 鍵--> System settings --> Data usage 進入到資料使用詳情介面,然後嘗試著開關 Mobile Data 來啟動和禁用網路,你就會看到有 Toast 提醒你網路發生了變化。
不過只是提醒網路發生了變化還不夠人性化,最好是能準確地告訴使用者當前是有網路還是沒有網路,因此我們還需要對上面的程式碼進行進一步的優化。修改 MainActivity 中的程式碼,如下所示:
public class MainActivity extends Activity {
......
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectivityManager = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable",
Toast.LENGTH_SHORT).show();
}
}
}
}
在 onReceive() 方法中,首先通過 getSystemService() 方法得到了 ConnectivityManager 的例項,這是一個系統服務類,專門用於管理網路連線的。然後呼叫它的 getActiveNetworkInfo() 方法可以得到 NetworkInfo 的例項,接著呼叫 NetworkInfo 的 isAvailable() 方法,就可以判斷出當前是否有網路了,最後我們還是通過 Toast 的方式對使用者進行提示。
另外,這裡有非常重要的一點需要說明,Android 系統為了保證應用程式的安全性做了規定,如果程式需要訪問一些系統的關鍵性資訊,必須在配置檔案中宣告許可權才可以,否則程式將會直接崩潰,比如這裡查詢系統的網路狀態就是需要宣告許可權的。開啟 AndroidManifest.xml 檔案,在裡面加入如下許可權就可以查詢系統網路狀態了:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="17" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
......
</manifest>
現在重新執行程式,然後按下 Home 鍵 --> 按下 Menu 鍵 --> System settings --> Data usage 進入到資料使用詳情介面,關閉 Mobile Data 會彈出無網路可用的提示,然後重新開啟 Mobile Data 又會彈出網路可用的提示。
2.2 靜態註冊實現開機啟動
動態註冊的廣播接收器可以自由地控制註冊與登出,在靈活性方面由很大的優勢,但是它也存在著一個缺點,即必須要在程式啟動之後才能接收到廣播,因為註冊的邏輯是寫在 onCreate() 方法中的。那麼有沒有什麼辦法可以讓程式在未啟動的情況下就能接收到廣播呢?這就需要使用靜態註冊的方式了。
這裡我們準備讓程式接收到一條開機廣播,當收到這條廣播時就可以在 onReceive() 方法裡執行相應的邏輯,從而實現開機啟動的功能。新建一個 BootCompleteReceiver 繼承自 BroadcastReceiver,程式碼如下所示:
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
}
}
可以看到,這裡不再使用內部類的方式來定義廣播接收器,因為稍後我們需要在 AndroidManifest.xml 中將這個廣播接收器的類名註冊進去。在 onReceive() 方法中,還是簡單地使用 Toast 彈出一段提示資訊。
然後修改 AndroidManifest.xml 檔案,程式碼如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest"
android:versionCode="1"
android:versionName="1.0" >
......
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
......
<receiver android:name=".BootCompleteReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
終於,<application> 標籤內出現了一個新的標籤<receiver>,所有的靜態註冊的廣播接收器都是在這裡進行註冊的。它的用法其實和<activity>標籤非常相似,首先通過 android:name 來指定具體註冊哪一個廣播接收器,然後在 <intent-filter> 標籤里加入想要接收的廣播就行了,由於 Android 系統啟動完成後會發出一條值為 android:intent.action.BOOT_COMPLETED 的廣播,因此我們在這裡添加了相應的 action。
另外,監聽系統開機廣播也是需要宣告許可權的,可以看到,我們使用 <uses-permission> 標籤又加入了一條 android.permission.RECEIVE_BOOT_COMPLETED 許可權。
現在重新執行程式後,我們的程式就已經開始接收開機廣播了,首先開啟到應用程式管理介面來檢視一下當前程式所擁有的許可權,如圖 5.5 所示。
圖 5.5
可以看到,我們的程式目前擁有訪問網路狀態和開機自啟動的許可權。然後將模擬器關閉並重新啟動,在啟動完成之後就會收到開機廣播了。
到目前為止,我們在廣播接收器的 onReceive() 方法中都只是簡單地使用 Toast 提示了一段文字資訊,當你真正在專案中使用到它的時候,就可以在裡面編寫自己的邏輯。需要注意的是,不要在 onReceive() 方法中新增過多的邏輯或者進行任何耗時操作,因為在廣播接收器中是不允許開啟執行緒的,當 onReceive() 方法運行了較長時間而沒有結束時,程式就會報錯。因此廣播接收器更多的是扮演一種開啟程式其他元件的角色,比如建立一條狀態列通知,或者啟動一個服務等。
3. 傳送自定義廣播
現在已經學會了廣播接收器來接收系統廣播,接下來我們就要學習一下如何在應用程式彙總傳送自定義的廣播。前面已經介紹過了,廣播主要分為兩種型別,標準廣播和有序廣播。接下來我們就講通過實踐的方式來看下這兩種廣播具體的區別。
3.1 傳送標準廣播
在傳送廣播之前,我們還是需要先定義一個廣播接收器來準備接收此廣播才行,不然發出去也是白髮。因此新建一個 MyBroadcastReceiver 繼承自 BroadcastReceiver,程式碼如下所示:
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceive",
Toast.LENGTH_SHORT).show();
}
}
這裡當 MyBroadcastReceiver 收到自定義的廣播時,就會彈出 received in MyBroadcastReceive 的提示。然後在 AndroidManifest.xml 中對這個廣播接收器進行註冊:<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest"
android:versionCode="1"
android:versionName="1.0" >
......
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
......
<receiver android:name=".MyBroadcastReceiver">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
</application>
</manifest>
可以看到,這裡讓 MyBroadcastReceiver 接收一條值為 com.example.broadcasttest.MY_BROADCAST 的廣播,因此待會兒在傳送廣播的時候,我們就需要發出這樣的一條廣播。
接下來修改 activity_main.xml 中的程式碼,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Broadcast"
/>
</LinearLayout>
這裡在佈局檔案中定義了一個按鈕,用於作為傳送廣播的觸發點。然後修改 MainActivity 中的程式碼,如下所示:public class MainActivity extends Activity {
......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
}
});
}
}
可以看到,我們在按鈕的點選事件里加入了傳送自定義廣播的邏輯。首先構建出了一個 Intent 物件,並把要傳送的廣播的值傳入,然後呼叫了 Context 的 sendBroadcast() 方法將廣播發送出去,這樣所有監聽 com.example.broadcasttest.MY_BROADCAST 這條廣播的廣播接收器就會收到訊息。此時發出去的廣播就是一條標準廣播。
這樣我們就成功完成了傳送自定義廣播的功能。另外,由於廣播是使用 Intent 進行傳遞的,因此你還可以在 Intent 中攜帶一些資料傳遞給廣播接收器。
3.2 傳送有序廣播
廣播是一種可以跨程序的通訊方式,這一點從前面接收系統廣播的時候就可以看出來了。因此在我們英語程式內發出的廣播,其他的應用程式應該也是可以收到的。為了驗證這一點,我們需要再新建一個 BroadcastTest2 專案。
將專案建立好之後,還需要在這個專案下定義一個廣播接收器,用於接收上一小節中的自定義廣播。新建 AnotherBroadcastReceiver 繼承自 BroadcastReceiver,程式碼如下所示:
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in AnotherBroadcastReceiver",
Toast.LENGTH_SHORT).show();
abortBroadcast();
}
}
這裡仍然是在廣播接收器的 onReceive() 方法中彈出了一段文字資訊。然後在 AndroidManifest.xml 中對這個廣播接收器進行註冊,程式碼如下所示:<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest2"
android:versionCode="1"
android:versionName="1.0" >
......
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
......
<receiver
android:name=".AnotherBroadcastReceiver">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
</application>
</manifest>
可以看到,AnotherBroadcastReceiver 同樣接收的是 com.example.broadcasttest.MY_BROADCAST 這條廣播。現在執行 BroadcastTest2 專案將這個程式安裝到模擬器上,然後重新回到 BroadcastTest 專案的主介面,並點選一下 Send Broadcast 按鈕,就會分別彈出兩次提示資訊。
這樣就強有力地證明了,我們的應用程式發出的廣播是可以被其他的應用程式接收到的。
不過到目前為止,程式裡發出的都還是標準廣播,現在我們來嘗試一下發送有序廣播。關閉 BroadcastTest2 專案,然後修改 MainActivity 中的程式碼,如下所示:
public class MainActivity extends Activity {
......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendOrderedBroadcast(intent, null);
}
});
}
}
可以看到,傳送有序廣播只需要改動一行程式碼,即將 sendBroadcast() 方法改成 sendOrderedBroadcast() 方法。sendOrderedBroadcast() 方法接收兩個引數,第一個引數仍然是 Intent,第二個引數是一個與許可權相關的字串,這裡傳入 null 就行了。現在重新執行程式,並點選 Send Broadcast 按鈕,你會發現,兩個應用程式仍然都可以接收到這條廣播。
看上去好像和標準廣播沒什麼區別嘛,不過別忘了,這個時候的廣播接收器是有先後順序的,而且前面的廣播接收器還可以將廣播階段,以阻止其繼續傳播。
那麼該如何設定廣播接收器的先後順序呢?當然是在註冊的時候進行設定了,修改 AndroidManifest.xml 中的程式碼,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest"
android:versionCode="1"
android:versionName="1.0" >
......
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
......
<receiver android:name=".MyBroadcastReceiver">
<intent-filter android:priority="100" >
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
</application>
</manifest>
可以看到,我們通過 android:priority 屬性給廣播接收器設定了優先順序,優先順序比較高的廣播接收器就可以先收到廣播。這裡將 MyBroadcastReceiver 的優先順序設成了 100,以保證它一定會在 AnotherBroadcastReceiver 之前收到廣播。
既然已經獲得了接收廣播的優先權,那麼 MyBroadcastReceiver 就可以選擇是否允許廣播繼續傳遞了。修改 MyBroadcastReceiver 中的程式碼。如下所示:
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceive",
Toast.LENGTH_SHORT).show();
abortBroadcast();
}
}
如果在 onReceive() 方法中呼叫了 abortBroadcast() 方法,就表示將這條廣播階段,後面的廣播接收器將無法再接收到這條廣播。現在重新執行程式,並點選一下 Send Broadcast 按鈕,你會發現,只有 MyBroadcastReceiver 中的 Toast 資訊能夠彈出,說明這條廣播經過 MyBroadcastReceiver 之後確實是終止傳遞了。
4. 使用本地廣播
前面我們傳送和接收的廣播全部都是屬於系統全域性廣播,即發出的廣播可以被其他任何的任何應用程式接收到,並且我們也可以接收來自於其他任何應用程式的廣播。這樣就很容易會引起安全性的問題,比如說我們傳送的一些攜帶關鍵性資料的廣播有可能被其他的應用程式截獲,或者其他的程式不停地向我們的廣播接收器裡傳送各種垃圾廣播。
為了能夠簡單地解決廣播的安全性問題,Android 引入了一套本地廣播機制,使用這個機制發出的廣播只能夠在應用程式的內部進行傳遞,並且廣播接收器也只能接收來自本應用程式發出的廣播,這樣所有的安全性問題就都不存在了。
本地廣播的用法並不複雜,主要就是使用了一個 LocalBroadcastManager 來對廣播進行管理,並提供了傳送廣播和註冊廣播接收器的方法。下面我們就通過具體的例項來嘗試一下它的用法,修改 MainActivity 中的程式碼,如下所示:
public class MainActivity extends Activity {
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this); // 獲取例項
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(
"com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent); // 傳送本地廣播
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 註冊本地廣播監聽器
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local broadcast",
Toast.LENGTH_SHORT).show();
}
}
}
有沒有感覺這些程式碼很熟悉?沒錯,其實這基本上就和我們前面所學的動態註冊廣播接收器以及傳送廣播的程式碼是一樣的。只不過現在首先是通過 LocalBroadcastManager 的 getInstance() 方法得到了它的一個例項,然後在註冊廣播接收器的時候呼叫的是 LocalBroadcastManager 的 registerReceiver() 方法,在傳送廣播的時候呼叫的是 LocalBroadcastManager 的 sendBroadcast() 方法,僅此而已。這裡我們在按鈕的點選事件裡面發出了一條 com.example.broadcast.LOCAL_BROADCAST 廣播,然後在 LocalReceiver 裡面去接收這條廣播。
有一點需要說明,本地廣播是無法通過靜態註冊的方式來接收的。其實這也完全可以理解,因為靜態註冊主要就是為了讓程式在未啟動的情況下也能收到廣播,而傳送本地廣播時,我們的程式肯定是已經啟動了,因為也完全不需要靜態註冊的功能。
最後我們再來盤點一下使用本地廣播的幾點優勢吧。
- 可以明確地知道正在傳送的廣播不會離開我們的程式,因此不需要擔心機密資料洩露的問題。
- 其他的程式無法將廣播發送到我們的程式的內部,因此不需要擔心會有安全漏洞的隱患。
- 傳送本地廣播比起傳送系統全域性廣播將會更加高效。