توسعه سرویس اندرویدی که هرگز متوقف نمی‌شود — راهنمای کاربردی

۱۸۷ بازدید
آخرین به‌روزرسانی: ۱۲ مهر ۱۴۰۲
زمان مطالعه: ۶ دقیقه
توسعه سرویس اندرویدی که هرگز متوقف نمی‌شود — راهنمای کاربردی

در این مقاله در مورد شیوه ساخت یک سرویس اندروید صحبت می‌کنیم که به صورت نامحدود اجرا شود. اگر به این بحث علاقه‌مند هستید با ما تا انتهای این راهنما همراه باشید.

بیان مسئله

به دلیل معرفی بحث بهینه‌سازی باتری که در نسخه 8.0 اندروید (API 28) معرفی شده است، سرویس‌های پس‌زمینه اینک با محدودیت مهمی مواجه هستند. در واقع این سرویس‌ها زمانی که اپلیکیشن برای مدتی در حالت پس‌زمینه بماند قطع می‌شوند و به این ترتیب ارزش خود را به عنوان سرویسی که باید به طور مداوم در حال اجرا باشند از دست می‌دهند.

بر اساس توصیه‌های اندروید ما باید از JobScheduler استفاده کنیم که به نظر می‌رسد کارکرد خوبی دارد و wakelock-ها را برای ما مدیریت می‌کند و زمانی که وظیفه‌ای در حال اجرا است گوشی را بیدار حفظ می‌کند.

متأسفانه این راه‌حل نیز کار نمی‌کند. JobScheduler کارها را به نیابت از اندروید اجرا می‌کنند و زمانی که گوشی وارد حالت Doze می‌شود، بسامد این کارها نیز به تدریج کاهش می‌یابد. موضوع بدتر این است که اگر بخواهید به شبکه دسترسی پیدا کنید، برای مثال اگر می‌خواهید داده‌هایی را به سرور ارسال کنید، قادر به انجام این کار نخواهید بود. برای مطالعه فهرست محدودیت‌های حالت DOZE به این صفحه (+) مراجعه کنید.

JobScheduler در صورتی که به دسترسی نداشتن به شبکه اهمیتی ندهید و کنترل کردن توالی کارها نیز چندان مهم نباشد به خوبی کار می‌کند. در این مقاله ما می‌خواهیم سرویسی بسازیم که با بسامد کاملاً خاصی کار می‌کند و هرگز متوقف نمی‌شود و از این رو این روش برای ما کار نمی‌کند.

سرویس‌های پیش‌زمینه

اگر تاکنون در اینترنت به دنبال راه‌حلی برای این مشکل گشته باشید، به احتمال زیاد به این صفحه (+) از مستندات اندروید رسیده‌اید. در این صفحه انواع مختلفی از سرویس‌هایی که اندروید ارائه می‌کند معرفی شده‌اند. توضیح «سرویس‌های پیش‌زمینه» (Foreground Service) به صورت زیر است:

سرویس پیش‌زمینه نوعی عملیات را اجرا می‌کند که در دید کاربر قرار دارد. برای نمونه یک اپلیکیشن صوتی می‌تواند از سرویس پیش‌زمینه برای پخش یک قطعه صوتی استفاده کند. سرویس‌های پیش‌زمینه باید یک نوتیفیکیشن نمایش دهند. سرویس‌های پیش‌زمینه حتی زمانی که کاربر با اپلیکیشن در تعامل نیست همچنان به اجرای خود ادامه می‌دهند. به نظر می‌رسد استفاده از سرویس‌های پیش‌زمینه چاره حل مشکل ما است.

کدنویسی

ایجاد یک پردازش foreground service کار نسبتاً سرراستی است و از این رو در ادامه همه مراحل مورد نیاز برای ساخت یک سرویس پیش‌زمینه را توضیح می‌دهیم که هرگز متوقف نخواهد شد.

در صورتی که می‌خواهید مستقیم به کد دسترسی داشته باشید، می‌توانند به این ریپازیتوری گیت‌هاب (+) مراجعه کنید.

افزودن برخی وابستگی‌ها

ما در این مثال از کاتلین استفاده کنیم و بنابراین از کتابخانه‌های coroutines و Fuel برای درخواست‌های HTTP کمک می‌گیریم. برای افزودن این وابستگی‌ها باید آن‌ها را به فایل build.gradle اضافه کنیم:

1dependencies {
2    implementation fileTree(dir: 'libs', include: ['*.jar'])
3    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
4    implementation 'com.android.support:appcompat-v7:28.0.0'
5    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
6    implementation 'com.jaredrummler:android-device-names:1.1.8'
7
8    implementation 'com.github.kittinunf.fuel:fuel:2.1.0'
9    implementation 'com.github.kittinunf.fuel:fuel-android:2.1.0'
10    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1'
11
12    testImplementation 'junit:junit:4.12'
13    androidTestImplementation 'com.android.support.test:runner:1.0.2'
14    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
15}

نوشتن سرویس

سرویس‌های پیش‌زمینه نیاز به یک نوتیفیکیشن دارند که نمایش یابد تا کاربر از این که اپلیکیشن در حال اجرا است آگاهی یابد. توجه کنید که ما برخی از متدهای callback سرویس را که جنبه‌های کلیدی چرخه عمر سرویس را کنترل می‌کنند override کرده‌ایم.

همچنین حتماً باید توجه کنید که ما از یک partial wakelock استفاده کرده‌ایم و از این رو سرویس ما هرگز تحت تأثیر حالت Doze قرار نمی‌گیرد. به خاطر بسپارید که این وضعیت بر عمر باتری گوشی ما تأثیرگذار است و از این رو باید کاملاً بررسی کنیم که آیا این کاربرد از سوی هر گونه روش جایگزین دیگر اندروید برای اجرای پردازش‌ها در پس‌زمینه قابل اجرا است یا نه.

برخی فراخوانی‌های تابع‌های کاربردی (log ،setServiceState) و برخی enum-های سفارشی در کد وجود دارند، اما جای نگرانی زیادی نیست. اگر می‌خواهید بدانید این موارد از کجا ناشی می‌شوند کافی است به ریپازیتوری مثال (+) نگاه کنید.

1class EndlessService : Service() {
2
3    private var wakeLock: PowerManager.WakeLock? = null
4    private var isServiceStarted = false
5
6    override fun onBind(intent: Intent): IBinder? {
7        log("Some component want to bind with the service")
8        // We don't provide binding, so return null
9        return null
10    }
11
12    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
13        log("onStartCommand executed with startId: $startId")
14        if (intent != null) {
15            val action = intent.action
16            log("using an intent with action $action")
17            when (action) {
18                Actions.START.name -> startService()
19                Actions.STOP.name -> stopService()
20                else -> log("This should never happen. No action in the received intent")
21            }
22        } else {
23            log(
24                "with a null intent. It has been probably restarted by the system."
25            )
26        }
27        // by returning this we make sure the service is restarted if the system kills the service
28        return START_STICKY
29    }
30
31    override fun onCreate() {
32        super.onCreate()
33        log("The service has been created".toUpperCase())
34        var notification = createNotification()
35        startForeground(1, notification)
36    }
37
38    override fun onDestroy() {
39        super.onDestroy()
40        log("The service has been destroyed".toUpperCase())
41        Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show()
42    }
43
44    private fun startService() {
45        if (isServiceStarted) return
46        log("Starting the foreground service task")
47        Toast.makeText(this, "Service starting its task", Toast.LENGTH_SHORT).show()
48        isServiceStarted = true
49        setServiceState(this, ServiceState.STARTED)
50
51        // we need this lock so our service gets not affected by Doze Mode
52        wakeLock =
53            (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
54                newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
55                    acquire()
56                }
57            }
58
59        // we're starting a loop in a coroutine
60        GlobalScope.launch(Dispatchers.IO) {
61            while (isServiceStarted) {
62                launch(Dispatchers.IO) {
63                    pingFakeServer()
64                }
65                delay(1 * 60 * 1000)
66            }
67            log("End of the loop for the service")
68        }
69    }
70
71    private fun stopService() {
72        log("Stopping the foreground service")
73        Toast.makeText(this, "Service stopping", Toast.LENGTH_SHORT).show()
74        try {
75            wakeLock?.let {
76                if (it.isHeld) {
77                    it.release()
78                }
79            }
80            stopForeground(true)
81            stopSelf()
82        } catch (e: Exception) {
83            log("Service stopped without being started: ${e.message}")
84        }
85        isServiceStarted = false
86        setServiceState(this, ServiceState.STOPPED)
87    }
88
89    private fun pingFakeServer() {
90        val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.mmmZ")
91        val gmtTime = df.format(Date())
92
93        val deviceId = Settings.Secure.getString(applicationContext.contentResolver, Settings.Secure.ANDROID_ID)
94
95        val json =
96            """
97                {
98                    "deviceId": "$deviceId",
99                    "createdAt": "$gmtTime"
100                }
101            """
102        try {
103            Fuel.post("https://jsonplaceholder.typicode.com/posts")
104                .jsonBody(json)
105                .response { _, _, result ->
106                    val (bytes, error) = result
107                    if (bytes != null) {
108                        log("[response bytes] ${String(bytes)}")
109                    } else {
110                        log("[response error] ${error?.message}")
111                    }
112                }
113        } catch (e: Exception) {
114            log("Error making the request: ${e.message}")
115        }
116    }
117
118    private fun createNotification(): Notification {
119        val notificationChannelId = "ENDLESS SERVICE CHANNEL"
120
121        // depending on the Android API that we're dealing with we will have
122        // to use a specific method to create the notification
123        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
124            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
125            val channel = NotificationChannel(
126                notificationChannelId,
127                "Endless Service notifications channel",
128                NotificationManager.IMPORTANCE_HIGH
129            ).let {
130                it.description = "Endless Service channel"
131                it.enableLights(true)
132                it.lightColor = Color.RED
133                it.enableVibration(true)
134                it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
135                it
136            }
137            notificationManager.createNotificationChannel(channel)
138        }
139
140        val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent ->
141            PendingIntent.getActivity(this, 0, notificationIntent, 0)
142        }
143
144        val builder: Notification.Builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder(
145            this,
146            notificationChannelId
147        ) else Notification.Builder(this)
148
149        return builder
150            .setContentTitle("Endless Service")
151            .setContentText("This is your favorite endless service working")
152            .setContentIntent(pendingIntent)
153            .setSmallIcon(R.mipmap.ic_launcher)
154            .setTicker("Ticker text")
155            .setPriority(Notification.PRIORITY_HIGH) // for under android 26 compatibility
156            .build()
157    }
158}

بررسی مانیفست اندروید

ما برای FOREGROUND_SERVICE ،INTERNET و WAKE_LOCK به برخی مجوزهای بیشتر نیاز داریم. مطمئن شوید که آن‌ها را حتماً درج کرده‌اید، چون در غیر این صورت کار نمی‌کند. زمانی که آن‌ها را در جای خود قرار دادید، باید سرویس خود را اعلان کنیم:

1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
3          package="com.robertohuertas.endless">
4
5    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
6    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
7    <uses-permission android:name="android.permission.WAKE_LOCK" />
8
9    <application
10            android:allowBackup="true"
11            android:icon="@mipmap/ic_launcher"
12            android:label="@string/app_name"
13            android:roundIcon="@mipmap/ic_launcher_round"
14            android:supportsRtl="true"
15            android:theme="@style/AppTheme">
16
17        <service
18                android:name=".EndlessService"
19                android:enabled="true"
20                android:exported="false">
21        </service>
22
23        <activity android:name=".MainActivity">
24            <intent-filter>
25                <action android:name="android.intent.action.MAIN"/>
26                <category android:name="android.intent.category.LAUNCHER"/>
27            </intent-filter>
28        </activity>
29    </application>
30</manifest>

روش آغاز کردن سرویس

باید بسته به نسخه اندروید، سرویس خود را با متدهای مختلفی آغاز کنیم. در نسخه‌های اندروید زیر API 26 باید از startService استفاده کنیم و در موارد دیگر می‌بایست از startForegroundService استفاده شود.

در ادامه MainActivity ما را می‌بینید که صرفاً صفحه‌ای با دو دکمه آغاز و توقف سرویس است. این‌ها تنها مواردی هستند که برای اجرای سرویس بی‌پایان نیاز داریم.

