这些天我一直在努力寻找在Android中运行永不中止服务的方法。这只是追求同一目标的指南。但愿能帮助到你!php
因为Android 8.0(API级别26)中引入了Android电池优化,后台服务如今有一些重要的限制。基本上,一旦应用程序处于后台运行一段时间,它们就会被杀死,这使得它们对于运行永不中止运行的服务毫无价值。 根据Android的建议,咱们应该使用JobScheduler唤醒锁,在做业运行时让手机保持清醒状态它彷佛运行得很好,而且会为咱们处理唤醒锁。 不幸的是,这是行不通的。最重要的是,JobScheduler打盹模式(你须要将数据发送到你的服务器)的限制列表将根据Android自行决定运行工做,一旦手机进入打盹模式,这些工做的运行频率将不断增长。甚至最糟糕的是,若是你想要访问网络 (你须要将数据发送到你的服务器)你将没法作到。查看打盹模式所施加的限制列表。 若是您不介意没法访问网络而且您不关心不控制周期性,JobScheduler也能够正常工做。在咱们的例子中,咱们但愿咱们的服务以很是特定的频率运行,永远不会中止,因此咱们须要其余东西。html
若是你一直在寻找解决这个问题的互联网,你极可能最终从Android的文档到达了这个页面。 在那里,咱们介绍了Android提供的不一样类型的服务。看一下Foreground Service
描述:java
前台服务执行一些用户能够注意到的操做。例如,音频应用程序将使用前台服务播放音频轨。前台服务必须显示通知。即便用户不与应用程序交互,前台服务也会继续运行。android
这彷佛正是咱们正在寻找的……确实如此!git
建立一个foreground service
真正是一个简单的过程,因此我将访问并解释构建永不中止的前台服务所需的全部步骤。 像往常同样,我已经建立了一个包含全部代码的存储库,以防您想要查看它并跳过帖子的其他部分。 添加一些依赖项github
我在这个例子中使用Kotlin协同程序Fuel,所以咱们将利用协同程序和Fuel库来处理HTTP请求。 为了添加这些依赖项,咱们必须将它们添加到咱们的build.gradle
文件中:shell
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.jaredrummler:android-device-names:1.1.8'
implementation 'com.github.kittinunf.fuel:fuel:2.1.0'
implementation 'com.github.kittinunf.fuel:fuel-android:2.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
复制代码
Foreground Services
须要显示通知,以便用户知道应用程序仍在运行。若是你考虑一下,这是有道理的。 请注意,咱们必须覆盖一些处理服务生命周期关键方面的Service回调方法(callback methods)。 咱们使用部分唤醒锁打盹模式的也很是重要,所以咱们的服务永远不会受到打盹模式的影响。请记住,这会对咱们手机的电池寿命产生影响,所以咱们必须评估咱们的用例是否能够经过Android提供的任何其余替代方案来处理,以便在后台运行流程。 代码中有一些实用函数调用(log
,setServiceState
)和一些自定义枚举(ServiceState.STARTED
),但不要太担忧。若是您想了解它们的来源,请查看示例存储库。编程
class EndlessService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
override fun onBind(intent: Intent): IBinder? {
log("Some component want to bind with the service")
// We don't provide binding, so return null return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { log("onStartCommand executed with startId: $startId") if (intent != null) { val action = intent.action log("using an intent with action $action") when (action) { Actions.START.name -> startService() Actions.STOP.name -> stopService() else -> log("This should never happen. No action in the received intent") } } else { log( "with a null intent. It has been probably restarted by the system." ) } // by returning this we make sure the service is restarted if the system kills the service return START_STICKY } override fun onCreate() { super.onCreate() log("The service has been created".toUpperCase()) var notification = createNotification() startForeground(1, notification) } override fun onDestroy() { super.onDestroy() log("The service has been destroyed".toUpperCase()) Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show() } private fun startService() { if (isServiceStarted) return log("Starting the foreground service task") Toast.makeText(this, "Service starting its task", Toast.LENGTH_SHORT).show() isServiceStarted = true setServiceState(this, ServiceState.STARTED) // we need this lock so our service gets not affected by Doze Mode wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply { acquire() } } // we're starting a loop in a coroutine
GlobalScope.launch(Dispatchers.IO) {
while (isServiceStarted) {
launch(Dispatchers.IO) {
pingFakeServer()
}
delay(1 * 60 * 1000)
}
log("End of the loop for the service")
}
}
private fun stopService() {
log("Stopping the foreground service")
Toast.makeText(this, "Service stopping", Toast.LENGTH_SHORT).show()
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
stopForeground(true)
stopSelf()
} catch (e: Exception) {
log("Service stopped without being started: ${e.message}")
}
isServiceStarted = false
setServiceState(this, ServiceState.STOPPED)
}
private fun pingFakeServer() {
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.mmmZ")
val gmtTime = df.format(Date())
val deviceId = Settings.Secure.getString(applicationContext.contentResolver, Settings.Secure.ANDROID_ID)
val json =
""" { "deviceId": "$deviceId", "createdAt": "$gmtTime" } """
try {
Fuel.post("https://jsonplaceholder.typicode.com/posts")
.jsonBody(json)
.response { _, _, result ->
val (bytes, error) = result
if (bytes != null) {
log("[response bytes] ${String(bytes)}")
} else {
log("[response error] ${error?.message}")
}
}
} catch (e: Exception) {
log("Error making the request: ${e.message}")
}
}
private fun createNotification(): Notification {
val notificationChannelId = "ENDLESS SERVICE CHANNEL"
// depending on the Android API that we're dealing with we will have // to use a specific method to create the notification if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; val channel = NotificationChannel( notificationChannelId, "Endless Service notifications channel", NotificationManager.IMPORTANCE_HIGH ).let { it.description = "Endless Service channel" it.enableLights(true) it.lightColor = Color.RED it.enableVibration(true) it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400) it } notificationManager.createNotificationChannel(channel) } val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, 0) } val builder: Notification.Builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder( this, notificationChannelId ) else Notification.Builder(this) return builder .setContentTitle("Endless Service") .setContentText("This is your favorite endless service working") .setContentIntent(pendingIntent) .setSmallIcon(R.mipmap.ic_launcher) .setTicker("Ticker text") .setPriority(Notification.PRIORITY_HIGH) // for under android 26 compatibility .build() } } 复制代码
是时候处理Android Manifest了json
咱们须要一些额外的权限FOREGROUND_SERVICE
,INTERNET
和WAKE_LOCK
。请确保您不要忘记包含它们,由于它不会起做用。 一旦咱们将它们放到位,咱们将须要声明服务。bash
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.robertohuertas.endless">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
<service android:name=".EndlessService" android:enabled="true" android:exported="false">
</service>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
复制代码
根据Android版本,咱们必须使用特定方法启动服务。 若是Android版本低于API 26,咱们必须使用startServicestartForegroundService。在任何其余状况下,是咱们使用startForegroundService。 在这里你能够看到咱们的MainActivity
,只有一个屏幕,有两个按钮来启动和中止服务。这就是您开始咱们永不中止的服务所需的一切。 请记住,您能够查看此GitHub存储库中的完整代码。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title = "Endless Service"
findViewById<Button>(R.id.btnStartService).let {
it.setOnClickListener {
log("START THE FOREGROUND SERVICE ON DEMAND")
actionOnService(Actions.START)
}
}
findViewById<Button>(R.id.btnStopService).let {
it.setOnClickListener {
log("STOP THE FOREGROUND SERVICE ON DEMAND")
actionOnService(Actions.STOP)
}
}
}
private fun actionOnService(action: Actions) {
if (getServiceState(this) == ServiceState.STOPPED && action == Actions.STOP) return
Intent(this, EndlessService::class.java).also {
it.action = action.name
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
log("Starting the service in >=26 Mode")
startForegroundService(it)
return
}
log("Starting the service in < 26 Mode")
startService(it)
}
}
}
复制代码
效果:在Android启动时启动服务
好的,咱们如今有永不中止的服务,每分钟都按照咱们的意愿发起网络请求,而后用户从新启动手机……咱们的服务不会从新开始……:(失望) 别担忧,咱们也能够为此找到解决方案。咱们将建立一个名为BroadCastReceiverStartReceiver
。
class StartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED && getServiceState(context) == ServiceState.STARTED) {
Intent(context, EndlessService::class.java).also {
it.action = Actions.START.name
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
log("Starting the service in >=26 Mode from a BroadcastReceiver")
context.startForegroundService(it)
return
}
log("Starting the service in < 26 Mode from a BroadcastReceiver")
context.startService(it)
}
}
}
}
复制代码
而后,咱们将再次修改咱们Android Manifest
并添加一个新的权限(RECEIVE_BOOT_COMPLETED
)和咱们新的BroadCastReceiver。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.robertohuertas.endless">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
<service android:name=".EndlessService" android:enabled="true" android:exported="false">
</service>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:enabled="true" android:name=".StartReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
</manifest>
复制代码
请注意,除非服务已在运行,不然不会从新启动该服务。这就是咱们编程的方式,并非说它必须是这样的。 若是您想测试这个,只要启动一个包含谷歌服务的模拟器,并确保在root模式下运行adb。
adb root
# If you get an error then you're not running the proper emulator.
# Be sure to stop the service
# and force a system restart:
adb shell stop
adb shell start
# wait for the service to be restarted!
复制代码