Android广播(接收,发送,标准,有序,全局,本地,实战,填坑)

前言

写完了,人也麻了,本来想着挺好李姐的东西,咋能写这么长一串,确实,难顶。写的还挺仔细的。
在这里插入图片描述

一、广播是什么?

通俗点李姐,广播就是安卓系统本身发出的声音,我们可以通过安卓提供给我们的一系列内容来接收和发出广播,以此来简单快捷地实现一些功能。

二、广播分类

1.标准广播

标准广播是完全异步执行(异步同步详情见百度,概念很好李姐)同时不可被截断,也就是说当前发出的一号广播,会被ABC三个接收器同时接收到。如下图
在这里插入图片描述

2.有序广播

顾名思义,一种有序的,同步的广播,它是按照次序来传递,先被优先级高的接收器接收到,响应完成后再传递给接收器B然后C,同时可以进行截断,比如在A处截断,后续B和C就无法接收到广播。如下图
在这里插入图片描述

三、广播的使用(接收器)

上面介绍了广播的基本信息,接下来进行一些广播的简单使用。

1.广播的注册

使用广播我们首先要进行广播的注册,注册广播有两种方式,动态注册和静态注册。动态注册的优点是可以实现灵活的广播注册和注销,但缺点就是必须要程序启动后才能接收到广播。如果想要在程序未启动时,比如刚开机的情况下接受到系统的开机广播,那就需要使用静态注册,但静态注册长期监听,消耗更多资源,因此大部分情况建议优先使用动态注册解决问题。

1.1动态注册实例

广播动态注册需要我们在代码中进行注册,首先写一个广播接收器,继承自BroadcastReceiver类

public class myReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        //事务处理代码
        //在这里写上相关的处理代码,一般来说,不要此添加过多的逻辑或者是进行任何的耗时操作
        //因为广播接收器中是不允许开启多线程的,过久的操作就会出现报错
        //因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态栏通知,或者启动某个服务
        String action = intent.getAction();
        if (Intent.ACTION_SCREEN_OFF.equals(action)) {
            //屏幕关闭
            Log.d(TAG, "屏幕关闭");
        } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
            Log.d(TAG, "屏幕打开");
        }
    }
}

这里我们实现的广播功能是监听屏幕的亮起和关闭。上面这片代码整体结构很容易理解,定义一个action作为接收到的广播信息,同时屏幕亮起和关闭的相关广播内容是安卓自带的,直接使用就行。在这里插入图片描述
逻辑部分就更容易理解了,equals方法判断打印。
到此是我们接收器中写的东西。接下来看主活动中要做什么。
首先第一步,将接收器实例化,这个用牛牛想都知道要做这个,然后,广播对吧,要设置频道,然后完成对广播的注册,在使用完广播后,要对广播进行注销。一步一步来,首先实例化接收器:
在这里插入图片描述

//      创建接收器
        myreceiver = new myReceiver();

ps:将其设置为全局变量,便于注册和注销。
然后,

//      意图过滤器
        intentFilter = new IntentFilter();
//      添加要监听的广播action
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);

/* 这个意图过滤器先李姐成标准流程就行 */。
这里意图过滤器简单点解释就是选择你想要监听的广播,因为系统中广播的使用是非常频繁的,而我们某一个广播接收器其实只处理某一个或者某几个广播,而过滤的内容其实就是这个功能,上述我们addAction加入的就是屏幕亮起和熄灭的广播内容。

注册广播接收器

//      注册广播接收器
        registerReceiver(myreceiver,intentFilter);

注意,到这还没完,还有最后一步,广播的注销。什么时候注销?不需要用到的时候,在这里咱们直接搞成活动销毁的时候,当个栗子直接用。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消注册 注销
        unregisterReceiver(myreceiver);
    }

这玩意应该还比较眼熟吧。销毁活动的方法,我们重写一下,别忘了把它本来的东西放进去。
在这里插入图片描述
到这完工,测试一下效果。
在这里插入图片描述

1.2 静态注册实例

首先还是,接收器先搞一个。

public class bReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"芜湖",Toast.LENGTH_LONG).show();
    }
}

然后就到了和动态注册有区别的地方,静态注册是在AndroidMainfest.xml文件中注册,打开AndroidMainfest.xml文件,我们需要添加如下内容

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

enable这个属性是设置是否启用这个广播接收器,exported这个属性是设置是否允许这个接收器接收到本程序以外的广播(这个涉及到全局广播和局部广播相关内容,咱们等会再讲)。
然后再为其添加:

<intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>

对比动态添加,这个就是action。广播内容也是由系统发出,我们只管接收。
全部内容如下:

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

然后咱们直接卡动,然后发现压根没反应,为什么呢?这里又涉及到Android中另一个很重要的知识点,权限。为了限制程序行为,保护系统和用户安全以及隐私,Android中引入了很复杂的权限系统。讲了这么多,没反应的原因很简单:没权限,那咋办,给他呗。这个就很快啊,啪的一下就站起来,一个uses-permission,一个android:name,一个android.permission.RECEIVE_BOOT_COMPLETED,我们全部都写出去了啊,写出去了

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

这一段加在哪里,加在
在这里插入图片描述
ok权限有了,咱们再来重启一下虚拟机,然后发现还是没反应?基本是因为虚拟机启动速度的问题,多来几次总能看到,实在看不到你也可以换另一种方式来表示接收到了广播,比如用log打印,这里就不展开叙述了。

不能静态注册的广播

这一点作了解,在出现问题之后至少能反应过来是不是我们静态注册了不能够静态注册的广播。
静态注册的特点是什么:一直监听,从负面角度来看,这样做会一直消耗系统资源去监听,如果被监听的广播是系统运行的基本事件,比如电量变化,屏幕亮灭,就会极大程度的消耗系统资源。这是此类广播不能静态注册的原因之一,现在我们只作了解。

//屏幕亮起
android.intent.action.SCREEN_ON
//屏幕熄灭
android.intent.action.SCREEN_OFF
//电量变化
android.intent.action.BATTERY_CHANGED
//屏幕方向,设备信息发生改变
android.intent.action.CONFIGURATION_CHANGED
//时间改变(每分钟发送一次)
android.intent.action.TIME_TICK

2.自定义广播

如何接收广播咱们已经了解了,现在我们要学习广播更自由的地方:发送自定义广播。在开始实操之前,咱们要先再捋一遍,有序广播,标准广播(无序广播)。这个再倒回去翻一翻就了解了。

2.1 发送标准广播(动态注册)

还是老规矩,先写个接收器,这个就不贴代码了,看完前面几个现在应该是有手就行。
然后咱们直接跳到广播的发送,回想一下之前咱们的动态注册,还是照样写,但是这次就要把我们要监听的action中的内容修改了,毕竟这次是我们自己定义的广播内容。

intentFilter2.addAction("com.example.viewmodeltest.MY_BROADCAST_RECEIVER");

除了这个,别的内容到此都没有变化,然后咱们添加一个button来发送广播

        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            //发送广播
            Intent intent = new Intent("com.example.viewmodeltest.MY_BROADCAST_RECEIVER");
            sendBroadcast(intent);
            }
        });

button的点击事件就不多说了。创建一个Intent对象,并且传入广播内容,再调用Context的sendBroadcast方法将其发送出去。此时我们发出的这条广播是全局标准广播,能够被同时所有的应用接收到。

2.2 发送有序广播(动态注册)

广播是一种可以跨进程的通信方式。是不是有点难懂,简单点理解就是广播是整个android系统的通信,再直白点就是我们目前发出的这些全局广播,是可以被系统中所有的程序所接收的。为了证实这点,我们新开一个项目demo,为其写一个能接收我们刚才写的那个标准广播的接收器。

public class cReceiver extends BroadcastReceiver {

    TextView textView;
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"起飞",Toast.LENGTH_LONG).show();
    }
}
public class MainActivity extends AppCompatActivity {
    TextView textView;
    IntentFilter intentFilter;
    cReceiver cReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        cReceiver = new cReceiver();
        intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.viewmodeltest.MY_BROADCAST_RECEIVER");
        registerReceiver(cReceiver, intentFilter);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消注册 注销
        unregisterReceiver(cReceiver);
    }
}

然后我们点击发送广播
在这里插入图片描述

在这里插入图片描述
会跳出这两个Toast,验证到此结束。
接下来是咱们想要的,有序的部分,我们现在希望程序A能够先接收到广播,然后再让程序B接收到。需要做的事情是修改发送方法。使用sendOrderedBroadcast方法发送广播。

sendOrderedBroadcast(intent, null);

在这个截图可以很容易辨认第二个参数大致是与接收权限有关,此处我们只需要设置为null,即为不需要权限。
接下来我们要设置的就是接收广播的先后顺序,即优先级。

//      优先级设置
        intentFilter2.setPriority(1000);

这意思就是把此接收器优先级设置为1000,同理,我们吧程序B的接收器优先级设置为100,然后再在程序A接收器中加入这个:

abortBroadcast();//截断广播

再来运行
在这里插入图片描述

可以看到只会触发A中接收器事件。
咱们再把截断去掉后运行
在这里插入图片描述
B又能接收到此条广播了。

2.3 静态注册注意事项

上面两个例子咱们都是使用动态注册,如果要使用静态注册,需要注意一个很关键的地方。
在发送广播之前,需要调用setPackage方法。

Intent intent = new Intent("com.example.viewmodeltest.MY_BROADCAST_RECEIVER");
//            sendBroadcast(intent);
            intent.setPackage(getPackageName());
        	sendOrderedBroadcast(intent, null);
        });

这是在Android8.0之后的改动,静态注册的接收器无法接收到隐式广播,但我们目前发出的广播都默认是隐式广播,因此需要调用setPackage方法指定此广播是发送给哪个应用程序,让其变成显式广播。(总之还是尽量多用动态注册)

至此咱们讲完了全局广播中的有序广播和标准广播,有没有发现一个说了半天都还没讲到的东西?与全局广播相对应的,本地广播。

3.本地广播

上面咱们讲过的所有内容都是全局广播,全局广播的意思就是,可以被整个系统中所有程序所接收到的广播。有没有想到一个很严重的问题,如果两个app的自定义广播内容刚好设置成一样了,会不会导致系统裂开?这是不是就不安全了。为了解决这个问题,android提供了另一种广播机制:本地广播。以用来解决安全性问题。
本地广播简单理解就是,只在当前程序内生效的广播,也就是写在A程序内的本地广播只会被A程序内部接收,不会像全局广播那样整个系统到处跑,年轻广播,啊,要讲武德,不要搞,窝里斗。

接下来咱们来实操一下本地广播的发送和接收。
第一步,接收器,一样的,诶 不谈。

public class LocalReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"喂 您好 放在门口就好 会给五星好评的谢谢",Toast.LENGTH_SHORT).show();
    }
}

然后到MainActivity中来,第一步还是照样,localReceiver,IntentFilter这俩不能少,但是这里咱们多了一个东西叫LocalBroadcastManager

private LocalBroadcastManager localBroadcastManager;

从字面意思很好理解,本地广播管理器。
接下来实例化,跟动态注册是一样的,但是主要这个本地广播管理器的实例化不是直接new,而是用getInstance方法获取实例。

//本地广播管理器获取实例
        localBroadcastManager = LocalBroadcastManager.getInstance(this);

继续走流程,到最后注册接收器的时候又有不同:

//       本地广播注册方式略有不同
        localBroadcastManager.registerReceiver(localReceiver,intentFilter3);

使用LocalBroadcastManager实例调用registerReceiver方法注册广播接收器。
然后到这,检查一下,还漏了什么,注销广播。

localBroadcastManager.unregisterReceiver(localReceiver);

写在onDestroy方法内。
到此广播注册和注销流程结束,然后咱们再搞个button来发送本地广播,流程也一个样

        button4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent("com.example.viewmodeltest.LOCALBROADCAST_TEST");
                localBroadcastManager.sendBroadcast(intent);
            }
        });
    }

注意上方发送广播也是用LocalBroadcastManager实例调用发送方法。

至此全部搞定,先来试试A程序本身能不能接收到广播
在这里插入图片描述
验证完成。接下来咱们就是要验证程序B是否能接收到这个广播了。方法很简单,在程序B中再写一个接收器,监听我们刚才发出的那条广播。

public class LocalReceiver2 extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"就 你们 有没有摸过 妹妹的手啊",Toast.LENGTH_LONG).show();
    }
}

很明显,并没有接收到此条A程序发出的广播。至此,我们的本地广播搞定。

再来回顾一下本地广播:只发送程序内广播,因此不需要担心数据或者信息被其他应用截取,也不用担心因为广播内容问题影响到其他程序运行。同时本地广播还有一个好处,不用在系统全局发送广播,所以广播发送更加快速高效。因此,对于大部分程序而言,使用动态注册本地广播更加合理。当然,实际使用情况视需求决定。

四、广播实战(强制下线功能)

这个东西详情参见郭神的《第一行代码》,最新版的是使用kotlin,使用java的是第二版。
在此就不贴太多内容,我们只需要理清其中几个关键点。
强制下线功能的实现,我们依托于广播,那么整体实现流程应该是这样:
触发->发送广播->执行响应(弹出对话框并锁死操作界面)->点击确认->销毁并关闭所有活动->回到登录界面

那么接下来我们整理一下每个步骤的具体实现,界面ui这种东西相信应该很熟悉了,就简单概括:
在这个强制下线测试界面,点击测试button会发送广播并执行咱们前面提到的相关内容,点击确定后直接返回主活动界面。
在这里插入图片描述
在这里插入图片描述

4.1 销毁全部活动

如果是通过郭神的《第一行代码》学习的Android,那么在第二章的的实践中会有此部分内容且更为齐全。
先从需求开始分析,目前我们已知的销毁活动方式是finish方法或者点击手机上back,但是有一个关键性问题在于,上述方法是一个一个销毁活动,而我们需要的是点击之后直接干碎全部活动然后重新启动主界面。那么一种解决思路即为,拿一个东西把全部活动装起来,等需要的时候直接调用方法统一全部销毁。这就跟你需要丢垃圾,但是垃圾有很多,那咋办?找个垃圾袋全部装起来一起丢掉。
所以我们需要新建一个类(垃圾袋),用来容纳全部的活动(垃圾),在这个类中,我们创建一个储存activity类型的list,并为其提供添加移除以及一个销毁全部活动的方法,其实这个方法也就是让程序来帮我们挨个挨个的销毁。

/**
 * Create by lundao
 * on 2020
 * use for:
 */
public class ActivityCollect {
    public static List<Activity> activities = new ArrayList<>();
//添加活动 (装垃圾)
    public static void addActivity(Activity activity){
        activities.add(activity);
    }
//移除活动 (移除已经丢掉的垃圾) 
    public static void removeActivity(Activity activity){
        activities.remove(activity);
    }
//销毁所有活动 (丢垃圾)
    public static void finishAll(){
        for (Activity activity:activities){
            if (!activity.isFinishing()){
                activity.finish();
            }
        }
    }

}

4.2 触发,发送广播,执行响应

这个就很简单,点击强制下线触发这个button,发送强制下线广播并被接收,这里咱们用动态的本地广播实现。
活动中:

public class OffOnlineTestActivity extends AppCompatActivity {
    private Button button;
    private OffOnlineReceiver offOnlineReceiver;
    private IntentFilter intentFilter;
    private LocalBroadcastManager localBroadcastManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_off_online_test);

        offOnlineReceiver = new OffOnlineReceiver();
        intentFilter = new IntentFilter();
        localBroadcastManager = LocalBroadcastManager.getInstance(this);

        intentFilter.addAction("com.example.viewmodeltest.OFFONLINETEST");
        localBroadcastManager.registerReceiver(offOnlineReceiver,intentFilter);

        button = findViewById(R.id.btn_offonline);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent("com.example.viewmodeltest.OFFONLINETEST");
                localBroadcastManager.sendBroadcast(intent);
            }
        });
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消注册 注销\
        localBroadcastManager.unregisterReceiver(offOnlineReceiver);
    }

}
public class OffOnlineReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        
    }
}

这就是之前我们讲过的内容,很容易理解。这里并没有给出对话框点击确定后关闭活动的操作。因为这里有个坑,我们留到最后实现起来再说。
实际应用中,不只是现在这么一两个活动,在实际使用中要怎么保证无论在哪个activity,都能直接接收广播跳出对话框并且执行响应操作?

到此,实现思路需要转变一下,我们需要能够只注册一次就可以在全部活动中生效的广播。怎么样才能在所有activity中动手?很简单可以思考到,我们需要从activity的父类动手,这里采用的方法取于《第一行代码》,我们通过写一个基类BaseActivity,继承AppCompatActivity,然后我们剩下的activity继承BaseActivity,将activity的添加和移除写进BaseActivity,这样它的所有子类自然就继承了这个广播的发送和接收器注册。因此我们需要在BaseActivity中注册接收器而不是MainActivity。

到这里我们的思路已经走在了正确的道路上,如果认真思考,此时会发现一个很关键的问题,既然有了BaseActivity,那么是不是可以往我们之前写的活动管理类中添加活动了?

/**
 * Create by lundao
 * on 2020
 * use for:
 */
public class BaseActivity extends AppCompatActivity {
//重写onCreate 活动加载时添加进管理类
    @Override
    protected  void onCreate(Bundle saveInstanceState){
        super.onCreate(saveInstanceState);
        ActivityCollect.addActivity(this);
    }
//    重写onDestroy 活动销毁时从管理类中移除
    @Override
    protected void onDestroy(){
        ActivityCollect.removeActivity(this);
        super.onDestroy();
    }

}

然后到这里,咱们已经搞定了活动管理类(实现销毁全部活动),搞定活动基类(本地动态注册一个广播接收器),接下来咱们应该实现的是接收器收到广播后的响应事件。

4.2.1 广播调用对话框弹出(小坑点)

从这里开始,填坑,
在这里插入图片描述

根据实现思路,我们首先需要在广播接收器中开启一个AlertDialog,正常情况下咱们的代码应该是这个样子:

                AlertDialog.Builder alertDialog = new AlertDialog.Builder(context);
//        此处context类型为Application Context
//        因此无法用来调用alertDialog
        alertDialog.setTitle("强制下线");
        alertDialog.setMessage("触发强制下线,点击确定回到起始界面");
        alertDialog.setCancelable(false);//设置取消按钮不可用
        alertDialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {

            }
        });

测试发现,我们此条对话框无法弹出,并报错:Unable to add window – token null is not valid; is your activity running?
分析此报错信息简单概括,我们这个context不是一个活动,因此无法通过其启动alertdialog。那接下来要怎么办?
查找很多数据后发现


对于不同注册方式的广播接收器回调OnReceive(Context context,Intent intent)中的context返回值是不一样的:
    对于静态注册(全局+应用内广播),回调onReceive(context, intent)中的context返回值是:ReceiverRestrictedContext;
    对于全局广播的动态注册,回调onReceive(context, intent)中的context返回值是:Activity Context;
    对于应用内广播的动态注册(LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Application Context。
    对于应用内广播的动态注册(非LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Activity Context

发现问题没有,咱们通过此种方式注册的广播传入的context并不是activity类型,所以我们要重新换一个东西来启动对话框,理论上最应该的对话框是谁?在这里插入图片描述
是这个OffOnlineTestActivity,那怎么获取它?这里给出我找到的办法(tmd折磨了兄弟半天,真有你的) 来源于
1.广播onReceive()方法的context类型探究
2.Android Receiver中开启一个弹窗,并且弹窗在进入下一个Activity时关闭
感谢上述两文作者给出的验证和思路。
回顾我们写的活动管理类,是不是储存了所有的活动集合,而且根据活动创建顺序,最后一个添加进去的activity在栈顶,也就是咱们现在需要搞到的OffOnlineTestActivity。那么实现起来就很简单了,在ActivityCollect中加入一个新的方法,getStackTopActivity用于得到当前栈顶活动。

//返回当前栈顶活动
    public static Activity getStackTopActivity(){
        return activities.get(activities.size() - 1);
    }

完成后再来测试对话框
在这里插入图片描述

4.2.2 销毁全部活动且重新开启主界面(小坑点)

刚才我们搞定了第一个坑,我们的对话框已经可以完成弹出和点击确定关闭。但别忘了,我们的根本目的是想要点击确定后完成对所有活动的销毁并回到主界面。
正常情况下咱们在AlertDialog的确认点击事件中可能会写成这样:

                ActivityCollect.finishAll();//销毁全部活动
                Intent intent1 = new Intent(context, MainActivity.class);
                context.startActivity(intent1);

测试发现,点击确认后,咱们整个程序直接关闭了,同时报错
Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
从活动上下文外部调用startActivity(),需要这个balabala。这真的是你想要的吗?

关于这个异常详情可以参考此文:
为什么有时候启动Activity需要加FLAG_ACTIVITY_NEW_TASK

简单概括一下就是,Context是一个抽象类,它有很多具体的实现,我们常用的Service,Application,Activity都是它的具体实现。而我们此处用到的startActivity方法也是一个抽象方法,啥叫抽象方法啊(后仰)
在这里插入图片描述
根据引用文章的解释,我们现在通过context调用的这个startActivity方法中包含了一次判断,因此我们需要添加FLAG_ACTIVITY_NEW_TASK,
而在activity中的的startActivity方法实现中,并没有这一句,因此我们在activity中调用并不需要加。
在这里插入图片描述

我并没有在源码找到我们此处context调用的startActivity方法具体实现在哪,或许这就是懒狗吧。
在这里插入图片描述
问题理论已经解决了,赶紧掏出代码填上,在startActivity之前加上这一句

intent1.addFlags(FLAG_ACTIVITY_NEW_TASK);

然后运行
在这里插入图片描述
在这里插入图片描述
功能实现。结束
在这里插入图片描述

五、总结

总结个屁,整整一万七千字,还看不懂我真的麻了。
在这里插入图片描述


版权声明:本文为qq_43667625原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。