1.Android廣播機制概述
今天來看一下Android中的廣播機制,我們知道廣播Broadcast是android中的四大組件之一,可見他的重要性了,當然它的用途也很大的,比如一些系統的廣播:電量低、開機、鎖屏等一些操作都會發送一個廣播
Android廣播分爲兩個方面:廣播發送者和廣播接收者,通常情況下,BroadcastReceiver指的就是廣播接收者(廣播接收器)。
廣播作爲Android組件間的通信方式,可以使用的場景如下:
- 1.同一app內部的同一組件內的消息通信(單個或多個線程之間);
- 2.同一app內部的不同組件之間的消息通信(單個進程);
- 3.同一app具有多個進程的不同組件之間的消息通信;
- 4.不同app之間的組件之間消息通信;
- 5.Android系統在特定情況下與App之間的消息通信。
從實現原理看上,Android中的廣播使用了觀察者模式,基於消息的發佈/訂閱事件模型。因此,從實現的角度來看,Android中的廣播將廣播的發送者和接受者極大程度上解耦,使得系統能夠方便集成,更易擴展。
具體實現流程要點粗略概括如下:
- 1.廣播接收者BroadcastReceiver通過Binder機制向AMS(Activity Manager Service)進行註冊;
- 2.廣播發送者通過binder機制向AMS發送廣播;
- 3.AMS查找符合相應條件(IntentFilter/Permission等)的BroadcastReceiver,將廣播發送到BroadcastReceiver(一般情況下是Activity)相應的消息循環隊列中;
4.消息循環執行拿到此廣播,回調BroadcastReceiver中的onReceive()方法。
對於不同的廣播類型,以及不同的BroadcastReceiver註冊方式,具體實現上會有不同。但總體流程大致如上。
由此看來,廣播發送者和廣播接收者分別屬於觀察者模式中的消息發佈和訂閱兩端,AMS(Activity Manager Service)屬於中間的處理中心。廣播發送者和廣播接收者的執行是異步的,發出去的廣播不會關心有無接收者接收,也不確定接收者到底是何時才能接收到。顯然,整體流程與EventBus非常類似。
在上文說列舉的廣播機制具體可以使用的場景中,現分析實際應用中的適用性:
第一種情形:同一app內部的同一組件內的消息通信(單個或多個線程之間),實際應用中肯定是不會用到廣播機制的(雖然可以用),無論是使用擴展變量作用域、基於接口的回調還是Handler-post/Handler-Message等方式,都可以直接處理此類問題,若適用廣播機制,顯然有些“殺雞牛刀”的感覺,會顯太“重”;
第二種情形:同一app內部的不同組件之間的消息通信(單個進程),對於此類需求,在有些教複雜的情況下單純的依靠基於接口的回調等方式不好處理,此時可以直接使用EventBus等,相對而言,EventBus由於是針對統一進程,用於處理此類需求非常適合,且輕鬆解耦。可以參見文件《Android各組件/控件間通信利器之EventBus》。
第三、四、五情形:由於涉及不同進程間的消息通信,此時根據實際業務使用廣播機制會顯得非常適宜。下面主要針對Android廣播中的具體知識點進行總結。
2.自定義BroadcastReceiver
自定義廣播接收器需要繼承基類BroadcastReceivre,並實現抽象方法onReceive(context,
intent)方法。廣播接收器接收到相應廣播後,會自動回調onReceive(..)方法。默認情況下,廣播接收器也是運行在UI線程,因此,onReceive方法中不能執行太耗時的操作。否則將因此ANR。一般情況下,根據實際業務需求,onReceive方法中都會涉及到與其他組件之間的交互,如發送Notification、啓動service等。
下面代碼片段是一個簡單的廣播接收器的自定義:
public class MyBroadcastReceiver extends BroadcastReceiver {
public static final String TAG = "MyBroadcastReceiver";
public static int m = 1;
@Override
public void onReceive(Context context, Intent intent) {
Log.w(TAG, "intent:" + intent);
String name = intent.getStringExtra("name");
Log.w(TAG, "name:" + name + " m=" + m);
m++;
Bundle bundle = intent.getExtras();
}
}
BroadcastReceiver註冊類型
BroadcastReceiver總體上可以分爲兩種註冊類型:靜態註冊和動態註冊。
廣播被分爲兩種不同的類型:“普通廣播(Normal broadcasts)”和“有序廣播(Ordered broadcasts)”。
普通廣播
是完全異步的,可以在同一時刻(邏輯上)被所有廣播接收者接收到,消息傳遞的效率比較高,但缺點是:接收者不能將處理結果傳遞給下一個接收者,並且無法終止廣播Intent的傳播;
有序廣播
是按照接收者聲明的優先級別(聲明在intent-filter元素的android:priority屬性中,數越大優先級別越高,取值範圍:-1000到1000。也可以調用IntentFilter對象的setPriority()進行設置),被接收者依次接收廣播。如:A的級別高於B,B的級別高於C,那麼,廣播先傳給A,再傳給B,最後傳給C。A得到廣播後,可以往廣播裏存入數據,當廣播傳給B時,B可以從廣播中得到A存入的數據。
Context.sendBroadcast()
發送的是普通廣播,所有訂閱者都有機會獲得並進行處理。
Context.sendOrderedBroadcast()
發送的是有序廣播,系統會根據接收者聲明的優先級別按順序逐個執行接收者,前面的接收者有權終止廣播(BroadcastReceiver.abortBroadcast()),如果廣播被前面的接收者終止,後面的接收者就再也無法獲取到廣播。對於有序廣播,前面的接收者可以將處理結果存放進廣播Intent,然後傳給下一個接收者。
1).靜態註冊:
直接在AndroidManifest.xml文件中進行註冊。規則如下:
<receiver android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:icon="drawable resource"
android:label="string resource"
android:name="string"
android:permission="string"
android:process="string" >
. . .
</receiver>
其中,需要注意的屬性
- android:exported ——此broadcastReceiver能否接收其他App的發出的廣播,這個屬性默認值有點意思,其默認值是由receiver中有無intent-filter決定的,如果有intent-filter,默認值爲true,否則爲false。(同樣的,activity/service中的此屬性默認值一樣遵循此規則)同時,需要注意的是,這個值的設定是以application或者application user id爲界的,而非進程爲界(一個應用中可能含有多個進程);
- android:name —— 此broadcastReceiver類名;
- android:permission ——如果設置,具有相應權限的廣播發送方發送的廣播才能被此broadcastReceiver所接收;
- android:process ——broadcastReceiver運行所處的進程。默認爲app的進程。可以指定獨立的進程(Android四大基本組件都可以通過此屬性指定自己的獨立進程)
常見的註冊形式有:
<receiver android:name=".MyBroadcastReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
其中,intent-filter由於指定此廣播接收器將用於接收特定的廣播類型。本示例中給出的是用於接收網絡狀態改變或開啓啓動時系統自身所發出的廣播。當此App首次啓動時,系統會自動實例化MyBroadcastReceiver,並註冊到系統中。
之前常說:靜態註冊的廣播接收器即使app已經退出,主要有相應的廣播發出,依然可以接收到,但此種描述自Android 3.1開始有可能不再成立,具體分析詳見本文後面部分。
2).動態註冊:
動態註冊時,無須在AndroidManifest中註冊組件。直接在代碼中通過調用Context的registerReceiver函數,可以在程序中動態註冊BroadcastReceiver。registerReceiver的定義形式如下:
registerReceiver(BroadcastReceiver receiver, IntentFilter filter)
registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)
典型的寫法示例如下:
public class MainActivity extends Activity {
public static final String BROADCAST_ACTION = "com.example.corn";
private BroadcastReceiver mBroadcastReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBroadcastReceiver = new MyBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BROADCAST_ACTION);
registerReceiver(mBroadcastReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mBroadcastReceiver);
}
}
注:Android中所有與觀察者模式有關的設計中,一旦涉及到register,必定在相應的時機需要unregister。因此,上例在onDestroy()回到中需要unregisterReceiver(mBroadcastReceiver)。
當此Activity實例化時,會動態將MyBroadcastReceiver註冊到系統中。當此Activity銷燬時,動態註冊的MyBroadcastReceiver將不再接收到相應的廣播。
廣播接收者(BroadcastReceiver)用於接收廣播Intent,廣播Intent的發送是通過調用Context.sendBroadcast()、Context.sendOrderedBroadcast()來實現的。通常一個廣播Intent可以被訂閱了此Intent的多個廣播接收者所接收,這個特性跟JMS中的Topic消息接收者類似。要實現一個廣播接收者方法如下:
第一步:定義廣播接收者,繼承BroadcastReceiver,並重寫onReceive()方法。
public class IncomingSMSReceiver extends BroadcastReceiver {
@Override public void onReceive(Contextcontext, Intentintent) {
}
}
第二步:訂閱感興趣的廣播Intent,訂閱方法有兩種:
第一種:使用代碼進行訂閱(動態訂閱)
IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
IncomingSMSReceiver receiver = new IncomingSMSReceiver();
registerReceiver(receiver, filter);
第二種:在AndroidManifest.xml文件中的節點裏進行訂閱(靜態訂閱)
<receiver android:name=".IncomingSMSReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
下面看一下動態廣播訂閱和靜態廣播訂閱的卻別:
靜態訂閱廣播
又叫:常駐型廣播,當你的應用程序關閉了,如果有廣播信息來,你寫的廣播接收器同樣的能接受到,他的註冊方式就是在你的應用程序中的AndroidManifast.xml進行訂閱的。
動態訂閱廣播
又叫:非常駐型廣播,當應用程序結束了,廣播自然就沒有了,比如你在activity中的onCreate或者onResume中訂閱廣播,同時你必須在onDestory或者onPause中取消廣播訂閱。不然會報異常,這樣你的廣播接收器就一個非常駐型的了。
這裏面還有一個細節那就是這兩種訂閱方式,在發送廣播的時候需要注意的是:動態註冊的時候使用的是隱式intent方式的,所以在發送廣播的時候需要使用隱式Intent去發送,不然是廣播接收者是接收不到廣播的,這一點要注意。但是靜態訂閱的時候,因爲在AndroidMainfest.xml中訂閱的,所以在發送廣播的時候使用顯示Intent和隱式Intent都可以(當然這個只針對於我們自己定義的廣播接收者),所以以防萬一,我們一般都採用隱式Intent去發送廣播。
下面看一下例子:
看一下項目結構:
看一下靜態訂閱廣播:
package com.broadcast.demo;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.example.androidbroadcastdemo.R;
/**
* 靜態訂閱廣播
* @author weijiang204321
*
*/
public class StaticRegisterBroadcastActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = (Button)findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
//使用靜態的方式註冊廣播,可以使用顯示意圖進行發送廣播
Intent broadcast = new Intent("com.broadcast.set.broadcast");
sendBroadcast(broadcast,null);
}
});
}
}
在AndroidMainfest.xml中訂閱:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.androidbroadcastdemo"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<!-- 權限 -->
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.broadcast.demo.StaticRegisterBroadcastActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 無序廣播註冊START -->
<receiver android:name="com.broadcast.receiver.UnSortBroadcastReceiver">
<intent-filter >
<action android:name="com.broadcast.demo.mybroadcast"/>
</intent-filter>
</receiver>
<!-- 無序廣播註冊END -->
<!-- 有序廣播的註冊START -->
<receiver android:name="com.broadcast.receiver.SortBroadcastReceiverA">
<intent-filter android:priority="999">
<action android:name="com.broadcast.set.broadcast"/>
</intent-filter>
</receiver>
<!-- 接收短信廣播 -->
<receiver android:name="com.broadcast.receiver.SortBroadcastReceiverB">
<intent-filter android:priority="1000">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
<!-- 有序廣播的註冊END -->
<!-- 上傳短信內容的Service -->
<service android:name="com.broadcast.service.UploadSMSService">
<intent-filter >
<action android:name="com.broadcast.service.uploadsmsservice"/>
</intent-filter>
</service>
</application>
</manifest>
先不要管其他的內容了,後面會講到,這裏只關注靜態廣播的註冊
<!-- 無序廣播註冊START -->
<receiver android:name="com.broadcast.receiver.UnSortBroadcastReceiver">
<intent-filter >
<action android:name="com.broadcast.demo.mybroadcast"/>
</intent-filter>
</receiver>
<!-- 無序廣播註冊END -->
下面來看一下廣播的接收者:
package com.broadcast.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* 廣播接收者
* @author weijiang204321
*
*/
public class UnSortBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.e("Intent_Action:",intent.getAction()+"");
}
}
在廣播接收者中的onReceive方法中的邏輯很簡單,就是打印Action的內容。
運行程序,結果很簡單,這裏就不上圖片了。
下面來看一下動態訂閱:
package com.broadcast.demo;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.broadcast.receiver.UnSortBroadcastReceiver;
import com.example.androidbroadcastdemo.R;
/**
* 使用動態的方式註冊廣播
* @author weijiang204321
*
*/
public class DynamicRegisterBroadcastActivity extends Activity {
public static final String NEW_LIFEFORM_DETECTED = "com.dxz.broadcasttest.NEW_LIFEFORM";
protected UnSortBroadcastReceiver receiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn0 = (Button) findViewById(R.id.btn);
btn0.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
//發送廣播
Intent it = new Intent(NEW_LIFEFORM_DETECTED);
sendBroadcast(it);
}
});
}
@Override
protected void onResume() {
super.onResume();
//註冊廣播
IntentFilter counterActionFilter = new IntentFilter(NEW_LIFEFORM_DETECTED);
receiver = new UnSortBroadcastReceiver();
registerReceiver(receiver, counterActionFilter);
}
@Override
protected void onPause() {
super.onPause();
//取消廣播
unregisterReceiver(receiver);
}
}
這裏我們是在onResume中進行訂閱廣播,在onPause中取消訂閱廣播。
AndroidMainfest.xml中將啓動的Activity改成DynamicRegisterBroadcastActivity,其他的內容不需要修改,運行程序,打印結果很簡單,這裏就不上圖了。
下面來看一下有序廣播和無序廣播
這個我們在開始的時候已經說到了,下面來看一下無序廣播:
首先我們定義兩個廣播接收者:
第一個廣播接收者:
package com.broadcast.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* 廣播接收者A
* @author weijiang204321
*
*/
public class SortBroadcastReceiverA extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Log.e("Demo:","廣播接收者A");
}
}
第二個廣播接收者:
package com.broadcast.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* 廣播接收者B
* @author weijiang204321
*
*/
public class SortBroadcastReceiverB extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Log.e("Demo:","廣播B");
}
}
在AndroidMainfest.xml中訂閱廣播
[html] view plain copy
<receiver android:name="com.broadcast.receiver.SortBroadcastReceiverA">
<intent-filter android:priority="999">
<action android:name="com.broadcast.set.broadcast"/>
</intent-filter>
</receiver>
<receiver android:name="com.broadcast.receiver.SortBroadcastReceiverB">
<intent-filter android:priority="1000">
<action android:name="com.broadcast.set.broadcast"/>
</intent-filter>
</receiver>
運行結果:
運行結果有點奇怪,爲什麼是接收者B在前面,接收者A在後面,原因就是我們在AndroidMainfest.xml中訂閱廣播的時候在intent-filter設置了android:priority屬性值,值越大優先級越高,接收者B的優先級是1000,接收者A的優先級是999,所以是B先接收到廣播,然後A才接收到,但是接收者B和接收者A之間沒有聯繫,也不能有交互的,因爲是這是無序廣播,是異步的,我們可以做個試驗就是在B中的onReceiver方法中添加代碼:
abortBroadcast();//終止此次廣播的傳輸
運行結果:
我們可以看到提示錯誤,就是non-ordered broadcast無序廣播不允許終止廣播,其實終止也沒有用,因爲接收者A還是接收到廣播了。
下面來看一下有序廣播,代碼需要做一下修改:
首先是在發送廣播的時候:
Intent broadcast = new Intent("com.broadcast.set.broadcast");
sendOrderedBroadcast(broadcast,null);
然後在B接收者中的添加終止廣播的方法:
abortBroadcast();
其他的代碼不需要修改,運行結果:
只有B接收者了,A接收者沒有接收到廣播了,因爲在B接收者中將廣播給終止了,後面的接收者都接受不到了。
下面在修改一下代碼:
接收者B:
package com.broadcast.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
/**
* 廣播接收者B
* @author weijiang204321
*/
public class SortBroadcastReceiverB extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Log.e("Demo:","廣播接收者B");
Bundle bundle = new Bundle();
bundle.putString("next_receiver", "下一個廣播接收者");
setResultExtras(bundle);
}
}
B接收到廣播後,存入一些值,傳給下一個接收者。
接收者A的代碼:
package com.broadcast.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
/**
* 廣播接收者A
* @author weijiang204321
*/
public class SortBroadcastReceiverA extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Log.e("Demo:","廣播接收者A");
Bundle bundle = getResultExtras(true);
String content = bundle.getString("next_receiver");
Log.e("Demo:",content+"");
}
}
運行結果如下:
接收者A收到了上一個接收者B傳遞的信息,這個信息可以保留到後面的所有接收者,直到廣播終止。
上面講到的就是有序廣播的特點,可以看出是一個同步的動作,接收者之間可以進行數據的交互(上一個傳遞數據給下一個),也可以控制廣播的終止。
3.廣播發送及廣播類型
經常說”發送廣播“和”接收“,表面上看廣播作爲Android廣播機制中的實體,實際上這一實體本身是並不是以所謂的”廣播“對象存在的,而是以”意圖“(Intent)去表示。定義廣播的定義過程,實際就是相應廣播”意圖“的定義過程,然後通過廣播發送者將此”意圖“發送出去。被相應的BroadcastReceiver接收後將會回調onReceive()函數。
下段代碼片段顯示的是一個普通廣播的定義過程,併發送出去。其中setAction(..)對應於BroadcastReceiver中的intentFilter中的action。
Intent intent = new Intent();
intent.setAction(BROADCAST_ACTION);
intent.putExtra("name", "qqyumidi");
sendBroadcast(intent);
根據廣播的發送方式,可以將其分爲以下幾種類型:
- 1.Normal Broadcast:普通廣播
- 2.System Broadcast: 系統廣播
- 3.Ordered broadcast:有序廣播
- 4.Sticky Broadcast:粘性廣播(在 android 5.0/api 21中deprecated,不再推薦使用,相應的還有粘性有序廣播,同樣已經deprecated)
- 5.Local Broadcast:App應用內廣播
下面分別總結下各種類型的發送方式及其特點。
1).Normal Broadcast:普通廣播
此處將普通廣播界定爲:開發者自己定義的intent,以context.sendBroadcast_”AsUser”(intent, …)形式。具體可以使用的方法有:
- sendBroadcast(intent)/
- sendBroadcast(intent, receiverPermission)/
- sendBroadcastAsUser(intent, userHandler)/
- sendBroadcastAsUser(intent, userHandler,receiverPermission)。
普通廣播會被註冊了的相應的感興趣(intent-filter匹配)接收,且順序是無序的。如果發送廣播時有相應的權限要求,BroadCastReceiver如果想要接收此廣播,也需要有相應的權限。
2).System Broadcast: 系統廣播
Android系統中內置了多個系統廣播,只要涉及到手機的基本操作,基本上都會發出相應的系統廣播。如:開啓啓動,網絡狀態改變,拍照,屏幕關閉與開啓,點亮不足等等。每個系統廣播都具有特定的intent-filter,其中主要包括具體的action,系統廣播發出後,將被相應的BroadcastReceiver接收。系統廣播在系統內部當特定事件發生時,有系統自動發出。
3)Ordered broadcast:有序廣播
有序廣播的有序廣播中的“有序”是針對廣播接收者而言的,指的是發送出去的廣播被BroadcastReceiver按照先後循序接收。有序廣播的定義過程與普通廣播無異,只是其的主要發送方式變爲:sendOrderedBroadcast(intent, receiverPermission, …)。
對於有序廣播,其主要特點總結如下:
- 1>多個具當前已經註冊且有效的BroadcastReceiver接收有序廣播時,是按照先後順序接收的,先後順序判定標準遵循爲:將當前系統中所有有效的動態註冊和靜態註冊的BroadcastReceiver按照priority屬性值從大到小排序,對於具有相同的priority的動態廣播和靜態廣播,動態廣播會排在前面。
- 2>先接收的BroadcastReceiver可以對此有序廣播進行截斷,使後面的BroadcastReceiver不再接收到此廣播,也可以對廣播進行修改,使後面的BroadcastReceiver接收到廣播後解析得到錯誤的參數值。當然,一般情況下,不建議對有序廣播進行此類操作,尤其是針對系統中的有序廣播。
4)Sticky Broadcast:粘性廣播
(在 android 5.0/api 21中deprecated,不再推薦使用,相應的還有粘性有序廣播,同樣已經deprecated)。
既然已經deprecated,此處不再多做總結。
5)Local Broadcast:App應用內廣播(本地廣播)
(此處的App應用以App應用進程爲界)
由前文闡述可知,Android中的廣播可以跨進程甚至跨App直接通信,且註冊是exported對於有intent-filter的情況下默認值是true,由此將可能出現安全隱患如下:
- 1.其他App可能會針對性的發出與當前App intent-filter相匹配的廣播,由此導致當前App不斷接收到廣播並處理;
- 2.其他App可以註冊與當前App一致的intent-filter用於接收廣播,獲取廣播具體信息。
無論哪種情形,這些安全隱患都確實是存在的。由此,最常見的增加安全性的方案是:
- 1.對於同一App內部發送和接收廣播,將exported屬性人爲設置成false,使得非本App內部發出的此廣播不被接收;
- 2.在廣播發送和接收時,都增加上相應的permission,用於權限驗證;
- 3.發送廣播時,指定特定廣播接收器所在的包名,具體是通過intent.setPackage(packageName)指定在,這樣此廣播將只會發送到此包中的App內與之相匹配的有效廣播接收器中。
App應用內廣播可以理解成一種局部廣播的形式,廣播的發送者和接收者都同屬於一個App。實際的業務需求中,App應用內廣播確實可能需要用到。同時,之所以使用應用內廣播時,而不是使用全局廣播的形式,更多的考慮到的是Android廣播機制中的安全性問題。
相比於全局廣播,App應用內廣播優勢體現在:
- 1.安全性更高;
- 2.更加高效;
- 3.正在發送的廣播不會離開應用程序,不必擔心機密數據泄露;
- 4.其他程序無法將廣播發送到應用程序內部,不擔心安全漏洞;
- 5.發送本地廣播比發送全局廣播高效。
爲此,Android v4兼容包中給出了封裝好的LocalBroadcastManager類,用於統一處理App應用內的廣播問題,使用方式上與通常的全局廣播幾乎相同,只是註冊/取消註冊廣播接收器和發送廣播時將主調context變成了LocalBroadcastManager的單一實例。
使用方法:
通過LocalBroadcastManager的getInstance()方法獲得實例,動態註冊。
//registerReceiver(mBroadcastReceiver, intentFilter);
//註冊應用內廣播接收器
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);
//unregisterReceiver(mBroadcastReceiver);
//取消註冊應用內廣播接收器
localBroadcastManager.unregisterReceiver(mBroadcastReceiver);
Intent intent = new Intent();
intent.setAction(BROADCAST_ACTION);
intent.putExtra("name", "qqyumidi");
//sendBroadcast(intent);
//發送應用內廣播
localBroadcastManager.sendBroadcast(intent);
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (null != intent && intent.getAction().equalsIgnoreCase(FILTER_CHANGE_XXX)) {
String filter = intent.getExtras().getString(KEY_XXX_FILTER);
resetFilter(filter);// Do Something
}
}
};
註冊:
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, new IntentFilter(FILTER_CHANGE_XXX));
取消註冊:
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
4.不同註冊方式的廣播接收器回調onReceive(context, intent)中的context具體類型
- 1).對於靜態註冊的ContextReceiver,回調onReceive(context, intent)中的context具體指的是ReceiverRestrictedContext;
- 2).對於全局廣播的動態註冊的ContextReceiver,回調onReceive(context, intent)中的context具體指的是Activity Context;
- 3).對於通過LocalBroadcastManager動態註冊的ContextReceiver,回調onReceive(context, intent)中的context具體指的是Application Context。
注:對於LocalBroadcastManager方式發送的應用內廣播,只能通過LocalBroadcastManager動態註冊的ContextReceiver纔有可能接收到(靜態註冊或其他方式動態註冊的ContextReceiver是接收不到的)。
5.不同Android API版本中廣播機制相關API重要變遷
1).Android5.0/API level 21開始粘滯廣播和有序粘滯廣播過期,以後不再建議使用;
2).”靜態註冊的廣播接收器即使app已經退出,主要有相應的廣播發出,依然可以接收到,但此種描述自Android 3.1開始有可能不再成立“
Android 3.1開始系統在Intent與廣播相關的flag增加了參數,分別是FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES。
- FLAG_INCLUDE_STOPPED_PACKAGES:包含已經停止的包(停止:即包所在的進程已經退出)
- FLAG_EXCLUDE_STOPPED_PACKAGES:不包含已經停止的包
主要原因如下:
自Android3.1開始,系統本身則增加了對所有app當前是否處於運行狀態的跟蹤。在發送廣播時,不管是什麼廣播類型,系統默認直接增加了值爲FLAG_EXCLUDE_STOPPED_PACKAGES的flag,導致即使是靜態註冊的廣播接收器,對於其所在進程已經退出的app,同樣無法接收到廣播。
詳情參加Android官方文檔:http://developer.android.com/about/versions/android-3.1.html#launchcontrols
由此,對於系統廣播,由於是系統內部直接發出,無法更改此intent flag值,因此,3.1開始對於靜態註冊的接收系統廣播的BroadcastReceiver,如果App進程已經退出,將不能接收到廣播。
但是對於自定義的廣播,可以通過複寫此flag爲FLAG_INCLUDE_STOPPED_PACKAGES,使得靜態註冊的BroadcastReceiver,即使所在App進程已經退出,也能能接收到廣播,並會啓動應用進程,但此時的BroadcastReceiver是重新新建的。
Intent intent = new Intent();
intent.setAction(BROADCAST_ACTION);
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
intent.putExtra("name", "qqyumidi");
sendBroadcast(intent);
注1:對於動態註冊類型的BroadcastReceiver,由於此註冊和取消註冊實在其他組件(如Activity)中進行,因此,不受此改變影響。
注2:在3.1以前,相信不少app可能通過靜態註冊方式監聽各種系統廣播,以此進行一些業務上的處理(如即時app已經退出,仍然能接收到,可以啓動service等..),3.1後,靜態註冊接受廣播方式的改變,將直接導致此類方案不再可行。於是,通過將Service與App本身設置成不同的進程已經成爲實現此類需求的可行替代方案。
最近在開發第三方SDK,其中要發一條廣播給設備上所有集成了SDK的應用,觸發一些邏輯,可是遇到一個問題,當通過“應用管理器”將應用“強制停止”後,被停止的應用就無法接收到廣播,靜態註冊的廣播也沒有用。查閱了相關資料發現了問題所在。
在android3.1以後的版本中,如果程序被強制停止後應用狀態會被標記爲STOPPED,此時應用無法收到其他應用的廣播,要等到應用再開啓一次,將STOPPED去掉以後纔可以。除此之外還有一個解決方法如下:
解決辦法:
在廣播發發送方發送廣播時需要設置Intent.FLAG_INCLUDE_STOPPED_PACKAGES
示例代碼:
Intent intent = new Intent();
intent.setAction("com.leeliwei.test.receiver.action.test");
if (android.os.Build.VERSION.SDK_INT >= 12) {
intent.setFlags(32);//3.1以後的版本需要設置Intent.FLAG_INCLUDE_STOPPED_PACKAGES
}
sendBroadcast(intent);
參照代碼再多說兩句高低版本兼容問題,if判斷中爲什麼寫>=12而不是大於>= android.os.Build.VERSION_CODES.HONEYCOMB_MR1這種常量形式。答案是我的sdk兼容從2.2版本開始的,如果寫成常量形式的會報編譯錯誤,如果調高了targetVersion(開發綁定版本),雖然編譯沒有錯誤,但在低版本運行時,會因爲找不到那個常量值兒報異常終止,所以建議使用數字12。同理,intent.setFlags(32)中沒有直接寫常量也是這個原因,Intent.FLAG_INCLUDE_STOPPED_PACKAGES是3.1版本時纔有的。低版本會報錯。
完整廣播示例:短信攔截
系統在收到短信的時候,會發送一個:android.provider.Telephony.SMS_RECEIVED這樣的廣播,而且這是一個有序的廣播,所以我們就可以攔截了這條短信,因爲系統中的短信接收者的訂閱優先級不是1000最高的,所以我們可以自己定義一個短信接收者,將訂閱優先級設置成1000,這樣我們就可以最先獲取到短信內容,然後將截取到知道號碼的短信內容上傳到服務器上進行保存,然後終止廣播。讓系統接收不到這條短信。
這裏面我們還需要了解的技術就是短信內容的獲取,這個我們在代碼中解釋:
下面來看一下我們定義的短信接收者:
package com.broadcast.receiver;
import java.text.SimpleDateFormat;
import java.util.Date;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.SmsMessage;
import android.util.Log;
import com.broadcast.service.UploadSMSService;
/**
* 廣播接收者A
* @author weijiang204321
*
*/
public class SortBroadcastReceiverA extends BroadcastReceiver{
@SuppressLint("SimpleDateFormat")
@Override
public void onReceive(Context context, Intent intent) {
//獲取短信的相關信息,使用字段pdus
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
StringBuilder content = new StringBuilder();
String receiveTime = "";
String senderNumber = "";
for(Object p : pdus){
byte[] pdu = (byte[]) p;
SmsMessage message = SmsMessage.createFromPdu(pdu);
content.append(message.getMessageBody());
Date date = new Date(message.getTimestampMillis());
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
receiveTime = format.format(date);
senderNumber = message.getOriginatingAddress();
}
Log.e("Demo:","上傳短信內容是:"+content.toString());
Log.e("Demo:","接收短信的時間是"+receiveTime);
Log.e("Demo:","發送短信的號碼是:"+senderNumber);
//攔截的號碼是:18910958627,注意前面還有+86是中國地區的編號
if("+8618910958627".equals(senderNumber)){
//攔截成功 ,上傳信息
Intent service = new Intent(context,UploadSMSService.class);
service.putExtra("content", content.toString());
service.putExtra("receiveTime",receiveTime);
service.putExtra("senderNumber", senderNumber);
context.startService(service);
}
}
}
在onReceive方法中我們通過intent中的Bundle獲取短信的相關信息。
下面在看一下上傳短信信息的服務Service:
package com.broadcast.service;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
/**
* 上傳短信內容的service
*
*/
public class UploadSMSService extends Service{
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//獲取短信內容和接收時間,發送者的號碼
final String content = intent.getStringExtra("content");
final String receiveTime = intent.getStringExtra("receiveTime");
final String senderNumber = intent.getStringExtra("senderNumber");
new Thread(){
@Override
public void run(){
sendSMS(content,receiveTime,senderNumber);
//上傳完成之後就結束service
stopSelf();
}
}.start();
return super.onStartCommand(intent, flags, startId);
}
/**
* 上傳短信內容
* @param content
* @param receiveTime
* @param senderNumber
* @return
*/
private boolean sendSMS(String content, String receiveTime, String senderNumber) {
try{
String params = "content="+ URLEncoder.encode(content, "UTF-8")+"&receivetime="+ receiveTime+ "&sendernumber="+ senderNumber;
byte[] entity = params.getBytes();
String path = "http://10.2.86.33:8080/ReceiveSMSContent/ReceiveSMSServlet";
HttpURLConnection conn = (HttpURLConnection) new URL(path).openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("Content-Length", String.valueOf(entity.length));
conn.getOutputStream().write(entity);
if(conn.getResponseCode() == 200){
return true;
}
}catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
這個服務的邏輯也很簡單的,在獲取到短信信息的時候,將內容進行上傳。
最後不能忘了在AndroidMainfest.xml中添加權限:
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.INTERNET"/>
訂閱短信廣播:
<receiver android:name="com.broadcast.receiver.SortBroadcastReceiverA">
<intent-filter android:priority="999">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
當然這裏還需要有一個短信內容的接收服務端,在服務端定義一個Servlet來接受數據,具體的服務端的環境搭建,自己上網搜索相關資料進行搭建:
package com.servlet.receivesms;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ReceiveSMSServlet extends HttpServlet{
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String content = req.getParameter("content");
String receiveTime = req.getParameter("receivetime");
String phoneNumber = req.getParameter("sendernumber");
System.out.println("發送內容:"+content);
System.out.println("發送時間:"+receiveTime);
System.out.println("發送號碼:"+phoneNumber);
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
super.doPost(req, resp);
}
}
測試結果,客戶端攔截的號碼是18910958627,注意前面有+86是中國區域的編碼,服務端接收數據爲:
這樣就表示截取短信內容成功,並且成功上傳到服務器上了。
對於上面的短信攔截的功能還有待加強,就是現在想把短信的內容攔截下來,進行篡改,然後再發給下一個接收者?
方案一:在我們定義的短信廣播接收者中,我們能夠從intent中的Bundle中通過key=”pdus”來獲取短信內容的,那麼我們可以自己從新組建一個新的Bundle,在這個Bundle中存入我們篡改的信息,然後替換之前的Bundle
問題:實施了,但是問題是Bundle是替換不了的,不知道是什麼原因?
方案二:由於第一種方案的失敗,導致了我從新想到一個方案就是截取短信內容之後,終止此次廣播,然後發送一條短信,這時候短信的內容爲我們篡改的內容,但是這裏需要注意的是,要做判斷,因爲我們定義的短信接收者的優先級最高,所以我們發送篡改後的短信又被我們的短信接收者給攔截了,但是我們是不想這樣的,所以要做個判斷,這個很簡單的,使用SharePerenced存入一個boolean值就行了。
問題:本來以爲這種方案是完美了,但是問題是發送信息的時候,系統提供的方法中的參數是:接收者號碼,發送者號碼,短信內容,還有其他的參數就不解釋了,這很簡單呀,我們現在正好需要這三個參數,立馬傳遞進去,結果是收取不到短信,查看文檔,發現那個發送者號碼的參數是短信服務中心的號碼(也不知道什麼意思),當把它設置成null的時候短信就可以發出去了,但是設置成null的話,就不能說是誰發的短信了沒有意義呀!
問題還沒有解決,如果有哪位大神有好的方法,請說明!
總結:
在Android中,程序的響應(Responsive)被活動管理器(ActivityManager)和窗口管理器(Window Manager)這兩個系統服務所監視。當BroadcastReceiver在10秒內沒有執行完畢,Android會認爲該程序無響應。所以在BroadcastReceiver裏不能做一些比較耗時的操作,否側會彈出ANR(Application No Response)的對話框。如果需要完成一項比較耗時的工作,應該通過發送Intent給Service,由Service來完成。而不是使用子線程的方法來解決,因爲BroadcastReceiver的生命週期很短(在onReceive()執行後BroadcastReceiver 的實例就會被銷燬),子線程可能還沒有結束BroadcastReceiver就先結束了。如果BroadcastReceiver結束了,它的宿主進程還在運行,那麼子線程還會繼續執行。但宿主進程此時很容易在系統需要內存時被優先殺死,因爲它屬於空進程(沒有任何活動組件的進程)。
public class IncomingSMSReceiver extends BroadcastReceiver {
@Override public void onReceive(Contextcontext, Intentintent) {
//發送Intent啓動服務,由服務來完成比較耗時的操作
Intent service =new Intent(context,XxxService.class);
context.startService(service);
}
}
每次廣播消息到來時都會創建BroadcastReceiver實例並執行onReceive() 方法。
所以上面的短信內容上傳到服務器上的邏輯功能不能在廣播中執行,需要開啓一個服務進行上傳。
本文參考了互聯網上的一些內容,苦於找不到原文出處,在此表示感謝!