5. 全局大喇叭-详解广播机制

03.jpg

第5章 全局大喇叭-详解广播机制

了解网络通信原理的应该会知道,在一个IP网络范围中,最大的IP地址是被保留作为广播地址来使用的。比如某个网络的IP范围是192.168.0.XXX,子网掩码是255.255.255.0,那么这个网络的广播地址就是192.168.0.255。广播数据包会被发送到同一网络上的所有端口,这样在该网络中的每台主机都将会收到这条广播。

1.广播机制简介

Android中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会接收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的。Android提供了一套完整的API,允许应用程序自由的发送和接收广播。

  • 标准广播
    是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。

  • 有序广播
    则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。

2.动态注册监听网络变化

注册广播的方式一般有两种,在代码中注册和在AndroidManifest.xml中注册,其中前者被称为动态注册,后者被称为静态注册

创建一个广播接收器,需要新建一个类,让它继承自BroadcastReceiver,并重写父类的onReceive()方法就可以了。

public class MainActivity extends AppCompatActivity {

    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(MainActivity.this, "network changes", Toast.LENGTH_SHORT).show();
        }

    }
}

MainActivity中定义了一个内部类NetworkChangeReceiver,这个类是继承自BroadcastReceiver的,并重写了父类的onReceive()方法。
onCreate()方法中,首先我们创建了一个IntentFilter的实例,并给它添加一个值为android.net.conn.CONNECTIVITY_CHANGEaction,我们的广播接收器想要监听什么广播,就在这里添加相应的action,接下来创建了一个NetworkChangeReceiver的实例,然后调用registerReceiver()方法进行注册,将NetworkChangeReceiver的实例和IntentFilter的实例都传了进去,这样NetworkChangeReceiver就会收到所有值为android.net.conn.CONNECTIVITY_CHANGE的广播。

动态注册的广播接收器一定都要取消注册才行,这里我们是在onDestroy()方法中通过调用unregisterReceiver()方法来实现的。

class NetworkChangeReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            //Toast.makeText(MainActivity.this, "network changes", Toast.LENGTH_SHORT).show();
            ConnectivityManager connectionManager = (ConnectivityManager)
                    getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();
            if (networkInfo != null && networkInfo.isAvailable()) {
                Toast.makeText(MainActivity.this, "network is available", Toast.LENGTH_SHORT).show();
            }
            else {
                Toast.makeText(MainActivity.this, "network is unavailable", Toast.LENGTH_SHORT).show();
            }

        }

    }

首先通过getSystemService()方法得到了ConnectivityManager的实例,这是一个系统服务类,专门用于管理网络连接的。然后调用它的getActiveNetworkInfo()方法可以得到NetworkInfo的实例,接着调用NetworkInfoisAvailable()方法,就可以判断出当前是否有网络了。

Android系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,就必须在配置文件中声明权限才可以,否则程序将会直接崩溃

3.静态注册实现开机启动

动态注册的广播接收器可以自由地控制注册和注销,在灵活性方面有很大的优势,但是它也存在着一个缺点,即必须要在程序启动之后才能接收到广播,因为注册的逻辑是写在onCreate()方法中的,让程序在未启动的情况下就能接收到广播,需要使用静态注册的方式

image

  • Exported: 表示是否允许这个广播接收器接收本程序以外的广播
  • Enabled: 表示是否启用这个广播接收器

静态的广播接收器一定要在AndroidManifest.xml文件中注册才可以使用。

<receiver
    android:name=".BootCompleteReceiver"
    android:enabled="true"
    android:exported="true">
</receiver>

<application>标签内出现了一个新的标签<receiver>,所有静态的广播接收器都是在这里进行注册的,它的用法其实和<activity>标签非常相似,也是通过android:name来指定具体注册哪一个广播接收器。

<receiver
      android:name=".BootCompleteReceiver"
      android:enabled="true"
      android:exported="true">
      <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
</receiver>

Android系统启动完成后会发出一道值为android.intent.action.BOOT_COMPLETED的广播,因此我们在<intent-filter>标签里添加了相应的action。另外,监听系统开机广播也是需要声明系统权限的,我们使用<uses-permission>标签又加入了一条android.permission.RECEIVE_BOOT_COMPLETED权限。

