添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

一、Foreground Service

从android O版本开始,google为了控制资源使用,对service的后台运行做了限制。service启动后,应用退到后台,如果没有任务执行,静默一段时间(几分钟)后service就会停止。Android 8.0引入了一个新方法 Context.startForegroundService() 来直接启动一个前台Service,但是当系统创建这个前台Service后,应用需要在5秒内调用 Service.startForeground() 来显示一个前台通知,否则系统会停止这个前台Service,并弹出ANR。 从Android 9.0开始(Android P, API 28),如果要创建前台Service,还要在AndroidManifest.xml中声明 android.permission.FOREGROUND_SERVICE 权限,这是一个普通的权限,系统会自动授予app。如果不这样做,会抛出异常。

二、封装的要点和思路

1、判断版本如果大于 Android.O 就开启前台服务,否则只是普通的混合开启 service
2、 Service 负责实现具体代码逻辑, Service 中的方法由管理类统一调用。
3、前台通知可点击跳转到具体的 Activity ,也可以直接移除不显示。

注意的点:

1、提前创建好 Notification ,如果 Notifacation null 不能开启前台服务。
2、开启前台服务时将 Notification 对象通过 Intent 传递到 Service onStartCommand 方法中。
3、在 Service onStartCommand 方法中开启前台通知 startForeground
4、前台通知是不能通过点击或者设置 setAutoCancel(true) 让它消失的,它会一直存在,除非调用 stopForeground(true) ,这也是开启前台服务后直接移除通知的办法。

模拟文件上传下载的存储管理服务

1、设置权限

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

2、Service对外暴露方法调用的管理类

