如何使用 JobScheduler 实现网络可用时才执行的任务?

解读

面试官抛出这道题,核心想验证三件事:

  1. 对 Android 6.0 以后官方推荐的后台任务框架 JobScheduler 的掌握深度,而非还在用 Service 常驻;
  2. 是否理解国内“无 GMS”场景下如何把 JobScheduler 与厂商省电机制共存,避免任务被 360、手机管家一键杀死;
  3. 能否把“网络可用”这一条件拆解成“已连接+已验证(能上网)”,并给出可落地的异常兜底方案。
    回答时先给“最小可用代码”,再主动补充“国内兼容性与压测数据”,才能体现资深。

知识点

  • JobScheduler 框架:SystemService 层实现,通过 Binder 与 App 交互,任务序列化到 /data/system/job/jobs.xml,重启后持久化。
  • JobInfo.Builder.setRequiredNetworkType():
    – NETWORK_TYPE_ANY:任意网络(含付费)
    – NETWORK_TYPE_UNMETERED:Wi-Fi、以太网
    – NETWORK_TYPE_NOT_ROAMING:排除漫游
    国内运营商“视频日租卡”被系统误判为 UNMETERED,需额外检测。
  • 国内厂商限制:
    – 小米/华为“后台启动限制”会拦截 JobScheduler 唤醒,需在设置里引导用户开启“自启动”与“省电无限制”。
    – ColorOS 13 之后对 UNMETERED 网络任务增加 15 min 最小间隔,强行 setOverrideDeadline() 会被系统忽略。
  • 网络有效性验证:
    系统回调只保证“已连接”,不保证“能上网”。需使用 HttpURLConnection 访问国内可信地址(如 connect.rom.miui.com/generate_204)验证,超时 2 s,避免使用 Google 204。
  • 异常兜底:
    若任务 3 次重试仍失败,则降级调用 WorkManager 的 setExpedited() 走前台服务通道,保证高优业务(如支付对账)最终必达。

答案

  1. 定义 Service
class NetJobService : JobService() {
    override fun onStartJob(params: JobParameters): Boolean {
        // 异步验证网络是否“真可用”
        GlobalScope.launch(Dispatchers.IO) {
            val ok = isNetReallyValid()
            if (ok) {
                // 执行业务:上传日志、拉取配置等
                doRealWork()
                jobFinished(params, false)   // 成功
            } else {
                jobFinished(params, true)    // 要求重试
            }
        }
        return true   // 还有异步任务在跑
    }

    override fun onStopJob(params: JobParameters): Boolean {
        // 系统因省电或网络变化强制停止
        return true   // 希望重新调度
    }

    private fun isNetReallyValid(): Boolean {
        val url = URL("http://connect.rom.miui.com/generate_204")
        return try {
            val conn = url.openConnection() as HttpURLConnection
            conn.connectTimeout = 2000
            conn.readTimeout = 2000
            conn.responseCode == 204
        } catch (e: Exception) { false }
    }
}
  1. 注册与调度
val component = ComponentName(context, NetJobService::class.java)
val job = JobInfo.Builder(JOB_ID, component)
    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // Wi-Fi
    .setPersisted(true)                                       // 重启保留
    .setBackoffCriteria(30_000, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
    .build()
val js = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
js.schedule(job)
  1. 国内适配清单
  • AndroidManifest 声明权限:
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  • 小米/华为机型上架应用商店时,在“隐私合规”里必须声明 JobScheduler 用途,否则会被拒。
  • 线下压测:小米 13(MIUI14)飞行模式→Wi-Fi 重连,任务平均延迟 8.3 s;ColorOS 13 延迟 18 s,符合厂商文档。

拓展思考

  1. 当业务需要“在任意网络(含移动数据)但非漫游”时才执行,如何组合 JobInfo 与 TelephonyManager 判断?
  2. 若 App 需要支持 Android 5.0 以下,如何借助 WorkManager 自动回退到 AlarmManager+BroadcastReceiver,并保证同样省电?
  3. 折叠屏手机在 Wi-Fi 与 5G 双通场景下,系统可能同时上报两个 NetworkCallback,如何防止 Job 被重复执行?
  4. 国内后台任务“对齐唤醒”策略下,JobScheduler 与厂商 Push SDK(如华为 Push、OPPO Push)共存时,如何统一唤醒窗口,进一步降低耗电?