จากที่เคยอธิบายไปแล้วว่าการสร้าง Service ขึ้นมานั้นจะสามารถเลือกได้ว่าจะใช้เป็นคลาส Service หรือคลาส Intent Service ซึ่งมีรูปแบบการทำงานแตกต่างกัน อยู่ที่ว่าจะให้ทำอะไรก็เลือกใช้ให้เหมาะสม สำหรับบทความนี้ก็จะมาดูกันว่าเวลาต้องเขียนโค้ดจริงๆเนี่ยการสร้าง Service จากทั้ง 2 คลาสนี้มันต่างกันยังไงบ้าง

บทความในซีรีย์เดียวกัน

Service กับ Intent Service ต่างกันยังไงนะ?

ขอทบทวนความทรงจำกันซักเล็กน้อยก่อนที่จะพูดถึงโค้ด เพราะคลาสทั้ง 2 ตัวนี้มีรูปแบบการทำงานที่แตกต่างกันดังนี้

Service

เป็น Base Class ของ Service ทุกๆตัว โดยทำงานอยู่บน Main Thread และเมื่อมีการสั่งงานจากหลายๆที่พร้อมๆกัน มันก็จะทำงานพร้อมๆกันทันที (Parallel)

การทำงานแบบ Parallel จะเหมาะกับงานที่สามารถจบได้ในตัวมันเอง

ยกตัวอย่างเช่น เจ้าของบล็อกต้องการสร้าง Service สำหรับเก็บข้อมูลลงใน Database โดยกำหนดช่วงเวลาที่จะเก็บข้อมูลดังนี้

  • เมื่อเครื่องเสียบชาร์จไฟ
  • เมื่อผ่านไปทุกๆ 15 นาที
  • เมื่อเครื่องต่อ WiFi

โดยจะเห็นว่าทั้ง 3 เงื่อนไขนี้มีโอกาสทำงานพร้อมๆกันได้ ดังนั้นการทำงานแบบ Parallel ของคลาส Service ก็จะตอบโจทย์นี้ได้อย่างพอดิบพอดี และจะยิ่งเหมาะมากๆถ้าไม่จำเป็นต้องส่งค่าให้ Service ผ่าน Intent

Intent Service

เป็นคลาสที่ประยุกต์มาจากคลาส Service อีกทีหนึ่ง โดยมีการสร้าง Worker Thread ขึ้นมาให้แล้ว และ Worker Thread ที่ว่านี้ยังทำงานเป็นแบบ Queue ด้วย เวลาถูกเรียกใช้งานจากหลายๆที่พร้อมๆกัน Intent Service จะเริ่มจากตัวแรกสุดที่เรียกใช้งานก่อน เมื่อทำงานเสร็จแล้วก็จะทำของตัวถัดไปเรื่อยๆ

ความสามารถ Queue ของ Intent Service ทำให้เหมาะกับงานที่ต้องการให้วนทำอะไรบางอย่างทีละอันจนเสร็จ เช่น การทำ Upload Multiple Photos ที่ให้ผู้ใช้สามารถเลือกรูปภาพหลายๆภาพเพื่ออัปโหลดขึ้น Web Server ได้ แต่ว่าไม่อยากให้ภาพถูกอัปโหลดพร้อมๆกัน อยากจะให้อัปโหลดทีละภาพแทน

มาดูคำสั่งเมื่อสร้าง Service จากคลาส Service กัน

จะยกตัวอย่างเป็น Background Service นะ เพราะใช้คำสั่งน้อย ดูแล้วเข้าใจง่าย

ในการสร้าง Service ซักตัวจากคลาส Service ก็มีลักษณะแบบนี้

// AwesomeBackgroundService.kt
class AwesomeBackgroundService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        ...
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        ...
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        ...
    }
}

ถึงแม้ว่าจะสร้างเป็น Background Service แต่ว่าคลาส Service จะบังคับให้ประกาศ onBind(...) ด้วย ก็ให้ประกาศซะ แล้ว Return ค่าเป็น Null ไป และในตัวอย่างข้างบนนี้จะกำหนดใน onStartCommand(...) ให้ Return ค่าเป็น START_NOT_STICKY ซึ่งจะทำให้ Service ไม่ Restart เมื่อถูกทำลายจากระบบแอนดรอยด์เพื่อคืน Memory ดังนั้นถ้าอยากให้ Restart ก็ให้กำหนดเป็น START_STICKY แทนนะ

เพื่อความสมจริงมากขึ้น ลองมายกตัวอย่างการทำงานที่ต้องการดีกว่า โดยเจ้าของบล็อกอยากจะเก็บข้อมูลจาก Temperature Sensor ของตัวเครื่องเพื่อดูว่าอุณหภูมิรอบๆในแต่ละครั้งมีค่ากี่องศาเซลเซียส โดยจะเก็บลงในฐานข้อมูลเพื่อนำไปใช้งานทีหลัง

เขียนให้ทำงานบน Main Thread โดยตรง (วิธีนี้ไม่ค่อยเหมาะ)

เพื่อความรวดเร็วและง่ายสำหรับการเขียนโค้ด ส่วนใหญ่จึงเขียนให้มันทำงานบน Main Thread ซะเลย

// TemperatureLoggerService.kt

class TemperatureLoggerService : Service() {
    private lateinit var temperatureSensor: Sensor
    private lateinit var sensorManager: SensorManager

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        temperatureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        sensorManager.registerListener(temperatureEventListener, temperatureSensor, SensorManager.SENSOR_DELAY_NORMAL)
        return START_NOT_STICKY
    }

    private fun saveTemperatureToDatabase(temperature: Float) {
        ...
    }

    private fun completeTemperatureLoggerService() {
        sensorManager.unregisterListener(temperatureSensor, temperatureSensor)
        stopSelf()
    }

    private val temperatureEventListener = object : SensorEventListener {
        override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
        }

        override fun onSensorChanged(event: SensorEvent) {
            val temperature = event.values[0]
            saveTemperatureToDatabase(temperature)
            completeTemperatureLoggerService()
        }
    }
}

ปัญหาของการใช้ Main Thread โดยตรงจะมีปัญหาหลักๆคือถ้าคำสั่ง saveTemperatureToDatabase(...) ทำงานนานแล้วผู้ใช้กำลังเปิดแอปฯอยู่ ก็จะทำให้ Block UI ได้

แต่ผู้ที่หลงเข้ามาอ่านบางคนอาจจะบอกว่า

“เฮ้ย คำสั่งในนั้นทำงานไวมาก มันไม่ Block UI หรอก โค้ดแบบนี้รับรองว่าไม่มีปัญหาแน่นอน”

จริงๆแล้วการเขียนแบบนี้นอกจากจะมีปัญหาเรื่อง Block UI แล้ว ยังมีปัญหาเรื่องการทำงานแบบ Parallel ที่ไม่ลงตัวอยู่ เพราะว่าในโค้ดตัวอย่างนี้เมื่อ onSensorChanged(...) ถูกเรียก ก็จะดึงค่าอุณหภูมิมาเก็บลงใน Database ด้วยคำสั่ง saveTemperatureToDatabase(...) เนอะ และเมื่อเสร็จแล้วก็จะทำลาย Service ทิ้งด้วยคำสั่ง completeTemperatureLoggerService() ที่ข้างในนี้มีคำสั่ง stopSelf() อยู่ด้วย นั่นล่ะปัญหา

เพราะว่าถ้า Service ถูกเรียกให้เก็บข้อมูลพร้อมๆกัน (เช่น เวลาผ่านไป 15 นาทีและผู้ใช้เสียบชาร์จไฟพอดิบพอดีตามเงื่อนไขที่เจ้าของบล็อกกำหนดไว้) ตัวที่ถูกเรียกก่อนก็จะทำงานจนเสร็จแล้วทำลาย Service ทิ้งทันทีโดยไม่สนใจว่าตัวถัดไปจะทำงานเสร็จแล้วหรือยัง

เพื่อไม่ให้ทำลาย Service โดยไม่ได้ตั้งใจแบบนี้ คำสั่ง stopSelf() จึงมี 2 แบบให้เลือกใช้งาน

stopSelf()
stopSelf(startId: Int)

ถ้าเป็น stopSelf() จะเป็นการทำลาย Service ทิ้งโดยทันที แต่ถ้าเป็น stopSelf(startId: Int) จะเป็นการลบ startId นั้นๆออกจาก List ที่เรียกใช้งาน Service เท่านั้น ถ้า startId ที่สร้างขึ้นใน onStartCommand(...) ถูกสั่ง stopSelf(...) จนครบทุกตัว Service ถึงจะถูกทำลาย

แล้วจะส่ง startId จาก onStartCommand(...) ไปยังตำแหน่งที่เรียกใช้คำสั่ง stopSelf(...) ยังไงดี?

ผู้ที่หลงเข้ามาอ่านบางคนอาจจะตอบว่า “ก็เก็บไว้เป็น Global Variable ซะเลยสิ”

// TemperatureLoggerService.kt
class TemperatureLoggerService : Service() {
    ...
    private var startId = -1
    ...
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        this.startId = startId
        ...
    }

    private fun completeTemperatureLoggerService() {
        ...
        stopSelf(startId)
    }
}

วิธีแบบนี้ยิ่งผิดไปใหญ่นะ เพราะว่า Service จะถูกสร้างขึ้นมาเพียงตัวเดียวเท่านั้น แล้วเรียกใช้งานซ้ำเรื่อยๆจนกว่าจะถูกทำลายทิ้ง ซึ่งไม่เหมือนกับ Activity

ดังนั้นถ้าเรียกใช้งาน Service หลายๆครั้ง จะทำให้ค่า startId เก็บเฉพาะค่าล่าสุดเท่านั้น แล้วเวลาตัวไหนทำงานเสร็จ ก็เอาค่า startId ล่าสุดไปใช้ในคำสั่ง stopSelf(...) ซะงั้น แทนที่จะเป็น startId ของตัวนั้นๆ

เพื่อไม่ให้ปัญหานี้สร้างความปวดหัวให้กับผู้ที่หลงเข้ามาอ่าน การสร้าง Background Thread แยกข้ึนมาจึงตอบโจทย์และแก้ปัญหานี้ได้

เขียนให้ทำงานบน Background Thread ที่สร้างขึ้นมาใหม่ (แนะนำให้ทำแบบนี้)

โค้ดอาจจะดูรกไปบ้าง แต่ว่าการสร้าง Background Thread จะช่วยให้คำสั่งต่างๆใน Service ทำงานได้อย่างถูกต้อง และ Service ก็ถูกทำลายอย่างถูกต้องด้วย

// TemperatureLoggerService.kt
class TemperatureLoggerService : Service() {
    private lateinit var sensorManager: SensorManager
    private lateinit var temperatureSensor: Sensor

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        temperatureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        runTemperatureLoggerThread(startId)
        return START_NOT_STICKY
    }

    private fun runTemperatureLoggerThread(startId: Int) {
        val thread = HandlerThread("TemperatureLoggerThread$startId", Process.THREAD_PRIORITY_BACKGROUND)
        thread.start()
        val serviceHandler = ServiceHandler(sensorManager, temperatureSensor, thread.looper)
        serviceHandler.sendMessage(serviceHandler.obtainMessage())
    }

    private inner class ServiceHandler(var startId: Int, var sensorManager: SensorManager, var temperatureSensor: Sensor, looper: Looper) : Handler(looper) {
        override fun handleMessage(msg: Message) {
            sensorManager.registerListener(temperatureEventListener, temperatureSensor, SensorManager.SENSOR_DELAY_NORMAL)
        }

        private val temperatureEventListener = object : SensorEventListener {
            override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
            }

            override fun onSensorChanged(event: SensorEvent) {
                val temperature = event.values[0]
                saveTemperatureToDatabase(temperature)
                completeTemperatureLoggerService()
            }
        }

        private fun saveTemperatureToDatabase(temperature: Float) {
            ...
        }

        private fun completeTemperatureLoggerService() {
            sensorManager.unregisterListener(temperatureEventListener, temperatureSensor)
            stopSelf(startId)
        }
    }
}

โดยเจ้าของบล็อกใช้วิธีสร้าง Handler ขึ้นมาเอง แล้วให้เก็บข้อมูลจาก Temperature Sensor ด้วยคำสั่งที่อยู่ในคลาส Handler แล้วให้ Service เป็นแค่ตัวสั่งงานให้ Background Thread เริ่มทำงานเท่านั้นเอง ซึ่งการสร้าง Background Thread ทุกครั้งที่ onStartCommand(...) จะทำให้สามารถส่ง startId ของแต่ละครั้งเข้าไปได้ด้วย จึงทำให้สามารถใช้คำสั่ง stopSelf(...) ด้วย startId ที่ถูกต้องได้

