【WorkManager】Android 后台任务调度的核心组件指南
【WorkManager】Android 后台任务调度的核心组件指南
- 一、定时上报需求描述
- 二、定时上报需求分析
- 三、定时上报实现方案
- 3.1 Application 注册网络变化监听
- 3.2 网络请求和后台任务
- 3.3 保存网络环境状态
- 3.4 实现网络变化接收器
一、定时上报需求描述
实现定时上报设备信息的Android系统应用,功能需求:开机上报、定时每24时上报、上报失败重试机制、网络环境变更上报、断网重连不上报,并且所有上报都需要网络连接时进行。
关于本应用的特殊性:不需要考虑如何被启动问题、不需要考虑被杀问题、需要考虑任务执行的准时性和可靠性、需要考虑开机设备未解锁时执行上报任务。
二、定时上报需求分析
基础设置:绑定 android:sharedUserId="android.uid.phone"
进程开机会自动启动应用,并设置 android:persistent="true"
属性在正常情况下应用进程永远不会被系统杀死,即使出现被杀也会自动重启,并且在 Direct Boot 模式下执行任务需要设置 android:directBootAware="true"
。
WorkManager
是 Android 平台上推荐用于处理可延迟工作的任务调度程序,作为 Android Jetpack 架构组件库中用于管理后台任务的核心组件,它专为可延迟、需可靠执行且不要求立即完成的任务设计,能确保任务即使应用退出或设备重启后仍能执行。这里我们考虑使用 WorkManager
执行定时上报任务,使用简单且可靠准时,由于原生 WorkManager 是不支持 Direct Boot 模式
,解决方案参考:【WorkManager】无法在 Direct Boot 模式下初始化。
三、定时上报实现方案
实现上诉的定时上报任务,主要按照以下步骤进行:
1. Application onCreate 中注册网络变化监听。
2. 网络监听中判断是否首次开机,必须执行上报;每次接收网络变化时保存当前网络环境为一个自定义参数,用于判断网络环境是否变化。
3. 执行上报任务,失败时重试三次;成功时计划24时的延时上报任务。
3.1 Application 注册网络变化监听
自定义的 Application 类,这个类是应用全局的上下文,在整个应用的生命周期中只存在一个实例,非常适合用来注册监听和管理全局的 BroadcastReceiver,用于网络变化的广播 ConnectivityManager.CONNECTIVITY_ACTION
public class DeviceReportApplication extends Application {private static final String TAG = "DeviceReportApplication";private NetworkChangeReceiver networkChangeReceiver;@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base.createDeviceProtectedStorageContext());}@Overridepublic void onCreate() {super.onCreate();LogUtil.i(TAG, "onCreate register NetworkChangeReceiver");registerNetworkChangeReceiver();}@Overridepublic void onTerminate() {super.onTerminate();LogUtil.i(TAG, "onTerminate unregister NetworkChangeReceiver");unregisterNetworkChangeReceiver();}private void registerNetworkChangeReceiver() {networkChangeReceiver = new NetworkChangeReceiver();IntentFilter filter = new IntentFilter();filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);registerReceiver(networkChangeReceiver, filter);}private void unregisterNetworkChangeReceiver() {if (networkChangeReceiver != null) {unregisterReceiver(networkChangeReceiver);networkChangeReceiver = null;}}@Overridepublic boolean isDeviceProtectedStorage() {return false;}
}
3.2 网络请求和后台任务
创建后台任务 (UploadWorker.java),这个 Worker 类负责执行实际的网络请求,以及请求失败重试机制。Worker 自带 getRunAttemptCount 方法获取重试次数,不需要额外计次。
public class UploadWorker extends Worker {private static final String TAG = "UploadWorker";private static final String SERVER_URL = "http://xxx.xxx.com/";private static final int MAX_RETRY_ATTEMPTS = 3;public UploadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {super(context, workerParams);}@NonNull@Overridepublic Result doWork() {Log.d(TAG, ">>> 开始执行上报任务...");int runAttemptCount = getRunAttemptCount();Log.d(TAG, ">>> 开始执行上报任务... (第 " + (runAttemptCount + 1) + " 次尝试)");// 自定义的获取设备信息的方法DeviceInfo deviceInfo = DeviceInfoHelper.getFullDeviceInfo(getApplicationContext());String jsonStr = deviceInfo.toJsonString();Log.d(TAG, "设备信息: " + jsonStr);try {String encodedJson = URLEncoder.encode(jsonStr, "UTF-8");URL url = new URL(SERVER_URL + "?jsonstr=" + encodedJson);HttpURLConnection connection = (HttpURLConnection) url.openConnection();// ... set timeouts ...connection.setRequestMethod("GET");connection.setConnectTimeout(15000); // 15秒超时connection.setReadTimeout(15000);int responseCode = connection.getResponseCode();Log.d(TAG, "服务器响应码: " + responseCode);if (responseCode == HttpURLConnection.HTTP_OK) {Log.d(TAG, "上报成功。现在开始调度下一次(24小时后)的上报任务...");// *** 核心修改在这里 ***// 无论何时成功,都去调度下一次延迟24小时的任务ReportScheduler.scheduleNextDelayedReport(getApplicationContext());return Result.success();} else {Log.w(TAG, "上报失败,准备重试。");return handleFailure(runAttemptCount);}} catch (IOException e) {Log.e(TAG, "上报异常: " + e.getMessage());return handleFailure(runAttemptCount);}}private Result handleFailure(int runAttemptCount) {Log.w(TAG, "上报失败。当前尝试次数: " + (runAttemptCount + 1));// 检查是否还有重试机会// 注意:首次运行 runAttemptCount 是 0,所以重试3次意味着 runAttemptCount 会是 0, 1, 2if (runAttemptCount < MAX_RETRY_ATTEMPTS - 1) {Log.d(TAG, "还有重试机会,将按照退避策略在30分钟后重试。");return Result.retry(); // 告诉 WorkManager 使用 setBackoffCriteria 的策略重试} else {// 所有重试次数已用尽Log.e(TAG, "已达到最大重试次数(" + MAX_RETRY_ATTEMPTS + "),上报彻底失败。");Log.d(TAG, "将在最后一次重试的24小时后,再次进行终端信息上报尝试。");// *** 核心逻辑:手动调度一个24小时后的任务 ***ReportScheduler.scheduleNextDelayedReport(getApplicationContext());// 明确地将当前任务标记为失败,结束这个任务链return Result.failure();}}
}
创建任务调度器 (ReportScheduler.java),这个类负责安排和取消 WorkManager 任务。OneTimeWorkRequest
是用于调度一次性执行的任务,setConstraints
为任务设置执行约束,setBackoffCriteria
设置重试策略,enqueueUniqueWork
方法专用于调度一次性的唯一任务,能确保同一时刻只有一个具有特定名称的任务实例在执行,有效避免重复调度的问题。
周期性任务可以使用 PeriodicWorkRequest,这里没有使用是便于控制唯一任务
public class ReportScheduler {// 我们现在只需要一个唯一的任务名称,因为所有任务都是同一个类型private static final String UPLOAD_WORK_NAME = "device_info_upload_work";/*** 调度一个立即执行的上报任务。* 用于开机或网络变化。*/public static void scheduleInitialReport(Context context) {Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();OneTimeWorkRequest uploadWorkRequest =new OneTimeWorkRequest.Builder(UploadWorker.class).setConstraints(constraints).setBackoffCriteria(BackoffPolicy.LINEAR,30, TimeUnit.MINUTES).build();// 使用 REPLACE 策略:如果有一个延迟的任务正在等待,// 而此时网络发生了变化,那么应该取消那个等待的任务,立即执行一次新的。WorkManager.getInstance(context).enqueueUniqueWork(UPLOAD_WORK_NAME,ExistingWorkPolicy.REPLACE,uploadWorkRequest);}/*** 调度一个在24小时后执行的上报任务。* 由 UploadWorker 成功后自己调用,形成链条。*/public static void scheduleNextDelayedReport(Context context) {Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();OneTimeWorkRequest uploadWorkRequest =new OneTimeWorkRequest.Builder(UploadWorker.class).setConstraints(constraints)// *** 延迟24小时 ***.setInitialDelay(24, TimeUnit.HOURS).setBackoffCriteria(BackoffPolicy.LINEAR,30, TimeUnit.MINUTES).build();// 同样使用 REPLACE 策略,确保任务链的唯一性WorkManager.getInstance(context).enqueueUniqueWork(UPLOAD_WORK_NAME,ExistingWorkPolicy.REPLACE,uploadWorkRequest);}
}
3.3 保存网络环境状态
关于网络环境变更
这一具体场景,例如:
- 设备一直连接着 Wi-Fi A。
- 用户关闭 Wi-Fi,设备自动切换到 4G/5G 移动网络。
- 用户走到另一个地方,设备从 4G 切换到 Wi-Fi B。
每次监听到连接状态下网络时候,获取当前活动网络的唯一标识信息(例如 Wi-Fi 的 SSID+BSSID
的信息组合,移动网络的 MCC+MNC+网络类型
的信息组合),并保存到 SharedPreferences。后面就可以从 SharedPreferences 中读取上一次保存的网络标识比较:如果当前标识和保存的标识不同,或者之前没有保存过标识,则说明网络环境发生了变化。
public class NetworkStateHelper {private static final String PREFS_NAME = "network_state_prefs";private static final String KEY_LAST_NETWORK_ID = "last_network_id";// 获取当前网络的唯一标识符public static String getCurrentNetworkId(Context context) {ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);if (cm == null) return "NO_CONNECTIVITY_MANAGER";Network activeNetwork = cm.getActiveNetwork();if (activeNetwork == null) return "NO_ACTIVE_NETWORK";NetworkCapabilities caps = cm.getNetworkCapabilities(activeNetwork);if (caps == null) return "NO_NETWORK_CAPABILITIES";if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);if (wifiManager != null) {WifiInfo connectionInfo = wifiManager.getConnectionInfo();// Wi-Fi的BSSID (路由器MAC地址) 是一个很好的唯一标识return "WIFI_" + (connectionInfo != null ? connectionInfo.getBSSID() : "UNKNOWN_BSSID");}} else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);if (tm != null) {String networkOperator = tm.getNetworkOperator();if (!TextUtils.isEmpty(networkOperator)) {// 移动网络的 "MCC+MNC (移动国家码+移动网络码)+网络类型" 可以作为标识//CELLULAR_46000_13return "CELLULAR_" + networkOperator + "_" + tm.getDataNetworkType();} else {// 移动网络的 "网络运营商+网络类型" 可以作为标识 (getNetworkOperatorName会随系统语言的改变而返回不同的字符串)//CELLULAR_中国移动_13 或 CELLULAR_China Mobile_13return "CELLULAR_" + tm.getNetworkOperatorName() + "_" + tm.getDataNetworkType();}}}return "UNKNOWN_NETWORK_TYPE";}// 读取上次保存的网络标识public static String getLastNetworkId(Context context) {SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);return prefs.getString(KEY_LAST_NETWORK_ID, null);}// 保存当前的网络标识public static void saveCurrentNetworkId(Context context, String networkId) {SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);SharedPreferences.Editor editor = prefs.edit();editor.putString(KEY_LAST_NETWORK_ID, networkId);editor.apply();}
}
3.4 实现网络变化接收器
当网络变化时候,首先判断是否连接状态。若为连接状态,则判断是否首次开机或变量变化。默认启动应用时即是首次开机,通过设置变量 isFirstConnectionAfterBoot = true
,第一次启动时则上报设备信息,完成就置为 false,后面就只有网络变化影响上报规则了,对比网络环境状态来判断是否上报设备信息。
public class NetworkChangeReceiver extends BroadcastReceiver {private static final String TAG = "NetworkChangeReceiver";public static final String NO_CONNECTION = "NO_CONNECTION";public static volatile boolean isFirstConnectionAfterBoot = true;@Overridepublic void onReceive(Context context, Intent intent) {ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);NetworkInfo activeNetwork = cm.getActiveNetworkInfo();boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();if (isConnected) {if (isFirstConnectionAfterBoot) {LogUtil.i(TAG, "First boot! report device info");ReportScheduler.scheduleInitialReport(context);saveCurrentNetworkSignature(context, getCurrentNetworkSignature(context));isFirstConnectionAfterBoot = false;} else {String currentSignature = getCurrentNetworkSignature(context);String lastSignature = getLastNetworkSignature(context);LogUtil.d(TAG, "Network Connected! currentSignature: " + currentSignature + ", lastSignature: " + lastSignature);if (!currentSignature.equals(NO_CONNECTION) && !currentSignature.equals(lastSignature)) {LogUtil.i(TAG, "Network Changed! report device info");ReportScheduler.scheduleInitialReport(context);saveCurrentNetworkSignature(context, currentSignature);} else {LogUtil.i(TAG, "Network Reconnected! not report device info");}}} else {LogUtil.i(TAG, "Network Disconnected!");}}
}