API 31+下基于Media3实现每日后台预下载媒体资源的方案咨询
基于Media3实现健身媒体预缓存的最佳实践
针对健身应用的媒体预下载需求,结合Android后台限制(尤其是API 31+)和Media3的特性,推荐以下落地方案:
一、用WorkManager替代AlarmManager,精准控制触发时机
放弃手动用AlarmManager唤醒Worker,直接借助WorkManager的PeriodicWorkRequest调度每日预下载任务——它会自动适配系统后台调度规则,同时可配置严格的触发约束:
- 要求设备处于充电状态
- 要求设备联网(可选限制为WiFi)
- 精准设置每日5点的触发时间
示例代码片段:
val constraints = Constraints.Builder() .setRequiresCharging(true) .setRequiredNetworkType(NetworkType.UNMETERED) // 可选:仅允许WiFi下下载 .build() val periodicWork = PeriodicWorkRequestBuilder<PreDownloadWorker>(1, TimeUnit.DAYS) .setInitialDelay(calculateDelayTo5AM(), TimeUnit.MILLISECONDS) .setConstraints(constraints) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "daily_fitness_pre_download", ExistingPeriodicWorkPolicy.REPLACE, periodicWork ) // 计算当前时间到次日5点的延迟时长 private fun calculateDelayTo5AM(): Long { val calendar = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, 5) set(Calendar.MINUTE, 0) set(Calendar.SECOND, 0) } if (calendar.timeInMillis <= System.currentTimeMillis()) { calendar.add(Calendar.DAY_OF_YEAR, 1) } return calendar.timeInMillis - System.currentTimeMillis() }
二、在Worker内正确调用Media3 DownloadManager
在CoroutineWorker的doWork()方法中,先调用setForegroundAsync()将Worker置为前台状态(这是绕过API 31+后台启动限制的核心),然后直接使用Media3的DownloadManager.addDownload()调度下载——绝对不要调用DownloadService.sendAddDownload(),因为该方法会尝试后台启动前台服务,触发ForegroundServiceStartNotAllowedException。
同时为避免ANR,必须用协程异步处理批量下载:
class PreDownloadWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { // 创建前台通知,告知用户正在预下载健身内容 val notification = NotificationCompat.Builder(applicationContext, DOWNLOAD_CHANNEL_ID) .setContentTitle("健身内容预下载") .setContentText("正在为您准备今日健身媒体") .setSmallIcon(R.drawable.ic_download) .build() setForegroundAsync(ForegroundInfo(DOWNLOAD_NOTIFICATION_ID, notification)) val downloadManager = DownloadManager.getInstance(applicationContext) val downloadRequests = getDailyFitnessDownloadRequests() // 自定义方法,获取当日需预下载的媒体请求 // 分批次异步添加下载请求,避免阻塞线程 downloadRequests.chunked(10).forEach { chunk -> withContext(Dispatchers.IO) { chunk.forEach { request -> downloadManager.addDownload(request) } } delay(500) // 批次间短暂延迟,降低系统负载 } return Result.success() } // 自定义方法:生成当日需要预下载的DownloadRequest列表 private fun getDailyFitnessDownloadRequests(): List<DownloadRequest> { // 根据健身流程逻辑,返回对应的视频/音频下载请求 return mutableListOf() } companion object { private const val DOWNLOAD_CHANNEL_ID = "fitness_pre_download_channel" private const val DOWNLOAD_NOTIFICATION_ID = 1001 } }
三、Manifest配置要点
确保正确注册Media3的DownloadService,并声明必要权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.INTERNET" /> <application ...> <service android:name="androidx.media3.exoplayer.download.DownloadService" android:exported="false"> <intent-filter> <action android:name="androidx.media3.exoplayer.download.DownloadService.ACTION_INITIALIZE" /> </intent-filter> </service> </application>
四、之前方案的问题根源
- 方案1失败原因:API 31+禁止后台通过
sendAddDownload()启动前台服务,而DownloadManager内部已封装前台服务的启动逻辑(会自动判断系统权限),直接调用addDownload()即可规避该问题。 - 方案2的ANR问题:批量添加下载请求时在主线程同步执行,或Worker执行时间过长。通过分批次异步处理、添加延迟,可有效避免ANR。
额外优化建议
- 给用户提供预下载开关,让用户自主选择是否开启该功能
- 为预下载的媒体设置过期策略(比如7天后自动删除),避免占用过多存储
- 通过
DownloadManager.Listener监听下载状态,失败时结合WorkManager的重试机制自动重试
内容的提问来源于stack exchange,提问作者Ana
相关产品推荐
相关产品推荐

