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

相关推荐
爬山算法23 分钟前
MySQL(84)如何配置MySQL防火墙?
android·数据库·mysql
爬山算法29 分钟前
MySQL(83)如何设置密码复杂度策略?
android·数据库·mysql
鹏说大数据32 分钟前
MySQL5.7 慢查询SQL语句集合
android·sql·adb
锋风2 小时前
安卓官方版fat-aar:使用Fused Library将多个Android库发布为一个库
android
Renounce2 小时前
【Android】四大组件ContentProvider
android
东风西巷4 小时前
有道翻译官手机版:智能翻译,随行助手
android·智能手机·软件需求
Renounce4 小时前
【Android】四大组件BroadcastReceiver
android
_一条咸鱼_6 小时前
Android Gson基础数据类型转换逻辑(6)
android·面试·gson
_一条咸鱼_6 小时前
Android Runtime并发标记与三色标记法实现原理(55)
android·面试·android jetpack