Bootstrap

【Android】广播机制

【Android】广播机制

前言

广播机制是Android中一种非常重要的通信机制,用于在应用程序之间或应用程序的不同组件之间传递信息。广播可以是系统广播,也可以是自定义广播。广播机制主要包括标准广播和有序广播两种类型。

简介

在Android中,广播(Broadcast)是一种消息,任何应用程序都可以发送广播消息,任何应用程序也都可以接收广播消息。广播通常用于通知应用程序某些事件的发生,比如系统启动、电量低、网络状态改变等。

广播的主要组件包括:

  • Broadcast Receiver(广播接收器):用于接收广播消息并响应这些消息的组件。
  • Intent(意图):用于传递广播消息的数据结构。
  1. 标准广播:

    标准广播(Normal Broadcast)是完全异步的,所有接收器几乎同时接收广播,并且接收顺序是不确定的。标准广播的特点是速度快,因为它们不需要等待其他接收器处理完广播才能继续传递。

    image-20240723203035435

  2. 有序广播:

    有序广播(Ordered Broadcast)是同步的,一个接收器接收到广播并处理完后,广播才会继续传递给下一个接收器。接收器可以修改广播的数据或截断广播,使其不再传递给其他接收器。有序广播允许通过设置优先级来控制接收器的接收顺序,优先级高的接收器会先接收广播。

    image-20240723203247638

接收系统广播

监听网络变化

先新建BroadcastTest项目,修改MainActivity

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 { // 内部类,继承自BroadcastReceiver

        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show(); // 网络变化时显示提示信息
        }
    }
}

动态注册的广播一定都要取消注册

取消注册原因:

防止内存泄漏

  • 如果广播接收器在不需要时未被注销,它会持有对 Context 的引用,可能会导致内存泄漏。特别是在 ActivityService 中,如果它们被销毁后广播接收器仍然存在,会导致这些组件无法被垃圾回收器回收,进而占用系统资源。

避免不必要的资源消耗

  • 如果不注销广播接收器,它仍然会继续接收广播,即使相关的 ActivityService 已经不再需要这些广播。这会导致不必要的系统资源消耗,因为每次接收到广播时都会触发 onReceive 方法的执行。

防止潜在的崩溃

  • 在一些情况下,如果广播接收器在 ActivityService 销毁后继续接收广播,可能会导致应用程序崩溃。例如,如果 onReceive 方法中试图访问已销毁的 Activity 的 UI 元素,会引发 NullPointerException 等异常。

良好的编程实践

  • 注销广播接收器是一种良好的编程习惯,有助于保持代码的整洁和可靠性。它确保每个资源都被合理管理和释放,避免因资源管理不当而导致的各种问题。

上面的代码只能提示网络是否变化,可以对上面的代码进行优化

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 { // 内部类,继承自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(); // 网络不可用时显示提示信息
            }
        }
    }
}

就可以显式网络是否连接了

静态注册实现开机启动

先在com/example/boardcasttest包下点击New→Other→Broadcast Receiver,修改名字为BootCompleteReceiver并且勾选Exported(是否允许这个广播接收器接收本程序以外的广播),Enabled(表示是否启用这个广播接收器)创建完成

修改BootCompleteReceiver中的代码:

class NetworkChangeReceiver extends BroadcastReceiver {
    @Override
	public void onReceive(Context context, Intent intent) {
	Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show();
	}
}

此外还需要在AndroidManifest文件中注册,但是由于我们使用的是快捷方式创建,所以这一步已经被自动完成了:

<?xml version="1.0" encoding="utf-8"?>
<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" />
    
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BoardcastTest"
        tools:targetApi="31">
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
            
        </receiver>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

新建了一个标签<receiver>

但是目前还是接收不到开机广播,需要对AndroidManifest进行修改:

<?xml version="1.0" encoding="utf-8"?>
<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"/>//
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BoardcastTest"
        tools:targetApi="31">
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>//
            </intent-filter>
        </receiver>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

重新运行后就可以接收开机广播了

自定义广播

发送标准广播

首先需要定义一个广播接收器接收广播,新建MyBroadcastReceiver

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

修改AndroidManifest中的代码:

<?xml version="1.0" encoding="utf-8"?>
<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"/>
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BoardcastTest"
        tools:targetApi="31">
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>//
            </intent-filter>
        </receiver>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

