用 Kotlin 协程构建一个前台服务

前言

前面我们说过,从 Android 8.0 开始,系统为了优化电池和内存,会严格限制后台活动。应用一旦进入后台,Service 随时可能被系统回收。如果需要一个长时间运行的后台任务,就可以使用前台 Service。

前台 Service 会在系统状态栏中显示一个常驻通知,让用户清楚该应用正在后台活动。

那我们来看看如何创建一个前台 Service。

前台 Service 代码实现

MyService 中的代码如下:

kotlin 复制代码
class MyService : Service() {

    companion object {
        private const val TAG = "MyService"
        private const val CHANNEL_ID = "my_service_channel"
        private const val NOTIFICATION_ID = 1
    }


    // 创建协程作用域
    private val serviceScope = CoroutineScope(Dispatchers.Main)
    // 持有我们的任务,以便后续可以取消它
    private var timerJob: Job? = null

    override fun onBind(intent: Intent?): IBinder? = null



    /**
     * 创建通知渠道
     */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "前台服务渠道",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }
    }

    /**
     * 服务启动的回调
     */
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 创建通知渠道
        createNotificationChannel()

        // 构建意图
        val pendingIntent: PendingIntent =
            PendingIntent.getActivity(
                this,
                0,
                Intent(this, MainActivity::class.java),
                PendingIntent.FLAG_IMMUTABLE  // FLAG_IMMUTABLE 可防止其他应用篡改 PendingIntent
            )

        // 创建通知
        val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("服务运行中")
            .setContentText("正在执行关键后台任务...")
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.avatar)) // 我的头像
            .setContentIntent(pendingIntent)
            .build()

        // 提升为前台服务
        startForeground(NOTIFICATION_ID, notification)

        // 启动协程来执行实际的耗时任务
        timerJob = serviceScope.launch {
            while (isActive) {
                // 执行我任务:打印当前时间
                val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
                val currentTime = sdf.format(Date())
                Log.d(TAG, "服务正在运行, 当前时间: $currentTime")

                delay(1000)
            }
        }


        return START_STICKY // 服务被关闭后,系统会重新创建
    }

    override fun onDestroy() {
        super.onDestroy()
        // 服务销毁时,取消协程
        timerJob?.cancel()
        Log.d(TAG, "服务已销毁,协程任务已取消")
    }
}

然后,在 MainActivity 中申请通知权限并启动服务。代码如下:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding


    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
            if (isGranted) {
                // 权限被授予
                startMyService()
            } else {
                // 权限被拒绝,向用户解释
                Toast.makeText(this, "需要通知权限以显示服务状态", Toast.LENGTH_SHORT).show()
            }
        }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)


        binding.startServiceBtn.setOnClickListener {
            checkPermissionAndStartService()
        }

        binding.stopServiceBtn.setOnClickListener {
            val intent = Intent(this, MyService::class.java)
            stopService(intent) // 停止服务
        }

    }

    /**
     * 检查通知权限是否被授予并启动服务
     */
    private fun checkPermissionAndStartService() {
        // 动态请求通知权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
                // 已有权限,直接启动服务
                startMyService()
            } else {
                // 请求通知权限
                requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
            }
        } else {
            startMyService()
        }
    }

    /**
     * 启动 MyService 服务
     */
    private fun startMyService() {
        val intent = Intent(this, MyService::class.java)
        // 使用 ContextCompat.startForegroundService 来启动
        ContextCompat.startForegroundService(this, intent)
    }

}

清单文件配置

启动前台 Service 需要进行权限声明,在 Android 13+ 后发送通知也需要声明权限。另外,从 Android 14 开始,我们要为前台服务指定具体用途(类型),并且必须声明一个与服务类型匹配的、更具体的权限。

所以 AndroidManifest.xml 文件中的代码如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

    <application ...>

        <service
            android:name=".MyService"
            android:exported="false"
            android:foregroundServiceType="dataSync">
        </service>

    </application>

</manifest>

因为是示例,所以前台服务的类型就指定为了 dataSync(数据同步),比如数据的上传和下载、备份和恢复、导入或导出、本地文件处理等。

现在重新运行程序,并点击"Start Service"按钮,MyService 就会以前台 Service 的模式启动了。即使你退出应用,MyService 服务会拥有高的运行优先级,从而能在后台持续运行,被系统回收的概率极低。

相关推荐
Cutecat_41 分钟前
视频字幕处理工具横向:提取模式 vs 编辑模式,该如何选择
android·前端·ios·语音识别
2601_961765292 小时前
【分享】PlayerPro媒体音乐播放器 完整专业版
android·媒体
JohnnyDeng944 小时前
【Android】Android 包体积优化:R8/ProGuard 深度配置全攻略
android·性能优化·kotlin·jetpack
故渊at4 小时前
第九板块:Android 多媒体体系 | 第二十四篇:Camera Service 与 HAL3 成像流水线
android·camera·多媒体体系·hal3
Jinkxs8 小时前
Python基础 - 初识内置函数 Python自带的便捷工具
android·java·python
私人珍藏库8 小时前
【Android】VLLO-韩国热门手机剪辑APP
android·app·工具·软件·多功能
Cloud_Shy6189 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第六章 Item 40 - 43)
android·开发语言·人工智能·笔记·python·学习方法
AFinalStone9 小时前
Android12 U盘插拔链路源码全解析(五):Framework层(下) StorageManagerService
android·frameworks
林九生11 小时前
【实用技巧】MySQL 绿色版一键路径更新脚本详解 —— update_path.bat 深度解析
android·数据库·mysql
故渊at12 小时前
第十三板块:Android 综合架构与未来演进 | 第三十一篇:Android 架构演进与 Fuchsia OS 的挑战
android·架构·宏内核·微内核·fuchsia·ipc 性能博弈