* StoreControlService的客户端管理类,可以调用Service所有的方法。不能直接使用Service对象。 class StoreClient { private var controlService: StoreControlService? = null //service对象,用于调用service的方法 private val serviceConnection = object: ServiceConnection { override fun onServiceConnected(name: ComponentName, binder: IBinder) { if (StoreServiceBinder::class.java.isAssignableFrom(binder.javaClass)) { //获取Service对象 controlService = (binder as StoreServiceBinder).getService()?.get serviceBound = true override fun onServiceDisconnected(name: ComponentName) { private lateinit var mServiceStartIntent: Intent // notification for Foreground Service private var foregroundServiceNotification: Notification? = null //前台服务的通知 private var foregroundServiceNotificationId = -1 //前台服务的通知的Id @Volatile private var serviceBound = false //service是否绑定 * 单例模式 companion object { private val SERVICE_NAME = StoreControlService::class.java.name private var instance: StoreClient? = null @Synchronized fun getInstance(): StoreClient { //单例 if (instance == null) { instance = StoreClient() return instance!! * 如果是>= Build.VERSION_CODES.O ,则需要设置前台服务 * 通知Notification的内容需要自定义 fun setForegroundService(notification: Notification, id: Int) { foregroundServiceNotification = notification foregroundServiceNotificationId = id * 绑定服务 fun bindService(context: Context) { mServiceStartIntent = Intent().apply { setClassName(context, SERVICE_NAME) var service: Any? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && foregroundServiceNotification != null) { Log.d("TAG", "startForegroundService") //将Notification放入Intent,在Service的onStartCommand方法中获取 mServiceStartIntent.putExtra( StoreControlService.FOREGROUND_SERVICE_NOTIFICATION, foregroundServiceNotification //将NotificationId放入Intent,在Service的onStartCommand方法中获取 mServiceStartIntent.putExtra( StoreControlService.FOREGROUND_SERVICE_NOTIFICATION_ID, foregroundServiceNotificationId //开启前台服务并传递Intent service = context.startForegroundService(mServiceStartIntent) } else { Log.d("TAG", "startService") try { //开启普通服务并传递Intent service = context.startService(mServiceStartIntent) } catch (ex: IllegalStateException) { if (service == null) { throw RuntimeException("cannot start service $SERVICE_NAME") Log.d("TAG", "bindService") //绑定服务,采用混合开启的方式 context.bindService(mServiceStartIntent, serviceConnection, Context.BIND_AUTO_CREATE) * 解绑服务 fun unbindService(context: Context) { if (serviceBound) { //如果有绑定 try { context.unbindService(serviceConnection) //解绑 context.stopService(mServiceStartIntent) //停止服务 serviceBound = false } catch (e: IllegalArgumentException) { * 上传文件的方法供外部调用,具体实现逻辑在Service中 fun uploadFile() { controlService?.uploadFile() * 下载文件的方法供外部调用,具体实现逻辑在Service中 fun downloadFile() { controlService?.downloadFile()

2、Service的代码

class StoreControlService : LifecycleService() {
    private var storeServiceBinder: StoreServiceBinder? = null
    companion object {
        val FOREGROUND_SERVICE_NOTIFICATION_ID =
            StoreControlService::class.java.simpleName + ".FOREGROUND_SERVICE_NOTIFICATION_ID"
        val FOREGROUND_SERVICE_NOTIFICATION =
            StoreControlService::class.java.simpleName + ".FOREGROUND_SERVICE_NOTIFICATION"
    override fun onCreate() {
        super.onCreate()
        storeServiceBinder = StoreServiceBinder(this)
    override fun onBind(intent: Intent): IBinder? {
        super.onBind(intent)
        storeServiceBinder?.binderToken =
            intent.getStringExtra(StoreConstants.CALLBACK_ACTIVITY_TOKEN)
        return storeServiceBinder
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        //通过Intent获取创建好的Notification
            val foregroundServiceNotification =
                intent?.getParcelableExtra<Notification>(FOREGROUND_SERVICE_NOTIFICATION)
            if (foregroundServiceNotification != null) {
            //开启前台通知
                startForeground(
                    intent.getIntExtra(FOREGROUND_SERVICE_NOTIFICATION_ID, 1),
                    foregroundServiceNotification
                //直接移除前台通知,也可以给Notification设置PendingIntent点击实现跳转
                stopForeground(true)
        return START_STICKY
     * 上传文件
    fun uploadFile() {
        //具体实现逻辑代码略...
     * 下载文件
    fun downloadFile() {
        //具体实现逻辑代码略...
    override fun onDestroy() {
        if (storeServiceBinder != null) {
            storeServiceBinder = null
        super.onDestroy()
//通知工具类
internal object Notify {
    private var MessageID = 100
    private const val channelId = "chn-01"
    private const val channelFireBaseMsg = "Chn Store"
    private val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
    } else {
        PendingIntent.FLAG_UPDATE_CURRENT
    //发通知
    fun notification(
        context: Context,
        messageString: String,
        intent: Intent?,
        notificationTitle: Int
        //Get the notification manage which we will use to display the notification
        val ns = Context.NOTIFICATION_SERVICE
        val notificationManager = context.getSystemService(ns) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                channelId,
                channelFireBaseMsg,
                NotificationManager.IMPORTANCE_LOW
            notificationChannel.enableLights(true)
            notificationChannel.lightColor = Color.RED
            notificationChannel.enableVibration(true)
            notificationChannel.vibrationPattern =
                longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
            notificationManager.createNotificationChannel(notificationChannel)
        val `when` = System.currentTimeMillis()
        //get the notification title from the application's strings.xml file
        val contentTitle: CharSequence = context.getString(notificationTitle)
        //the message that will be displayed as the ticker
        val ticker = "$contentTitle $messageString"
        //build the pending intent that will start the appropriate activity
        val pendingIntent = PendingIntent.getActivity(context, 0, intent, pendingIntentFlags)
        //build the notification
        val notificationCompat = NotificationCompat.Builder(context, channelId)
        notificationCompat.setAutoCancel(true)
            .setContentTitle(contentTitle)
            .setContentIntent(pendingIntent)
            .setContentText(messageString)
            .setTicker(ticker)
            .setWhen(`when`)
            .setSmallIcon(R.mipmap.app_icon_round)
        val notification = notificationCompat.build()
        notificationManager.notify(MessageID, notification)
        MessageID++
    //创建前台通知对象
    fun foregroundNotification(
        context: Context,
        connectionName: String,
        intent: Intent?,
        notificationTitle: Int
    ): Notification {
        //Get the notification manage which we will use to display the notification
        val ns = Context.NOTIFICATION_SERVICE
        val notificationManager = context.getSystemService(ns) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                channelId,
                channelFireBaseMsg,
                NotificationManager.IMPORTANCE_LOW
            notificationChannel.enableLights(true)
            notificationChannel.lightColor = Color.RED
            notificationChannel.enableVibration(true)
            notificationChannel.vibrationPattern =
                longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
            notificationManager.createNotificationChannel(notificationChannel)
        val `when` = System.currentTimeMillis()
        //get the notification title from the application's strings.xml file
        val contentTitle: CharSequence = context.getString(notificationTitle)
        //the message that will be displayed as the ticker
        val ticker = "$contentTitle $connectionName"
        //build the pending intent that will start the appropriate activity
        val pendingIntent =
            PendingIntent.getActivity(context, 0, intent ?: Intent(), pendingIntentFlags)
        //build the notification
        val notificationCompat = NotificationCompat.Builder(context, channelId)
        notificationCompat
            .setAutoCancel(true)
            .setContentTitle(contentTitle)
            .setContentIntent(pendingIntent)
            .setContentText(connectionName)
            .setTicker(ticker)
            .setWhen(`when`)
            .setSmallIcon(R.mipmap.app_icon_round)
        return notificationCompat.build()
internal interface StoreConstants {
    companion object {
        const val CALLBACK_ACTIVITY_TOKEN = "activityToken"
//弱引用,不然会有内存泄漏
class StoreServiceBinder() : Binder() {
    var binderToken: String? = null
    private var weakReferenceService: WeakReference<StoreControlService>? = null
    constructor(service: StoreControlService) : this() {
        weakReferenceService = WeakReference(service)
    fun getService(): WeakReference<StoreControlService>? = weakReferenceService

完整的封装就在上面了

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //如果大于8.0需要设计好Notification的显示内容
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
        //这是Notification点击跳转使用的PendingIntent,如果想直接移除前台通知,下面的pendingIntent参数可给null
            val pendingIntent = Intent(this, MainActivity::class.java).apply {   
                flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
            //创建一个前台通知的Notification对象
            val foregroundNotification = Notify.foregroundNotification(
                this,
                "我是通知的内容",
                pendingIntent,
                R.string.notification_title  //通知的标题
            //先设置好Notification和NotificationId
            StoreClient.getInstance().setForegroundService(foregroundNotification, 100)
        //再开启服务
        StoreClient.getInstance().bindService(this)
    override fun onDestroy() {
        super.onDestroy()
        //解绑服务
        StoreClient.getInstance().unbindService(this)

参考了以下博客,表示感谢:

Android O对后台Service限制

个人学习笔记

分类:
Android
标签: