Android Broadcast

参考承香墨影的两篇博客
Android--广播BroadcastReceiver
Android--拦截系统BroadcastReceiver

一、什么是BroadcastReceiver?

BroadcastReceiver,广播接收者,它是一个系统全局的监听器,用于监听系统全局的Broadcast消息,所以它可以很方便的进行系统组件之间的通信。
  BroadcastReceiver虽然是一个监听器,但是它和之前用到的OnXxxListener不同,那些只是程序级别的监听器,运行在指定程序的所在进程中,当程序退出的时候,OnXxxListener监听器也就随之关闭了,但是BroadcastReceiver属于系统级的监听器,它拥有自己的进程,只要存在与之匹配的Broadcast被以Intent的形式发送出来,BroadcastReceiver就会被激活。
  虽然同属Android的四大组件,BroadcastReceiver也有自己独立的声明周期,但是和Activity、Service又不同。当在系统注册一个BroadcastReceiver之后,每次系统以一个Intent的形式发布Broadcast的时候,系统都会创建与之对应的BroadcastReceiver广播接收者实例,并自动触发它的onReceive()方法,当onReceive()方法被执行完成之后,BroadcastReceiver的实例就会被销毁。虽然它独自享用一个单独的进程,但也不是没有限制的,如果BroadcastReceiver.onReceive()方法不能在10秒内执行完成,Android系统就会认为该BroadcastReceiver对象无响应,然后弹出ANR(Application No Response)对话框,所以不要在BroadcastReceiver.onReceive()方法内执行一些耗时的操作。
  如果需要根据广播内容完成一些耗时的操作,一般考虑通过Intent启动一个Service来完成该操作,而不应该在BroadcastReceiver中开启一个新线程完成耗时的操作,因为BroadcastReceiver本身的生命周期很短,可能出现的情况是子线程还没有结束,BroadcastReceiver就已经退出的情况,而如果BroadcastReceiver所在的进程结束了,该线程就会被标记为一个空线程,根据Android的内存管理策略,在系统内存紧张的时候,会按照优先级,结束优先级低的线程,而空线程无异是优先级最低的,这样就可能导致BroadcastReceiver启动的子线程不能执行完成。

二、BroadcastReceiver的种类

上面提到,当系统以一个Intent的形式发送一个Broadcast出去之后,所有与之匹配的BroadcastReceiver都会被实例化,但是这里是有区别的,根据Broadcast的传播方式区别,在系统中有如下两种Broadcast:

  • 普通广播
    Normal Broadcast,它是完全异步的,也就是说,在逻辑上,当一个Broadcast被发出之后,所有的与之匹配的BroadcastReceiver都同时接收到Broadcast。优点是传递效率比较高,但是也有缺点,就是一个BroadcastReceiver不能影响其他响应这条Broadcast的BroadcastReceiver。
  • 有序广播
    Ordered Broadcast,它是同步执行的,也就是说有序广播的接收器将会按照预先声明的优先级依次接受Broadcast,是链式结构,优先级越高(-1000~1000),越先被执行。因为是顺序执行,所有优先级高的接收器,可以把执行结果传入下一个接收器中,也可以终止Broadcast的传播(通过abortBroadcast()方法),一旦Broadcast的传播被终止,优先级低于它的接收器就不会再接收到这条Broadcast了。

虽然系统存在两种类型的Broadcast,但是一般系统发送出来的Broadcast均是有序广播,所以可以通过优先级的控制,在系统内置的程序响应前,对Broadcast提前进行响应。这就是市场上一些拦截器类(如:短信拦截器、电话拦截器)的软件的原理。

三、如何发送一个广播

上面已经介绍了系统中两种不同的Broadcast,而根据Broadcast传播的方式,Context提供了不同的方法来发布它们:
sendBroadcast():发送普通广播。
sendOrderedBroadcast():发送有序广播。
  以上两个方法都有多个重载方法,根据不同的场景使用,最简单的莫过于直接传递一个Intent来发送一个广播。
<pre>
Intent intent = new Intent("com.example.boadcasttest.MY_BROADCAST");
sendBroadcast(intent);
</pre>

四、如何使用BroadcastReceiver

BroadcastReceiver本质上还是一个监听器,所以使用BroadcastReceiver的方法也是非常简单,只需要继承BroadcastReceiver,在其中重写onReceive(Context context,Intent intent)即可。一旦实现了BroadcastReceiver,并部署到系统中后,就可以在系统的任何位置,通过sendBroadcast、sendOrderedBroadcast方法发送Broadcast给这个BroadcastReceiver。
  但是仅仅继承BroadcastReceiver和实现onReceive()方法是不够的,同为Android系统组件,它也必须在Android系统中注册,注册一个BroadcastReceiver有两种方式:

  • 动态注册
    在代码中使用Content.registerReceiver(BroadcastReceiver receiver, IntentFilter filter)进行注册,在使用完毕使用Content.unregisterReceiver(BroadcastReceiver receiver)方法进行注销。
    <pre>
    public class MainActivity extends Activity{
    private IntentFilter intentFilter;
    private NeworkChangeReceiver networkChangeReceiver;

    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 NeworkChangeReceiver();
    registerReceiver(networkChangeReceiver ,intentFilter);
    }

    protected void onDestroy(){
    super.onDestroy();
    unregisterReceiver(networkChangeReceiver);
    }

    class NeworkChangeReceiver extends BroadcastReceiver{
    public void onReceive(Context context,Intent intent){
    ConnectivityManger connectivityManger = new (ConnectivityManger) getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connectivityManger.getActiveNetworkInfo();
    if(networkInfo != null && networkInfo .isAvailable()){
    Toast.makeText(context,"net is available,Toast.LENGTH_SHORT").show();
    }else{
    Toast.makeText(context,"net is unavailable,Toast.LENGTH_SHORT").show();
    }
    }
    }
    }

</pre>
注意在AndroidMainfest.xml声明查询系统网络状态的权限
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

  • 静态注册
    使用清单文件AndroidManifest.xml注册,在<application/>节点中,使用<receiver/>节点注册,并用android:name属性中指定注册的BroadcastReceiver对象,一般还会通过<Intent-filter/>指定<action/>和<category/>,并在<Intent-filter/>节点中通过android:priority属性设置BroadcastReceiver的优先级,在-1000~1000范围内,数值越到优先级越高。
      虽然Android系统提供了两种方式注册BroadcastReceiver,但动态注册必须要在程序启动之后才能接收广播,如果想在程序未启动情况下就接收广播,就只能使用静态注册了。一般在实际开发中,还是会使用清单文件进行静态注册:
    <pre>
    <receiver android:name="cn.bgxt.Broadcastdemo.Basic.BasicBroadcast">
    <intent-filter android:priority="100">
    <action android:name="cn.bgxt.Broadcastdemo.Basic.broadcast"/>
    </intent-filter>
    </receiver>
    </pre>
    下面通过一个简单的示例,讲解一下BroadcastReceiver的声明,以及如何向这个BroadcastReceiver发送消息。
      首先先声明一个BroadcastReceiver,BasicBroadcast.java:
    <pre>
    package cn.bgxt.Broadcastdemo.Basic;
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.widget.Toast;
    public class BasicBroadcast extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    Toast.makeText(context,
    "接收到Broadcast,消息为:" + intent.getStringExtra("msg"),
    Toast.LENGTH_SHORT).show();
    }
    }
    </pre>

再声明一个Activity,用于发送Broadcast:BasicActivity.java:
<pre>
package cn.bgxt.Broadcastdemo.Basic;

import com.bgxt.datatimepickerdemo.R;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class BasicActivity extends Activity {
Button btnBasicSendNormal, btnBasicSendOrdered;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_basic);

    btnBasicSendNormal = (Button) findViewById(R.id.btnBasicSendNormal);
    btnBasicSendOrdered = (Button) findViewById(R.id.btnBasicSendOrdered);
    btnBasicSendNormal.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            Intent broadcast=new Intent();
            broadcast.setAction("cn.bgxt.Broadcastdemo.Basic.broadcast");
            broadcast.putExtra("msg", "这是一个普通广播");
            sendBroadcast(broadcast);
        }
    });

    btnBasicSendOrdered.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            Intent broadcast=new Intent();
            broadcast.setAction("cn.bgxt.Broadcastdemo.Basic.broadcast");
            broadcast.putExtra("msg", "这是一个有序广播");
            sendOrderedBroadcast(broadcast, null);
        }
    });
}

}
</pre>

在实际开发当中,大部分情况下是不需要自己发布一个Broadcast或者接收自己定义的Broadcast的,一般而言,都是拦截系统在做某个操作而发布的Broadcast,对其进行相应的处理。

五、系统中的广播

在Android系统中,内置了很多Action常量,在触发这些Action的时候,均会发布相应的Broadcast。一般而言,查看Android的API文档中,关于Intent的说明即可找到对应Action的Broadcast,但是列举的还不是很全,最好还是下载Android的源代码,通过查看源代码的方式查看需要拦截的Broadcast。下面列举一些常用的广播:

  • android.intent.action.TIME_SET:系统时间被修改。
  • android.intent.action.DATE_CHANGED:系统日期被修改。
  • android.intent.action.BOOT_COMPLETED:系统启动完成。
  • android.intent.action.BATTERY_CHANGED:设备电量改变。
  • android.intent.action.BATTERY_LOW:设备电量低。
  • android.intent.action.ACTION_POWER_CONNECTED:设备连接电源。
  • android.intent.action.ACTION_POWER_DISCONNECTED:设备断开电源。
  • android.provider.Telephony.SMS_RECEIVED:系统收到短信。
  • android.intent.action.NEW_OUTGOING_CALL:拨打电话。

下面通过两个例子,来讲解如何在Android下,拦截系统Broadcast并对其进行处理。

1.通过关键字拦截短信

