【Android】大喇叭——广播

广播机制介绍

Android中的广播分为两种类型:标准广播和有序广播

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

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

接收广播系统

广播接收器可以自由地对自己感兴趣的广播进行注册,当有相应的广播发出时,广播接收器就能接收到该广播,并在内部处理相应的逻辑。注册广播的方式有两种:在代码中注册、在AndroidManifes.xml中注册,前者被称为动态注册,后者被称为静态注册。

动态注册监听网络变化

创建一个广播接收器只需要创建一个类,让它继承BroadcastReceiver,并重写父类的onReceive()方法即可,当广播来临的时候,这个方法就会得到执行。

示例如下所示:

java 复制代码
public class MainActivity extends AppCompatActivity {
    private IntentFilter intentFilter;
    private NetworkChangeReceiver networkChangeReceiver;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        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) {
            ConnectivityManager connectivityManager = (ConnectivityManager)
                    getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
            if (networkInfo != null && networkInfo.isAvailable()) {
                Toast.makeText(context, "network is available", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

我们在主活动中定义了一个内部类,使其继承BroadcastReceiver,每当网络发生变化,这段代码都会得到执行。

先看主活动onCreate()里的代码:

  • 建立一个过滤器:IntentFilter 是用来指定 BroadcastReceiver 想要接收的 Intent 类型的过滤器。通过添加特定的Action来确定监听哪些广播,此处我们是要监听网络变化,当网络发生变化时,发出的就是一条值为android.net.conn.CONNECTIVITY_CHANGE的action。
  • 设立一个广播接收器
  • 进行注册:registerReceiver(networkChangeReceiver, intentFilter);``registerReceiver 方法用来注册 BroadcastReceiver,使其能够接收特定的 Intent。第一个参数是 BroadcastReceiver 的实例,第二个参数是之前创建并配置的 IntentFilter
  • 取消注册:动态注册的广播接收器一定要取消注册,此处我们是在onDestroy()方法里面调用unregisterReceiver()方法实现

上面的代码在我们连接上网络的时候或者断掉网络的时候会发出相应的通知。

静态注册实现开机启动

动态注册的广播接收器可以自由地控制注册与注销,在灵活性方面有很大的优势,但存在一个缺点,必须在程序启动之后才能接收到广播。有没有办法能让程序未启动的情况下就可以接收到广播呢?这就需要静态注册的方式了。

安卓提供的快捷方式创建一个广播接收器,点击包名→new→Other→Broadcast Receiver,此时弹出:

Exported属性:表示是否允许这个广播接收本程序以外的广播

Enabled属性:表示是否启用这个广播接收器

此时就会为我们自动创建了,我们只需要修改其中的代码:

java 复制代码
public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show();
    }
}

静态的广播接收器一定要在AndroidManifest.xml文件中注册才可以使用,由于我们时使用的快捷方式,因此注册这一步已经被自动完成了。但是此时还是不能接收到开机广播的,我们需要对AndroidManifest.xml文件进行修改:

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" >
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
......
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
......
</manifest>

注意:不要在onReceive()方法中添加过多的逻辑或者进行任何耗时的操作,在广播接收器当中是不允许接收线程的,当onReceive()方法运行了较长的时间而没有结束时,程序就会报错。广播接收器更多的扮演一种打开程序其他组件的角色。

发送自定义广播

发送标准广播

我们先定义一个广播接收器来接收此广播

java 复制代码
public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "receive in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
    }
}

然后与之前一样对AndroidManifest.xml文件进行修改:

xml 复制代码
<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
    </intent-filter>
</receiver>

修改主活动的XML文件的代码,放一个按钮用来发送广播,我们需要在主活动当中对按钮注册点击事件:

java 复制代码
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
        intent.setPackage(getPackageName());
        sendBroadcast(intent);
    }
});
  • setPackage(getPackageName()): 调用 setPackage 方法并传入当前应用的包名,这限制了广播只在当前应用内传播
  • sendBroadcast(intent): 发送一个广播。这个调用会通知所有注册了相同 action 的 BroadcastReceiver

注意:我们没有这句指令就收不到广播,是因为setPackage()方法是指定将这个广播发送给那个程序,让隐式的广播转化为显式的广播。在Android8.0以后,静态注册的BroadcastReceiver是无法接受广播的。

此时运行程序,当按下按钮的时候发出广播:

发送有序广播

在上面提到静态注册的广播是无法接收的,要使用为显式的广播,因此无法做到两个应用互相传递信息。建立一个新的广播接收器:

java 复制代码
public class AnotherBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "receive in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
    }
}

与上面同样的方法在AndroidManifes.xml里面注册信息,此时按下按钮它也可以接收到广播,并作出回应,但此时仍为标准广播。要发送有序广播只需要改一句:

java 复制代码
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
        intent.setPackage(getPackageName());
        sendOrderedBroadcast(intent, null);
    }
});

此时就只改了发送广播的代码,这里的第二个参数null是一个与权限相关的字符串,这里设置为null即可,此时运行程序会接收到两条不同广播接收器做出的回应。但我们知道有序广播是有一定的先后顺序的,在何处设定广播接收器的先后顺序呢?就是在注册的时候设定,修改AndroidManifes.xml里的代码:

xml 复制代码
<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter android:priority="100">
        <action android:name="com.example.broadcasttest.MY_BROADCAST" />
    </intent-filter>
</receiver>

我们通过android:priority属性给广播接收器设置了优先级,优先级较高就可以先接收到广播。优先级的取值范围为-1000到1000,当我们没有设置优先级时,优先级默认为0。

此时MyBroadcastReceiver的优先级为100,此时AnotherBroadcastReceiver的优先级默认为0,因此前一个广播接收器会先收到广播。运行程序,我们先接收到优先级高的做出的回应即MyBroadcastReceiver做出的回应。使用有序广播的优点就在于可以随时截断广播的传播,MyBroadcastReceiver的优先级更高,因此可以选择是否继续传递:

java 复制代码
public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "receive in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
        abortBroadcast();
    }
}

我们从中调用了 abortBroadcast()方法表示将这个广播进行截断,后面的广播接收器就无法再接收这条广播。重新运行程序,此时广播就会在MyBroadcastReceiver部分进行截断,之后的广播接收器AnotherBroadcastReceiver就接收不到广播了,从而不会做出任何的反应。

使用本地广播

如果发送的广播可以被所有的程序都接收到,这样你所发送的广播就会被其他的所有程序所接收,其他程序也会发来很多的不需要的广播,为了能够解决广播的安全性问题,Android引入了一套本地广播机制,使用这个机制发出的广播只能够在应用程序内部进行传递,也只能接收本程序发出的广播。

本地广播就是使用了一个LocalBroadcastManager来对广播进行管理。提供了发送广播和注册广播接收器的方法。

java 复制代码
public class MainActivity extends AppCompatActivity {
    private LocalReceivew localReceiver;
    private LocalBroadcastManager localBroadcastManager;
    private IntentFilter intentFilter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        
        localBroadcastManager = LocalBroadcastManager.getInstance(this);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            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 LocalReceivew();
        localBroadcastManager.registerReceiver(localReceiver, intentFilter);
    }

    @Override
    protected void onDestroy () {
        super.onDestroy();
        localBroadcastManager.unregisterReceiver(localReceiver);
    }
    
    class LocalReceivew extends BroadcastReceiver {
        @Override
        public void onReceive (Context context, Intent intent) {
            Toast.makeText(context, "receive local broadcast", Toast.LENGTH_SHORT).show();
        }
    }
}

是不是觉得代码非常的熟悉呢?没错,基本上与我们动态的注册广播接收器的步骤是一样的,只是在方法调用的时候使用了LocalBroadcastManager进行管理。

本地广播的优势:

  1. 可以明确知道正在发送的广播不会离开我们的程序,因此无需担心机密数据泄露
  2. 其他的程序无法将广播发送到我们程序的内部,因此不需要担心有安全泄露的隐患
  3. 发送本地广播比发送系统全局广播将会更加高效

实践---实现强制下线功能

强制下线功能是我们所经常用到的,当我们的微信、QQ等有人在别的手机设备上面登录的时候,我们的手机就会弹出一个弹窗:

这时,你就只能按下确定按钮,它会将你之前的活动全部关闭,重新回到登录界面。接下来就自己去实现吧。

我们要按下按钮就关闭所有的活动,回到登录界面就需要一个活动管理器,创建一个ActivityCollector类用于管理所有的活动:

java 复制代码
import android.app.Activity;

import java.util.ArrayList;
import java.util.List;

public class ActivityCollector {
    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();
            }
        }
        activities.clear();
    }
}

创建BaseActivity类作为所有活动的父类:

java 复制代码
public class BaseActivity extends AppCompatActivity {

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

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

}

要想使用户重新登录我们就需要写登录界面的布局,新建一个登录活动:

xml 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".LoginActivity">

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account:"/>
        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_weight="1"
            android:id="@+id/account"/>
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password:"/>
        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_weight="1"
            android:id="@+id/textPassword"/>
    </LinearLayout>

    <Button
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:id="@+id/login"
        android:text="Login"/>

</LinearLayout>

登陆界面设置了输入用户的账户与密码以及登录的按钮,此时当用户进行输入结束之后按下登录按钮,会对账户与密码进行匹配,匹配成功进入下一个活动的界面,此时对登录活动的代码进行修改:

java 复制代码
public class LoginActivity extends BaseActivity {

    private EditText accountEdit;
    private EditText passwordEdit;
    private Button login;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_login);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        accountEdit = (EditText) findViewById(R.id.account);
        passwordEdit = (EditText) findViewById(R.id.textPassword);
        login =(Button) findViewById(R.id.login);
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String account = accountEdit.getText().toString();
                String password = passwordEdit.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,
                            "account or password is invalid", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

简单的登录界面就完成了,当你输入正确的登录信息的时候就会跳转到主活动了,此时我们只需要做到强制下线,在主活动当中添加一个按钮用来触发强制下线功能:

xml 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/force_offline"
        android:text="Send force offline broadcast"
        android:textAllCaps="false"/>

</LinearLayout>

修改按钮的点击事件,使点击按钮会发出一个广播,当接收到这个广播的时候就关闭所有的活动,重新回到登录界面:

java 复制代码
public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        Button button = (Button) findViewById(R.id.force_offline);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent("com.example.broadcastbestpractice.FORCE_OFFLINE");
                sendBroadcast(intent);
            }
        });
    }
}

我们需要无论在哪一个活动,在接收到强制下线的时候弹出一个对话框提示你必须重新登录,所有的活动都继承与BaseActivity,因此我们将广播接收器注册在这个活动当中。

java 复制代码
public class BaseActivity extends AppCompatActivity {

    private ForceOfflineReceiver receiver;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityCollector.addActivity(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE");
        receiver = new ForceOfflineReceiver();
        registerReceiver(receiver, intentFilter);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (receiver != null) {
            unregisterReceiver(receiver);
            receiver = null;
        }
    }

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

    class ForceOfflineReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("Warning");
            builder.setMessage("You are forced to be offline.please try to login again.");
            builder.setCancelable(false);
            builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    ActivityCollector.finishAll();
                    Intent intent = new Intent(context, LoginActivity.class);
                    context.startActivity(intent);
                }
            });
            builder.show();
        }
    }
}

当广播接收器接收到广播,就会弹出一个对话框,当年按下OK按钮就会关闭所有的活动,跳转到登录界面,注意要使用builder.setCancelable(false);使其不可通过Back按钮关闭对话框。

为什么要将广播接收器的注册与取消放在onResume()onPause()里面呢?因为我们要保证活动只有位于栈顶才会接收到广播,没有在栈顶的就没有必要接收到这条广播信息。我们要一开始就是登录界面,因此将登录界面设置为主活动。

为什么receiver = null:这样是为了确保在调用注销广播接收器之后以避免在已经注销的广播接收器上继续操作而可能导致异常或其他问题。

运行程序:

位于登录界面,输入信息进行登录:

按下活动的按钮弹出对话框,此时按Back按钮是没有任何反应的,只能按下OK按钮,此时就会回到登录界面。

文章到这里就结束了!

相关推荐
古月居GYH几秒前
在C++上实现反射用法
java·开发语言·c++
Winston Wood6 分钟前
Perfetto学习大全
android·性能优化·perfetto
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol1 小时前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-13141 小时前
jdk各个版本介绍
java
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺2 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端