1class MainActivity : AppCompatActivity() {
2
3    override fun onCreate(savedInstanceState: Bundle?) {
4        super.onCreate(savedInstanceState)
5        setContentView(R.layout.activity_main)
6
7        title = "Endless Service"
8
9        findViewById<Button>(R.id.btnStartService).let {
10            it.setOnClickListener {
11                log("START THE FOREGROUND SERVICE ON DEMAND")
12                actionOnService(Actions.START)
13            }
14        }
15
16        findViewById<Button>(R.id.btnStopService).let {
17            it.setOnClickListener {
18                log("STOP THE FOREGROUND SERVICE ON DEMAND")
19                actionOnService(Actions.STOP)
20            }
21        }
22    }
23
24    private fun actionOnService(action: Actions) {
25        if (getServiceState(this) == ServiceState.STOPPED && action == Actions.STOP) return
26        Intent(this, EndlessService::class.java).also {
27            it.action = action.name
28            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
29                log("Starting the service in >=26 Mode")
30                startForegroundService(it)
31                return
32            }
33            log("Starting the service in < 26 Mode")
34            startService(it)
35        }
36    }
37}

نکته مهم: آغاز سرویس در زمان بوت شدن اندروید

ما اینک سرویس بی‌پایانی داریم که بنا به خواست ما هر دقیقه یک بار یک درخواست شبکه ارسال می‌کند اما اگر کاربر گوشی را ری‌استارت کند چطور؟ در این حالت سرویس ما در زمان بوت شدن اندروید به صورت خودکار اجرا نمی‌شود و این یک مشکل محسوب می‌شود. اما جای نگرانی نیست ما راه‌حلی برای این مسئله نیز یافته‌ایم. به این منظور یک BroadCastReceiver به نام StartReceiver می‌سازیم.

1class StartReceiver : BroadcastReceiver() {
2
3    override fun onReceive(context: Context, intent: Intent) {
4        if (intent.action == Intent.ACTION_BOOT_COMPLETED && getServiceState(context) == ServiceState.STARTED) {
5            Intent(context, EndlessService::class.java).also {
6                it.action = Actions.START.name
7                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
8                    log("Starting the service in >=26 Mode from a BroadcastReceiver")
9                    context.startForegroundService(it)
10                    return
11                }
12                log("Starting the service in < 26 Mode from a BroadcastReceiver")
13                context.startService(it)
14            }
15        }
16    }
17}

سپس Android Manifest را بار دیگر تغییر می‌دهیم و یک مجوز جدید به نام RECEIVE_BOOT_COMPLETED و کلاس BroadCastReceiver را اضافه می‌کنیم.

1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3          package="com.robertohuertas.endless">
4    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
5    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
6    <uses-permission android:name="android.permission.WAKE_LOCK" />
7    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
8
9    <application
10            android:allowBackup="true"
11            android:icon="@mipmap/ic_launcher"
12            android:label="@string/app_name"
13            android:roundIcon="@mipmap/ic_launcher_round"
14            android:supportsRtl="true"
15            android:theme="@style/AppTheme">
16
17        <service
18                android:name=".EndlessService"
19                android:enabled="true"
20                android:exported="false">
21        </service>
22
23        <activity android:name=".MainActivity">
24            <intent-filter>
25                <action android:name="android.intent.action.MAIN"/>
26                <category android:name="android.intent.category.LAUNCHER"/>
27            </intent-filter>
28        </activity>
29
30        <receiver android:enabled="true" android:name=".StartReceiver">
31            <intent-filter>
32                <action android:name="android.intent.action.BOOT_COMPLETED"/>
33            </intent-filter>
34        </receiver>
35
36    </application>
37</manifest>

بدین ترتیب به پایان این راهنما می‌رسیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
robertohuertasm
۱ دیدگاه برای «توسعه سرویس اندرویدی که هرگز متوقف نمی‌شود — راهنمای کاربردی»

مقاله خوبیه مرسی از زحماتتون ولی یه سوال برام پیش اومد شما میگید که از partial wake lock استفاده کنیم ولی توی این لینک اومده که داز مود wake lock رو ایگنور میکنه

https://developer.android.com/training/monitoring-device-state/doze-standby

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *