一、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)
参考了以下博客,表示感谢:
个人学习笔记