用 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 服务会拥有高的运行优先级,从而能在后台持续运行,被系统回收的概率极低。

相关推荐
祖国的好青年13 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴13 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭14 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首14 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil15 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙15 小时前
echarts,3d堆叠图
android·3d·echarts
李白的天不白15 小时前
如何项目发布到github上
android·vue.js
summerkissyou198715 小时前
Android-RTC、NTP 和 System Time(系统时间)
android
小书房15 小时前
Kotlin使用体验及理解1
android·开发语言·kotlin
撩得Android一次心动16 小时前
Android Navigation 组件全面讲解
android·jetpack·navigation