構建永不中止運行的Android服務

這些天我一直在努力尋找在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'
}
複製代碼

咱們的service

Foreground Services須要顯示通知,以便用戶知道應用程序仍在運行。若是你考慮一下,這是有道理的。 請注意,咱們必須覆蓋一些處理服務生命週期關鍵方面的Service回調方法(callback methods)。 咱們使用部分喚醒鎖打盹模式的也很是重要,所以咱們的服務永遠不會受到打盹模式的影響。請記住,這會對咱們手機的電池壽命產生影響,所以咱們必須評估咱們的用例是否能夠經過Android提供的任何其餘替代方案來處理,以便在後臺運行流程。 代碼中有一些實用函數調用(logsetServiceState)和一些自定義枚舉(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_SERVICEINTERNETWAKE_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 &amp;&amp; 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 &amp;&amp; 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!
複製代碼

Enjoy!

相關文章
相關標籤/搜索