แต่การสร้าง Background Thread ทุกครั้งแบบนี้ก็อาจจะไม่ได้เหมาะกับทุกงานเสมอไป ขึ้นอยู่กับว่าผู้ที่หลงเข้ามาอ่านต้องการออกแบบให้ Service ทำงานแบบไหน เพราะถึงแม้ว่าการสร้าง Background Thread ขึ้นมาใหม่ต่อการเรียกใช้งานครั้งหนึ่งจะช่วยแก้ปัญหาการทำลาย Service ผิดเวลาก็จริง แต่ถ้าเป็นการทำงานที่ทำงานพร้อมๆกันเยอะมาก การสร้าง Background Thread ขึ้นมาเยอะๆก็เป็นหนึ่งสาเหตุที่ทำให้ประสิทธิภาพของแอปฯลดลงได้ ดังนั้นควรจะมั่นใจว่าคำสั่งใน Background Thread จะใช้เวลาทำงานไม่นานจนเกินไป

เพื่อให้เห็นรูปแบบของ Handler ที่หลากหลาย งั้นลองเปลี่ยนโจทย์ใหม่เป็น…

อยากจะสร้าง Service ที่สามารถอัปโหลดรูปภาพหลายๆภาพขึ้น Web Server โดยให้อัปโหลดทีละรูปจนครบ

จะได้โค้ดออกมามีหน้าตาเป็นแบบนี้แทน

// PhotoUploaderService.kt
class PhotoUploaderService : Service() {
    companion object {
        const val EXTRA_PHOTO_PATH = "photo_path"
    }

    private lateinit var serviceHandler: ServiceHandler

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        val thread = HandlerThread("PhotoUploaderThread", Process.THREAD_PRIORITY_BACKGROUND)
        thread.start()
        serviceHandler = ServiceHandler(thread.looper)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val photoPath = intent?.getStringExtra(EXTRA_PHOTO_PATH)
        val message = serviceHandler.obtainMessage()
        message.arg1 = startId
        message.data.putString(EXTRA_PHOTO_PATH, photoPath)
        serviceHandler.sendMessage(message)
        return START_NOT_STICKY
    }

    private inner class ServiceHandler(looper: Looper) : Handler(looper) {
        override fun handleMessage(msg: Message) {
            val startId = msg.arg1
            val photoPath = msg.data.getString(EXTRA_PHOTO_PATH)
            uploadPhotoToWebServer(photoPath)
            stopSelf(startId)
        }

        private fun uploadPhotoToWebServer(photoPath: String) {
            ...
        }
    }
}

ในกรณีนี้จะสร้าง Background Thread ด้วยคลาส Handler มาแค่ตัวเดียวเท่านั้น แล้วใช้วิธีสั่งให้ทำงานด้วยคำสั่ง sendMessage(...) หลายๆครั้งแทน โดยจะได้ลักษณะการทำงานออกมาเป็นแบบนี้

เฮ้ย ทำงานเป็น Queue เหมือนกับ Intent Service เลยนี่หว่า!!!

ถูกต้องครับ เบื้องหลังของ Intent Service ก็คือ Service ที่สร้าง Background Thread ด้วย Handler ในรูปแบบนี้นั่นเอง

ดังนั้นจึงหมายความว่าจริงๆแล้วรูปแบบการทำงานจะเป็นแบบ Parallel หรือ Queue นั้นขึ้นอยู่กับโค้ดที่ผู้ที่หลงเข้ามาอ่านเขียนเข้าไปนั่นเอง โดยพื้นฐานการเรียกใช้งาน Service ใน onStartCommand(…) จะทำงานเป็น Parallel อยู่แล้ว แต่เพราะ Service นั้นทำงานบน Main Thread จึงเสมือนบังคับให้ผู้ที่หลงเข้ามาอ่านต้องสร้าง Background Thread ขึ้นมาเพื่อเรียกใช้งานอีกทีหนึ่ง และ Background Thread ที่ว่านี่แหละ ที่สามารถกำหนดเองได้ว่าอยากจะให้ทำงานเป็นแบบ Parallel หรือ Queue

มาดูคำสั่งเมื่อสร้าง Service จากคลาส Intent Service กันต่อ

จากที่อธิบายไปก่อนหน้านี้ว่าเบื้องหลังของ Intent Service ก็คือ Service ที่สร้าง Background Thread แบบ Queue ไว้ให้แล้ว (แต่ใน Intent Service จะเรียกว่า Worker Thread) นั่นหมายความว่าถ้าอยากจะสร้าง Service เพื่อทำงานบางอย่างที่เป็นแบบ Queue ก็ให้ใช้ Intent Service ดีกว่า เพราะว่าเค้าสร้างมาให้พร้อมใช้งานแล้วนั่นเอง

โดย Intent Service จะมีรูปแบบในการเรียกใช้งานดังนี้

// AwesomeService.kt
class AwesomeService : IntentService("AwesomeService") {

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    override fun onCreate() {
        super.onCreate()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return super.onStartCommand(intent, flags, startId)
    }
    
    override fun onDestroy() {
        ...
    }

    override fun onHandleIntent(intent: Intent?) {
        // Do something
    }
}

สำหรับ Service จะใส่คำสั่งที่ต้องการไว้ใน onStartCommand(...) แต่สำหรับ Intent Service นั้นจะให้ใส่ไว้ใน onHandleIntent(...) แทน

และ Intent Service บังคับว่าใน onCreate() จะต้องเรียก super.onCreate() ทุกครั้งเสมอ และใน onStartCommand(...) ให้ Return ด้วย super.onStartCommand(...) เสมอเช่นกัน เดี๋ยวให้ Intent Service มันจัดการของมันเอง

ดังนั้นถ้าไม่ได้ทำอะไรใน onCreate() หรือ onStartCommand(...) ก็ไม่จำเป็นต้องประกาศก็ได้ แล้วไปเรียกคำสั่งที่ต้องการใน onHandleIntent(...) ได้เลย

// PhotoUploaderService.kt
class PhotoUploaderService : IntentService("PhotoUploaderService") {
    companion object {
        const val EXTRA_PHOTO_PATH = "photo_path"
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    override fun onHandleIntent(intent: Intent?) {
        val photoPath = intent?.getStringExtra(EXTRA_PHOTO_PATH)
        photoPath?.let {
            uploadPhotoToWebServer(photoPath)
        }
    }

    private fun uploadPhotoToWebServer(photoPath: String) {
        ...
    }
}

จะเห็นว่าคำสั่งใน Intent Service จะกระชับและสั้นมาก เมื่อเทียบกับการสร้างเองด้วยคลาส Service เพราะไม่ต้องมานั่งสร้าง Background Thread ด้วย Handler เอง และไม่ต้องมานั่งเรียกคำสั่ง stopSelf(...) เองด้วย

ดังนั้นถ้าอยากสร้าง Service ให้ทำงานเป็นแบบ Queue ก็ใช้ Intent Service เถอะครับ

สรุป

เมื่อใดที่ต้องการสร้าง Service แล้วไม่รู้ว่าจะใช้ Service หรือ Intent Service ก็ให้ดูว่าอยากจะให้มันทำงานแบบไหน แล้วดูต่อว่า Intent Service นั้นตอบโจทย์มั้ย ถ้าไม่ตอบโจทย์ก็ให้ไปเขียนเองด้วยคลาส Service

ซึ่งการสร้าง Service ด้วยคลาส Service ถ้าคำสั่งที่ต้องการนั้นใช้เวลาทำงานนานมาก ก็ไม่แนะนำให้เรียกใน Service โดยตรง เพื่อเลี่ยงปัญหา Block UI โดยใช้วิธีสร้าง Background Thread แยกขึ้นมาแทนแล้วใส่คำสั่งที่ต้องการไว้ในนั้น ส่วน Intent Service ไม่ต้องทำอะไร เพราะมันถูกจัดการให้หมดแล้ว

โดยการสร้างและเรียกใช้งาน Background Thread นั้นก็ขึ้นอยู่กับรูปแบบการทำงานของ Service ว่าอยากจะให้ออกมาเป็นแบบไหน ถ้าอยากให้ทำงานแบบ Parallel ก็ให้สร้าง Background Thread ขึ้นมาใหม่ทุกครั้งที่เรียกใช้งาน แต่ถ้าอยากได้แบบ Queue ก็ไปใช้ Intent Service เถอะนะ

ในบทความนี้พูดถึงการสร้าง Service กับ Intent Service โดยยกตัวอย่างด้วยการสร้าง Service เป็นแบบ Background Service เพราะอยากให้โค้ดตัวอย่างไม่เยอะจนเกินไป แต่ในความเป็นจริงนั้น ไม่ว่าจะเป็น Service หรือ Intent Service ก็สามารถนำไปสร้างเป็น Background Service, Foreground Service และ Bound Service ได้ทั้งหมดเลย อยู่ที่ว่าอยากจะได้ Service แบบไหนเท่านั้นเอง