我们让MyBroadcastReceiver接收值为`的广播

修改activity_main中的代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    
    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send BoardCast"/>

</LinearLayout>

定义了一个按钮用于作为发送广播的触发点

修改MainActivity中的代码:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        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);
            }
        });
    }
}

首先创建intent对象,将要发送的广播的值传入,然后调用sendBroadcast()进行发送,我们之前设置的接收器就可以接收到广播了

还要注意的是setPackage的作用是指定这条广播发送给哪个程序,使得隐式广播转化为显式广播。因为Android8.0以后,静态注册的BroadcastReceiver是无法接受广播的

发送有序广播

修改MainActivity中的代码:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        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");
                sendOrderedBroadcast(intent, null);
            }
        });
    }
}

我们将sendBroadcast()方法改成了sendOrderedBroadcast()

新建一个类AnotherBroadcastReceiver继承BroadcastReceiver:

public class AnotherBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "receciver in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
        abortBroadcast();//表示截断广播
    }
}

修改AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<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"/>
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BoardcastTest"
        tools:targetApi="31">
        <receiver
            android:name=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
            </intent-filter>
        </receiver>

        <receiver
            android:name=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="100">
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
            </intent-filter>
        </receiver>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

可以通过修改

<intent-filter android:priority="100">

来确定接收广播的优先级,数字大的先接收

使用本地广播

public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter; // 意图过滤器,用于监听特定广播事件
    private LocalReceiver localReceiver; // 本地广播接收器实例
    private LocalBroadcastManager localBroadcastManager; // 本地广播管理器实例

    @Override
    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 View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 创建一个意图,并通过本地广播发送
                Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
                localBroadcastManager.sendBroadcast(intent);
            }
        });

        // 初始化意图过滤器,并添加广播事件
        intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.broadcasttest.MY_BROADCAST");

        // 初始化本地广播接收器
        localReceiver = new LocalReceiver();
        
        // 注册本地广播接收器
        localBroadcastManager.registerReceiver(localReceiver, intentFilter);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 注销本地广播接收器
        localBroadcastManager.unregisterReceiver(localReceiver);
    }

    // 定义本地广播接收器类,继承自BroadcastReceiver
    class LocalReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 接收到本地广播时显示提示信息
            Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
        }
    }
}

广播的最佳实践——强制下线功能

先创建一个ActivityCollector类管理所有活动

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类作为所有活动的父类

public class BaseActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
        ActivityCollector.addActivity(this);
    }

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

创建LoginActivity,并自动生成activity_login布局文件,修改如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <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:text="Account:"
            android:textSize="18sp"/>

        <EditText
            android:id="@+id/account"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"/>
    </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:text="Password:"
            android:textSize="18sp"/>

        <EditText
            android:id="@+id/password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"/>
    </LinearLayout>

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

</LinearLayout>

这个布局就不多做解释了

下来修改LoginActivity中的代码:

public class LogActivity extends BaseActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_log);
        accountEdit = (EditText) findViewById(R.id.account);
        passwordEdit = (EditText) findViewById(R.id.password);
        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("123") && password.equals("123")) {
                    Intent intent = new Intent(LogActivity.this, MainActivity.class);
                    startActivity(intent);
                    finish();
                } else {
                    Toast.makeText(LogActivity.this, "account or password is invalid", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

模拟了一个点单的登录功能,账号为123且密码为123则登陆成功,跳转到MainActivity

下来修改activity_main中的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

</LinearLayout>

只用实现一个按钮用来触发强制下线功能

修改MainActivity中的代码:

public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button forceOffline = (Button) findViewById(R.id.force_offline);
        forceOffline.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent("com.example.broadcastbestpractice.FORCE_OFFLINE");
                intent.setPackage(getPackageName());
                sendBroadcast(intent);
            }
        });
    }
}

其中come.example.broadcastbestpractice.FORCE_OFFLINE是用来通知程序强制下线的

下来需要创建广播接收器来接收广播,但是如果创建一个静态注册的广播接收器是没有办法在onReceive()里弹出对话框那样的UI控件,是不现实的

我们只需要在BaseActivity中动态注册一个广播接收器就可以了,因为所有活动继承自BaseActivity

修改BaseActivity中的代码:

public class BaseActivity extends AppCompatActivity {

    private ForceOfflineReceiver receiver; // 广播接收器实例

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
        ActivityCollector.addActivity(this); // 将活动添加到活动管理器中
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ActivityCollector.removeActivity(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; // 将接收器置为空
        }
    }

    class ForceOfflineReceiver extends BroadcastReceiver { // 定义内部类,继承自BroadcastReceiver
        @Override
        public void onReceive(final Context context, Intent intent) {
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("Warning"); // 设置对话框标题
            builder.setMessage("You are forced to be offline!"); // 设置对话框消息
            builder.setCancelable(false); // 设置对话框不可取消
            builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { // 设置对话框确认按钮
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    ActivityCollector.finishAll(); // 关闭所有活动
                    Intent i = new Intent(context, LogActivity.class); // 创建意图,启动LogActivity
                    context.startActivity(i); // 启动LogActivity
                }
            });
            builder.show(); // 显示对话框
        }
    }
}

下来对AndroidManifest文件进行修改:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BroadcastBestPractice"
        tools:targetApi="31">
        <activity android:name=".MainActivity" >
        </activity>
        <activity android:name=".LogActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

完成了所有代码,当我们登录后点击按钮,就可以实现强制退出了。

下面是实现的效果:

image-20240724212643794

image-20240724212732409

image-20240724212748833


已经到底啦!!

;