現實中的廣播:電臺為了傳達一些消息而發送廣播,通過廣播攜帶要傳達的消息,群眾只要買一個收音機,就可以收到廣播了。
Android中的廣播:Android系統在運行過程中,會發生很多事件,比如電量改變、耳機插入、收到短信、撥打電話、屏幕解鎖、系統開機等,為了讓App知道事件的發生,系統會發送該事件的廣播,App只要注冊一個BroadcastReceiver,就可以接收到對應的廣播,以便做出響應。
在Android系統中,廣播是進行進程間通信(IPC)的重要手段,所以Android系統為我們提供了BroadcastReceiver來接收程序(包括我們開發的程序和系統內建的程序)所發出的廣播(實際上是Broadcast Intent)
本文所提到的廣播主要是指全局廣播,而對于進程內通信,建議使用局部廣播 LocalBroadcastManger
各種OnXxxListener只是程序級別的監聽器,這些監聽器運行在指定程序所在進程中,當程序退出時,OnXxxListener監聽器也隨之關閉;而BroadcastReceiver屬于系統級的監聽器,在Android 4.0以前,對于靜態注冊的BroadcastReceiver,只要存在與之匹配的Intent被廣播出來就會被觸發它,即便它所在的應用程序還沒有啟動,系統也會啟動這個應用程序
廣播接收器的創建
1. 定義
創建一個類XxxReceiver繼承于BroadcastReceiver,并重寫onReceive()
public class XxxReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
}
}
2. 注冊
-
靜態注冊(常駐)
靜態注冊就是在AndroidManifest中注冊BroadcastReceiver,并指定它所接收的廣播種類,如下面配置的XxxReceiver用來接收開機廣播
<receiver android:name=".XxxReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>
使用靜態注冊的BroadcastReceiver,在Android 4.0以前,只要app被安裝,它就會一直生效;而Android 4.0之后,在app安裝后還需要先啟動一次,它才會生效,并且如果用戶在應用管理界面手動殺死了這個BroadcastReceiver所在進程,那么它將不在生效,直到用戶下一次啟動app,才會再次生效。但是如果是系統在內存不足時自動殺死了這個BroadcastReceiver所在進程,它仍然還是生效的。
BroadcastReceiver一旦生效,每次與之匹配的Intent被廣播出來,系統就會創建對應的BroadcastReceiver實例,并自動觸發它的onReceive()方法,onReceive()方法執行完后,BroadcastReceiver實例就會被銷毀(生命周期結束)。
-
動態注冊(非常駐)
動態注冊是指在Java代碼中注冊BroadcastReceiver,并通過IntentFilter來指定它接收的廣播種類。
使用動態注冊BroadcastReceiver,通常是在onResume()
中使用registerReceiver(xxxReceiver, intentFilter)
注冊它,在onPause()
使用unregisterReceiver(xxxReceiver)
注銷它,注銷之后BroadcastReceiver立即失效,這樣可以有效的節約系統消耗。下面代碼動態注冊的XxxReceiver用于接收屏幕開關廣播:@Override protected void onResume() { super.onResume(); receiver = new XxxReceiver(); IntentFilter intentFilter = new IntentFilter(); // 用于指定接收廣播的類型 intentFilter.addAction(Intent.ACTION_SCREEN_ON); // 屏幕點亮 intentFilter.addAction(Intent.ACTION_SCREEN_OFF); // 屏幕熄滅 registerReceiver(receiver, intentFilter); // 注冊廣播接收器 } @Override protected void onPause() { unregisterReceiver(receiver); // 注銷廣播接收器 super.onPause(); }
有些特殊的廣播,必須使用動態注冊的BroadcastReceiver來接收,比如:
- 屏幕開關
- 電量改變
- 耳機插拔
如果一個BroadcastReceiver用于更新UI,那么通常會使用動態注冊。
接收系統發出的廣播
IP撥號器
撥打電話時,系統會發出一個廣播,廣播中攜帶著用戶所要撥打的號碼。
我們可以創建一個BroadcastReceiver接收這個廣播,然后取出廣播中攜帶的號碼進行修改(這里是加上線路號碼),然后把修改后的號碼放回廣播。
在IP撥號器中定義廣播接收器接收打電話廣播
public class CallReceiver extends BroadcastReceiver {
/**
* 當廣播接收器接收到廣播時,此方法會調用
* @param context
* @param intent
*/
@Override
public void onReceive(Context context, Intent intent) {
String number = getResultData(); // 拿到用戶撥打的號碼
setResultData("17951" + number); // 修改廣播內的號碼
}
}
在清單文件中靜態注冊該receiver并定義它接收的廣播類型
<receiver android:name=".CallReceiver">
<intent-filter >
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
不要忘了申請接收打電話廣播所需要的權限
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
短信攔截器
Android系統在收到短信時會發送一條廣播,廣播中攜帶著短信的源號碼和內容。
我們可以寫一個短信攔截器app,在程序中定義一個BroadcastReceiver,并使其優先級高于系統短信應用的BroadcastReceiver優先級,這樣我們的app會先一步收到短信廣播,然后攔截廣播,使短信應用收不到短信廣播,用戶也就看不到被攔截的短信了。
在短信攔截器中定義廣播接收器接收短信廣播
public class SmsBlockReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras(); // 從廣播中取出短信
Object[] objects = (Object[]) bundle.get("pdus");// 如果對方發來的短信內容過長,短信會被拆分成多條,所以這里用的是數組
// PDU(Protocol Data Unit)即協議數據單元
for (Object object : objects) { // 數組中的每一個元素,就是一條短信
SmsMessage sms = SmsMessage.createFromPdu((byte[]) object); // 把數組中的元素轉換成短信對象
String number = sms.getOriginatingAddress(); // 獲取對方(源)號碼
String content = sms.getMessageBody(); // 獲取短信內容
System.out.println(number + ":" + content);
if ("13888888888".equals(number)) {
abortBroadcast(); // 阻止其他廣播接收器接受該廣播,即攔截13888888888發來的短信
}
}
}
}
然后在清單文件中配置BroadcastReceiver接收的廣播類型,注意要設置優先級(設置為1000即高于系統應用的BroadcastReceiver優先級),保證我們app的BroadcastReceiver優先級高于短信應用的BroadcastReceiver優先級,才能實現短信攔截
<receiver android:name=".SmsBlockReceiver">
<intent-filter android:priority="1000">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
接收短信廣播同樣也需要申請權限
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
SD卡狀態偵聽
首先定義廣播接收器
public class SdcardReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction(); // 取出Broadcast Intent中的action,判斷收到的是哪一個廣播
if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
Toast.makeText(context, "SD卡就緒", Toast.LENGTH_SHORT).show();
} else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
Toast.makeText(context, "SD卡被卸載", Toast.LENGTH_SHORT).show();
} else if (action.equals(Intent.ACTION_MEDIA_REMOVED)) {
Toast.makeText(context, "SD卡被拔出", Toast.LENGTH_SHORT).show();
}
}
}
然后在清單文件中注冊這個廣播接收器,并指定它所接收的廣播類型。
一個廣播接收器可以接收多種廣播,只需要在intent-filter標簽下定義多個action即可
<receiver android:name=".SdcardReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED"/> <!-- SD卡就緒 -->
<action android:name="android.intent.action.MEDIA_UNMOUNTED"/> <!-- SD卡被卸載(系統設置中卸載) -->
<action android:name="android.intent.action.MEDIA_REMOVED"/> <!-- SD卡被拔出(物理插拔) -->
<data android:scheme="file"/> <!-- 廣播中攜帶著以file為前綴的SD卡路徑,添加這個data項才能匹配 -->
</intent-filter>
</receiver>
勒索app
寫一個勒索app(僅供學習),使其具有下面的功能:
- Back鍵無效:重寫onBackPressed()使其不調用finish().
- Home鍵無效:通過監控Task棧,如果Task棧頂不是我們勒索app的Activity,就啟動這個Activity.
- 開機自啟:在勒索app中定義接收開機廣播的BroadcastReceiver,并在它的onReceive()中啟動勒索app的Activity,這里主要介紹的就是這個功能的實現。
在勒索app中定義廣播接收器接收開機廣播
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 當接收到開機廣播,啟動勒索軟件MainActivity
Intent intent1 = new Intent(context, MainActivity.class);
// intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent1);
}
}
以上代碼還不能啟動MainActivity。使用標準啟動模式啟動Activity,Activity默認會進入啟動它的組件所屬的Task棧中,廣播接收器(Activity Context之外)并沒有Task棧,也就無法啟動Activity,解決的方法就是要為待啟動的Activity指定FLAG_ACTIVITY_NEW_TASK
標記位,這樣啟動的時候就會為它創建一個新的Task棧
intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
在清單文件中注冊接收開機廣播的廣播接收器
<receiver android:name=".BootReceiver">
<intent-filter >
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
申請權限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
發送廣播
廣播是通過Intent發送的,給Intent設置一個action,然后調用下面三種方法即可發送我們自己的廣播:
sendBroadcast()
:最普通的發送intent的方式,是一種無序的廣播機制,理論上,所有的接收器同時獲得該intent的消息,接受器之間不存在先后順序,不能截斷/修改intent的數據。sendOrderedBroadcast()
:有序的發送廣播的機制,所有接受器可以設置priority ,按照priority的大小順序進行傳遞,上一個優先級的接受器可以截斷和修改intent里面的數據。同時,也可以設置一個結果接收器(總是在最后一個接收到這個intent,用來實現一些特定的功能)。:是一種粘性廣播。所謂的粘性是指,這個intent沒有周期限制,一般廣播只能將intent發送給當前已經注冊了的BroadcastReceiver,一旦發送完畢就失去作用,而粘性廣播沒有這個限制,即便后來注冊的BroadcastReceiver也可以收到這個廣播。從android 5.0開始,出于安全性的考慮,官方已經正式廢棄了粘性廣播。sendStickyBroadcast()
public void sendMyBroadcast(View v) {
Intent intent = new Intent();
intent.setAction("a.b.c"); // a.b.c是我們自己的action
sendBroadcast(intent); // 發送普通廣播
}
無序廣播(普通廣播)和有序廣播
對于無序廣播(普通廣播),所有與廣播Intent匹配的BroadcastReceiver,都可以收到這條廣播,并且不分先后順序,視為同時收到,上面的
sendBroadcast()
發送的就是無序廣播。這種廣播的效率比較高,但缺點是接收器不能將處理結果傳遞給下一個接收器,并且無法在中途終止廣播。-
對于有序廣播,所有與廣播Intent匹配的BroadcastReceiver,不一定都會收到這條廣播,因為有序廣播的接收是分先后順序的,優先級高的先收到,優先級低的后收到,通過
sendOrderedBroadcast()
可以發送有序廣播。- 對于靜態注冊的BroadcastReceiver,優先級聲明在<intent-filter.../>標簽內的
android:priority
屬性中;對于動態注冊的BroadcastReceiver,通過調用IntentFilter對象的setPriority()
也可以設置優先級。優先級用一個整數來表示,值越大代表優先級越高,取值范圍為-1000~1000 - abortBroadCast():終止廣播,類似攔截,只有有序廣播可以被攔截(Andorid 4.4之后只有用戶設置的默認短信應用調用這個方法可以攔截短信廣播)
- 優先接收到廣播的接收器可以通過
setResultXxx()
修改廣播內容,然后下一個接收器通過相應的getResultXxx()
獲取上一個接收器修改后的數據 - 結果接收器resultReceiver:在通過
sendOrderedBroadcast()
發送有序廣播時可以在第三個參數處指定這條有序廣播的結果接收器,在這種情況下,當所有匹配的BroadcastReceiver都接收到該有序廣播后,結果接收器才會收到,并且一定會收到該有序廣播(即使使用abortBroadCast()攔截也會收到)。在前面IP撥號器的例子中,打電話應用中的BroadcastReceiver就是一個結果接收器,所以我們的IP撥號器中的BroadcastReceiver即使沒設置優先級也會在打電話應用之前收到撥號廣播,且打電話應用不能被攔截
- 對于靜態注冊的BroadcastReceiver,優先級聲明在<intent-filter.../>標簽內的
BroadcastReceiver和Service
如果BroadcastReceiver的onReceive()
方法不能在5s內執行完成,就會拋出ANR,所以不能在此方法中執行一些耗時的操作。
如果確實需要根據Broadcast來完成一項比較耗時的操作,則可以考慮在onReceive()
中啟動一個IntentService來完成該操作。
不應考慮在onReceive()
中啟動新線程去完成耗時操作,因為BroadcastReceiver本身的生命周期很短(靜態注冊下當onReceive()
執行完生命周期即結束),很可能出現的情況是子線程還沒有執行結束,BroadcastReceiver就已經被銷毀了,此時應用進程可能由于不含有任何活動的應用組件而變為空進程,Android系統在內存緊張時會優先結束空進程,從而導致執行耗時操作的子線程不能執行完成。
拋出ANR的條件
對于Activity、BroadcastReceiver、Service這三大組件,如果在主線程(UI線程)中進行耗時操作,都有可能導致應用拋出ANR(Application Not Responding)異常,下面給出在這三大組件中拋出ANR的條件,源碼基于Nougat - 7.1.1_r6
-
對于Activity,如果在主線程中執行耗時操作,并且從用戶按鍵或觸摸屏幕開始算起5s內耗時操作仍然未執行完,就會拋出ANR,這類ANR會有提示框彈出,用戶可以選擇force close或者繼續等待。對應ActivityManagerService.java源碼:
// How long we wait until we timeout on key dispatching. static final int KEY_DISPATCHING_TIMEOUT = 5 * 1000; // How long we wait until we timeout on key dispatching during instrumentation. static final int INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT = 60 * 1000;
-
對于BroadcastReceiver的onReceive(),如果在主線程中執行耗時操作,并且在60s內(默認后臺隊列廣播)耗時操作仍然未執行完,就會拋出ANR,這類ANR沒有提示框彈出。對應ActivityManagerService.java源碼:
// How long we allow a receiver to run before giving up on it. static final int BROADCAST_FG_TIMEOUT = 10 * 1000; static final int BROADCAST_BG_TIMEOUT = 60 * 1000;
關于前臺隊列廣播和后臺隊列廣播的區別,詳見Android廣播機制——廣播的發送以及說說Android的廣播(4) - 前臺廣播為什么比后臺廣播快?
-
對于前臺Service,如果在主線程中執行耗時操作,并且在20s內耗時操作仍然未執行完,就會拋出ANR,這類ANR同樣沒有提示框彈出。對應ActiveServices.java源碼:
// How long we wait for a service to finish executing. static final int SERVICE_TIMEOUT = 20 * 1000; // How long we wait for a service to finish executing. static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
關于ANR問題的詳細分析,這里有一篇不錯的文章,ANR問題分析指南