需要注意的是,不要再onReceiver()方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会报错,因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态栏通知,或者启动一个服务等。

4.发送自定义广播

// 1. 
 public void initView() {
   button.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v){
           Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
           sendBroadcast(intent);
       }
   });
}

// 2.
<receiver
    android:name=".MyBroadcastReceiver"
    android:exported="true"
    android:enabled="true">

    <intent-filter>
        <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
    </intent-filter>

</receiver>

首先构建出了一个Intent对象,并把要发送的广播的值传入,然后调用了ContextsendBroadcast()方法将广播发送出去,这样所有监听com.example.broadcasttest.MY_BROADCAST这条广播的广播接收器就会收到消息。

广播是使用Intent进行传递的,因此你还可以在Intent中携带一些数据传递给广播接收器。

发送有序广播

广播是一种可以跨进程的通信方式,我们应用程序发出的广播,其他的应用程序也是可以收到的。

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
        sendOrderedBroadcast(intent,null);
    }
});

sendBroadcast()方法改成sendOrderedBroadcast()方法。sendOrderedBroadcast()方法接收两个参数,第一个参数仍然是Intent,第二个参数是一个与权限相关的的字符串,这里传入null就行了。

<receiver
    android:name=".AnotherBroadcastReceiver"
    android:exported="true"
    android:enabled="true">

    <intent-filter
        android:priority="100">
        <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
    </intent-filter>

</receiver>

通过android:priority属性给广播接收器设置了优先级,优先级比较高的广播接收器就可以先收到广播。

public class AnotherBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
        abortBroadcast();
    }
}

onReceiver()方法中调用了abortBroadcast()方法,就表示将这条广播截断,后面的广播接收器将无法在接收到这条广播。

5.使用本地广播

前面我们发送和接收的广播全部属于系统全局广播,即发出的广播可以被其他任何应用程序接收到,并且我们也可以接收到来自于其他任何应用程序的广播。
为了能够简单地解决广播的安全性问题,Android引入了一套本地广播机制,使用这个机制发出的广播只能在应用程序的内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播。
本地广播主要是使用了一个LocalBroadcastManager来对广播进行管理。

public class MainActivity extends AppCompatActivity
{

    private IntentFilter intentFilter;
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        localBroadcastManager = LocalBroadcastManager.getInstance(this);

        button = (Button) findViewById(R.id.button);
        initEvent();
        intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.broadcast.LOCAL_BROADCAST");
        localReceiver = new LocalReceiver();
        localBroadcastManager.registerReceiver(localReceiver,intentFilter);
    }

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

    public void initEvent()
    {
        button.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Intent intent = new Intent("com.example.broadcast.LOCAL_BROADCAST");
                localBroadcastManager.sendBroadcast(intent);
            }
        });
    }

    class LocalReceiver extends BroadcastReceiver
    {

        @Override
        public void onReceive(Context context, Intent intent)
        {
            Toast.makeText(context, "received in local broadcast", Toast.LENGTH_SHORT).show();
        }
    }
}

首先是通过LocalBroadcastManager的getInstance()方法得到了一个他的实例,然后在注册广播接收器的时候调用的是LocalBroadcastManager的sendBroadcast()方法。
另外还有一点需要说明,本地广播是无法通过静态注册的方式来接收的,其实这也完全可以理解,因为静态注册主要就是为了让程序在未启动的时候,也能收到广播,而发送本地广播时,我们的程序肯定是已经启动了,因此也完全不需要使用静态注册的功能。
**优势: **
1.可以明确地知道正在发送的广播不会离开我们的程序,因此不必担心机密数据泄露。
2.其他的程序无法将广播发送到我们程序的内部,因此不需要担心会有安全漏洞的隐患。
3.发送本地广播比发送系统全局广播将会更加高效。

6.广播的最佳实践-实现强制下线功能

实现强制下线功能的思路比较简单,只需要在界面上弹出一个对话框,让用户无法进行其他任何操作,必须要点击对话框中的确定按钮,然后回到登录界面即可。
强制下线功能需要先关闭掉所有的活动,然后回到登录界面。

public class ActivtyCollector
{
    public static List<Activity> activityList = new ArrayList<>();

    public static void addActivity(Activity activity)
    {
        activityList.add(activity);
    }

    public static void removeActivity(Activity activity)
    {
        activityList.remove(activity);
    }

    public static void finishAll()
    {
        for (Activity activity : activityList)
        {
            if (!activity.isFinishing())
            {
                activity.finish();
            }
        }
    }
}

Activity在Destroy之前,activity.isFinishing返回false,Activityon在Destroy之后,返回true

public class LoginActivity extends BaseActivity
{
    private Button button;
    private EditText editText_Account,editText_Password;

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

        initView();
        initEvent();
    }

    public void initView()
    {
        button = (Button) findViewById(R.id.button_login);
        editText_Account = (EditText) findViewById(R.id.edit_account);
        editText_Password = (EditText) findViewById(R.id.edit_password);
    }

    public void initEvent()
    {
        button.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                String account = editText_Account.getText().toString();
                String password = editText_Password.getText().toString();

                if (account.equals("admin") && password.equals("123456"))
                {
                    Intent intent = new Intent(LoginActivity.this,MainActivity.class);
                    startActivity(intent);
                    finish();
                }
                else
                {
                    Toast.makeText(LoginActivity.this, "输入的账号或密码有误!!!", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

public class MainActivity extends BaseActivity
{

    private Button button;
    public static String Tag = "com.example.broadcastbestpractice_FORCE_OFFLINE";

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

        button = (Button) findViewById(R.id.button_send);
        initEvent();
    }

    public void initEvent()
    {
        button.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Intent intent = new Intent(Tag);
                sendBroadcast(intent);
            }
        });
    }
}

MainActivity只是发送了一个标准广播。
强制用户下线的逻辑并不是写在MainActivity里的,而是应该写在接收这条广播的广播接收器里面,这样强制下线的功能就不会依附于任何的界面,不管是在程序的任何地方,只需要发出一条这样的广播,就可以完成强制下线的操作了。
注册的静态的广播接收器,是没有办法在onReceive()方法里弹出对话框这样的UI控件的,而我们显然也不可能在每个活动中都去注册一个动态的广播接收器。

public class BaseActivity extends AppCompatActivity
{
    private IntentFilter intentFilter;
    private ForceOfflineReceiver forceOfflineReceiver;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        ActivtyCollector.addActivity(this);
    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        ActivtyCollector.removeActivity(this);
    }

    @Override
    protected void onResume()
    {
        super.onResume();
        intentFilter = new IntentFilter();
        intentFilter.addAction(MainActivity.Tag);
        forceOfflineReceiver = new ForceOfflineReceiver();
        registerReceiver(forceOfflineReceiver,intentFilter);
    }

    @Override
    protected void onPause()
    {
        super.onPause();
        unregisterReceiver(forceOfflineReceiver);
    }

    class ForceOfflineReceiver extends BroadcastReceiver
    {

        @Override
        public void onReceive(Context context, final Intent intent)
        {
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("警告!");
            builder.setCancelable(false);
            builder.setMessage("账号在别处登录,你被迫下线,请重新登录");
            builder.setPositiveButton("确定", new DialogInterface.OnClickListener()
            {
                @Override
                public void onClick(DialogInterface dialog, int which)
                {
                    ActivtyCollector.finishAll();
                    Intent intent1 = new Intent(BaseActivity.this,LoginActivity.class);
                    startActivity(intent1);
                }
            });
            builder.show();
        }
    }
}

注意这里一定要调用builder.setCancelable(false)将对话框设为不可取消。
我们始终需要保证只有处于栈顶的活动才能接收到这条强制下线广播,非栈顶的活动不应该也没有必要去接收这条广播,所以写在onResume()和onPause()方法里就可以很好的解决这个问题,当一个活动失去栈顶位置时就会自动取消广播接收器的注册。

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

推荐阅读更多精彩内容