从上面列举的一些动作会发布的Broadcast,可以找到,当系统接收到一条短信的时候,会发布一个“android.provider.Telephony.SMS_RECEIVED”的Broadcast,之前已经介绍过了,一般系统Broadcast都是有序广播,如果不被高优先级的BroadcastReceiver停止传递,会按照优先级顺序传递下去。
  而在这个示例中,通过监听接收短信的广播,当其内容有黑名单中的关键字的话,则阻止Broadcast继续传播,并使用Toast提示,否则正常提示短信信息。
  通过上一篇博客了解到,onReceive方法的Intent参数包含了这条广播传递的参数,对于短信信息而言,需要获取key为"pdus"的数组,取出数组中每一项,它的每一项代表了一个byte[]格式的短信,需要使用SmsMessage类解析短信内容。
  当然,拦截短信的Broadcast侵犯了隐私,需要注册接收短信的权限:
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
  下面直接展示源代码了,关键注释已经写的很清楚了,这里不再累述:
  MessageBroadcast.java:
<pre>
package cn.bgxt.Broadcastdemo.MessageWarn;

import java.text.SimpleDateFormat;
import java.util.Date;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.widget.Toast;

public class MessageBroadcast extends BroadcastReceiver {
// 在模拟器上,通过DDMS发送短信会产生乱码,所以使用拼音代替
//在真机上不存在乱码的问题
private final String[] blackKeyWord = new String[] { "baoxian", "chuxiao",
"jiangjia" };

@Override
public void onReceive(Context context, Intent intent) {
    // 判断当前接收到的Broadcast是否是收到短信的action
    if (intent.getAction()
            .equals("android.provider.Telephony.SMS_RECEIVED")) {
        StringBuilder sb = new StringBuilder();
        // 获取Broadcast传递的数据
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
            Object[] pdus = (Object[]) bundle.get("pdus");
            for (Object p : pdus) {
                byte[] pud = (byte[]) p;
                // 声明一个SmsMessage,用于解析短信的byte[]数组
                SmsMessage message = SmsMessage.createFromPdu(pud);
                boolean flag = false;
                for (String str : blackKeyWord) {
                    if (message.getMessageBody().contains(str) ) {
                        // 发现黑名单关键字,则标记为true
                        flag = true;
                        break;
                    }
                }
                if (flag) {
                    sb.append("发件人:\n");
                    sb.append(message.getOriginatingAddress());
                    sb.append("\n发送时间:\n");
                    Date date = new Date(message.getTimestampMillis());
                    SimpleDateFormat format = new SimpleDateFormat(
                            "yyyy-MM-dd HH:mm:ss");
                    sb.append(format.format(date));
                    sb.append("\n短信内容:\n");
                    sb.append(message.getMessageBody());

                    Toast.makeText(context, sb.toString(),
                            Toast.LENGTH_SHORT).show();
                    // 如果存在黑名单关键字内容,停止Broadcast传播
                    abortBroadcast();
                }
                
            }
        }
    }

}

}
</pre>

在AndroidManifest.xml中配置Receiver。
<pre>
<receiver android:name="cn.bgxt.Broadcastdemo.MessageWarn.MessageBroadcast">

<intent-filter android:priority="200">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
</pre>

Paste_Image.png

2.IP拨号
再来看看IP拨号的示例,在Android中,如果触发拨打电话的Action,则会发布一个"android.intent.action.NEW_OUTGOING_CALL"的Broadcast出来,只需要针对它进行拦截即可,然后在加上IP前缀,把处理过的号码添加到数据传递给下一个Receiver。
  处理接收拨打电话的Broadcast,需要对Android增加权限:
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
 下面直接上代码了,注释写的很清楚,这里不再累述了。
  IpCallPhone.java:
<pre>
package cn.bgxt.Broadcastdemo.IpCall;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class IpCallPhone extends BroadcastReceiver {
private final String STARTS="17951";
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前拨号的号码
String number=getResultData();
// 此号码没有被加IP拨号的前缀
if(!number.startsWith(STARTS)){
// 设置加了IP号码的号码
String newnumber=STARTS+number;
// 把新号码增加到返回结果数据中,用于传递给后面的Receiver
setResultData(newnumber);
}
}
}
</pre>

AndroidManifest.xml配置Receiver:

<pre><receiver android:name="cn.bgxt.Broadcastdemo.IpCall.IpCallPhone">
<intent-filter android:priority="200">
<action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
</intent-filter>
</receiver>
</pre>

效果展示:

Paste_Image.png

Paste_Image.png
六、使用本地广播

本地广播不需要担心机密数据泄露,因为发送的广播不会离开本地程序。同样,其他程序也无法将广播发送到我们程序内部,因此不用担心安全漏洞隐患。另外,本地广播比系统全局广播更高效。
需要注意的是,本地广播无法通过静态注册方式使用。这是因为静态注册主要是为了让程序未启动也能接收广播,而发送本地广播时,程序肯定已经启动了。
<pre>
public class MainActivity extends Activity{
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;

private NeworkChangeReceiver networkChangeReceiver;

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(){
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);
}

protected void onDestroy(){
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}

class LocalReceiver extends BroadcastReceiver{
public void onReceive(Context context,Intent intent){
Toast.makeText(context, "received local broadcast",
Toast.LENGTH_SHORT).show();
}
</pre>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容