Service in Android — Lifecycle ของ Service

จากบทความในตอนแรกที่ได้เกริ่นถึงเรื่องพื้นฐานของ Service ที่นักพัฒนาแอนดรอยด์จะต้องรู้กันไปแล้ว คราวนี้เจ้าของบล็อกจะมาพูดถึงเรื่องของ Lifecycle ของ Service กันต่อฮะ เพราะถือว่าเป็นอีกเรื่องหนึ่งที่จะพลาดไปไม่ได้ถ้าต้องการเรียกใช้งาน Service

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

Service ก็มี Lifecycle นะจ๊ะ

อย่างที่บอกไปในบทความก่อนหน้านี้ว่า Service เป็นหนึ่งใน Component พื้นฐานของแอปฯแอนดรอยด์ และมี Lifecycle เช่นเดียวกับ Activity แต่ว่าจะมีรูปแบบที่ง่ายกว่ามาก

ภาพนี้แสดง Lifecycle ของ Service ก็จริง แต่จะเห็นว่ามันมีการแบ่งระหว่าง Started Service กับ Bound Service ออกจากกัน เพราะทั้ง 2 แบบนี้มี Lifecycle ที่แตกต่างกันเล็กน้อย โดยจะเห็นว่าที่เหมือนกันก็คือ onCreate() กับ onDestroy()

Lifecycle ของ Started Service

onCreate()

ทำงานก็ต่อเมื่อ Service ถูกสร้างขึ้นมาใหม่เท่านั้น เหมาะกับการ Setup การทำงานต่างๆที่จำเป็นต้องใช้ใน Service ตัวนั้นๆ

เมื่อ Service ถูกสร้างขึ้นมาและทำงานแล้ว เมื่อ Component เรียกใช้งาน Service ตัวนี้ในระหว่างนั้น Method นี้จะไม่ถูกเรียก (เพราะถูกสร้างขึ้นมาแล้ว) แต่ถ้า Service นี้ทำงานจนเสร็จและถูกทำลายลง เมื่อมีการเรียกใช้งาน Service ใหม่อีกครั้ง Method นี้ก็จะทำงาน เพราะว่าต้องสร้าง Service ขึ้นมาใหม่

onStartCommand(intent: Intent?, flags: Int, startId: Int) : Int

คำสั่งนี้จะทำงานทุกครั้งเมื่อ Component เรียกใช้งาน Service ด้วยคำสั่ง startService(intent: Intent) อยากให้ Service ทำงานอะไรก็จะเรียกคำสั่งในนี้ทันที (แต่ถ้าจะให้ดีที่สุดคือเริ่มจากการสร้าง Background Thread ที่นี่ครับ)

โดย Method ตัวนี้จะส่งค่าเข้ามา 3 ตัว ดังนี้

  • intent เป็น Intent ที่ส่งเข้ามาตอนเรียกคำสั่ง startService(intent: Intent) นั่นเอง ดังนั้น Component อยากจะส่งข้อมูลอะไรเข้ามาให้ Service ในตอนที่จะเรียกใช้งาน ก็จะส่งเข้ามาผ่าน Intent ตัวนี้
  • flags เป็นค่า Integer ที่จะบอกว่า onStartCommand(...) ทำงานเพราะอะไร ซึ่งปกติจะมีค่าเป็น 0 อยู่แล้ว มีสำหรับตอนที่ Service ถูกระบบแอนดรอยด์คืน Memory ก่อนที่ onStartCommand(...) จะทำงานเสร็จ จะได้กลับมาทำงานใหม่ต่อจากของเดิมแล้วรู้ได้ว่าก่อนหน้านี้หยุดทำงานไปในตอนไหน
  • startId เป็นค่า Integer ที่เอาไว้ระบุ ID ในการเรียกใช้งาน Service ตัวนั้นๆ เพราะโดยปกติแล้วถ้า Service ถูกสร้างขึ้นมา ค่า startId ในตอนแรกสุดจะมีค่าเป็น 1 แต่ระหว่างที่ Service ทำงานอยู่นั้น ถ้าเกิดมี Component ตัวไหนเรียกใช้งาน Service ขึ้นมา ก็จะได้ค่า startId เป็น 2, 3, 4 ไปเรื่อยๆ จะได้แยก onStartCommand(...) ในแต่ละครั้งได้ว่ามาจากการเรียกใช้งานคนละครั้งกันหรือป่าว และเมื่อ Service ถูกทำลายทิ้ง แล้วสร้างขึ้นมาใหม่ ค่า startId ก็จะเริ่มนับจาก 1 ใหม่อีกครั้ง

การเรียกใช้งาน Service ส่วนใหญ่จะส่งค่าผ่าน Intent กัน เพราะว่า Component ต้องส่งข้อมูลบางอย่าง เพื่อให้ Service ทำงานตามต้องการ ดังนั้นอยากจะส่งอะไรก็ส่งมาเลยจ้า

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    ...
    fun startAwesomeService() {
        val intent = Intent(this, AwesomeService::class.java)
        intent.putExtra("user_name", "Akexorcist")
        intent.putExtra("level", 99)
        startService(intent)
    }
}


// AwesomeService.kt
class AwesomeService : Service() {
    ...
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        
        val userName = intent?.getStringExtra("user_name")
        val level = intent?.getIntExtra("level", 0) ?: 0

        // userName = "Akexorcist"
        // level = 99

        return ...
    }
}

ทีนี้จะเห็นว่า onStartCommand(...) นั้นเป็น Method ที่ต้องส่งค่า Integer ออกมาด้วย ซึ่งค่าที่ว่าเนี่ยสำคัญมาก เป็นตัวกำหนดการทำงานของ Service ว่าจะให้ทำงานยังไง

เพราะเมื่อใดที่ Memory ในเครื่องไม่เพียงพอต่อการใช้งานแอปฯที่เปิดอยู่ ระบบแอนดรอยด์มีโอกาสที่จะสั่งหยุด Service แล้วทำลายลงชั่วคราวเพื่อคืน Memory ให้กับตัวเครื่อง ซึ่งค่า Integer ที่ว่านี่แหละ ที่จะเป็นตัวกำหนดว่าถ้า Memory มีเหลือมากพอแล้ว จะให้ Service กลับมาทำงานต่อหรือป่าว โดยกำหนดด้วย Flag ซึ่งค่าที่นิยมใช้กำหนดกันจะมีอยู่ 2 ตัวดังนี้

  • START_NOT_STICKY เป็นค่าที่กำหนดให้ Service หยุดทำงานไปเลย ไม่ต้องกลับมาทำงานใหม่
  • START_STICKY เป็นค่าที่กำหนดให้ Service จะกลับมาทำงานใหม่อีกครั้งเมื่อ Memory เหลือมากพอ

จริงๆแล้วจะมีค่าอื่นด้วย แต่เจ้าของบล็อกขอข้ามเรื่องนี้ไปก่อน ไว้เดี๋ยวค่อยอธิบายในบทความแทนนะ ว่าแต่ละตัวมีผลต่อการทำงานของ Service ยังไง

สมมติว่า Service ของเจ้าของบล็อกเป็น Service ที่สามารถหยุดทำงานได้เลย ไม่จำเป็นต้องกลับมาทำงานต่อจากของเดิม (เช่น Sync ข้อมูลจาก Web Service มาเก็บไว้ใน Database ของเครื่อง) ก็จะส่งค่ากลับไปแบบนี้

// AwesomeService.kt
class AwesomeService : Service() {
    ...
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        ...
        return START_STICKY
    }
}

onDestroy()

ทำงานเมื่อ Service ถูกทำลายทิ้งด้วยคำสั่ง stopService(...) หรือ stopSelf() ถือว่าเป็น Override Method ตัวสุดท้ายที่ทำงานก่อนที่ Service จะถูกทำลายลง เหมาะกับการใส่คำสั่งเคลียร์ค่าต่างๆที่เรียกใช้ใน Service เพื่อคืน Resource ให้กับระบบ

Lifecycle ของ Bound Service

onCreate()

ทำงานก็ต่อเมื่อ Service ถูกสร้างขึ้นมาใหม่เท่านั้น เหมาะกับการ Setup การทำงานต่างๆที่จำเป็นต้องใช้ใน Service ตัวนั้นๆ

เมื่อ Service ถูกสร้างขึ้นมาและทำงานแล้ว เมื่อ Component เรียกใช้งาน Service ตัวนี้ในระหว่างนั้น Method นี้จะไม่ถูกเรียก (เพราะถูกสร้างขึ้นมาแล้ว) แต่ถ้า Service นี้ทำงานจนเสร็จและถูกทำลายลง เมื่อมีการเรียกใช้งาน Service ใหม่อีกครั้ง Method นี้ก็จะทำงาน เพราะว่าต้องสร้าง Service ขึ้นมาใหม่

onBind(intent: Intent?) : IBinder?

คำสั่งนี้จะทำงานทุกครั้งเมื่อ Component เรียกใช้งาน Service ด้วยคำสั่ง bindService(intent: Intent, connection: ServiceConnection, flags: Int) และสิ่งที่ต่างจาก onStartCommand(...) ของ Started Service ก็คือถ้า Component ตัวไหนเคย Bind กับ Service แล้ว แล้วเรียกคำสั่ง binsService(...) อีกครั้ง onBind(...) ก็จะไม่ทำงานซ้ำ เพราะว่าเคย Bind เรียบร้อยแล้ว

โดย Method ตัวนี้จะส่งค่าเข้ามาตัวเดียวคือ Intent

  • intent เป็น Intent ที่ส่งเข้ามาตอนเรียกคำสั่ง bindService(...) ดังนั้น Component อยากจะส่งข้อมูลอะไรเข้ามาให้ Service ในตอนที่ Bind Service ก็จะส่งเข้ามาผ่าน Intent ตัวนี้
// MainActivity.kt
class MainActivity : AppCompatActivity() {
    ..
    private fun bindAwesomeService() {
        val intent = Intent(this, AwesomeService::class.java)
        intent.putExtra("user_name", "Akexorcist")
        intent.putExtra("level", 99)
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    private val serviceConnection = ...
}


// AwesomeService.kt
class AwesomeService : Service() {
    ...
    override fun onBind(intent: Intent?): IBinder {

        val userName = intent?.getStringExtra("user_name")
        val level = intent?.getIntExtra("level", 0) ?: 0

        // userName = "Akexorcist"
        // level = 99

        return ...
    }
}

โดย onBind(...) เป็น Method ที่จะต้องส่งคลาส IBinder ออกมาด้วย ซึ่งค่าที่ว่าเนี่ยเป็นตัวกลางในการสื่อสารระหว่าง Component กับ Service ซึ่งจะต้องส่งกลับไปด้วย

// AwesomeService.kt
class AwesomeService : Service() {
    private val mBinder = LocalBinder()

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

    inner class LocalBinder : Binder() {
        internal
        val service: BindService
            get() = this@BindService
    }
}

ส่วนการสร้างและการส่งข้อมูลผ่านคลาส IBinder เดี๋ยวไว้อธิบายทีหลังตอนที่พูดถึงเรื่อง Bound Service ละกันเนอะ

onUnbind(intent: Intent?) : Boolean

ทำงานเมื่อ Service ถูก Unbind ด้วยคำสั่ง unbindService(...) จึงสามารถใช้คำสั่งต่างๆที่ต้องการให้ตอนที่ Unbind ได้ โดย Override Method ตัวนี้จะต้องส่ง Boolean กลับไปด้วยเพื่อบอกว่าจะให้ Service สามารถ Bind ใหม่ (Rebind) ได้หรือไม่

// AwesomeService.kt
class AwesomeService : Service() {
    ...
    override fun onUnbind(intent: Intent?): Boolean {
        ...
        return true
    }
}

onDestroy()

ทำงานเมื่อ Service ถูกทำลายทิ้งด้วยคำสั่ง unbindService(...) ถือว่าเป็น Override Method ตัวสุดท้ายที่ทำงานก่อนที่ Service จะถูกทำลายลง เหมาะกับการใส่คำสั่งเคลียร์ค่าต่างๆที่เรียกใช้ใน Service เพื่อคืน Resource ให้กับระบบ

Started Service มีโอกาสถูกทำลายเพื่อคืน Memory ได้นะจ๊ะ

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

นั่นหมายความว่า Service ที่สั่งให้ทำงานไว้ ก็อาจจะถูกทำลายชั่วคราวได้ และเมื่อเครื่องมี Memory เหลือเฟือแล้วก็จะสั่งให้ Service กลับมาทำงานต่อได้ (ขึ้นอยู่กับว่ากำหนดไว้ในโค้ดยังไง) ซึ่งการโดนปิด Service เพื่อคืน Memory นั้นจะเกิดขึ้นกับ Started Service เท่านั้น เพราะว่า Bound Service เป็น Service ที่ทำงานตาม Lifecycle ของ Component อยู่แล้ว ก็เลยรอดตัวไป

โดย Background Service จะเป็น Service ที่มีโอกาสถูกทำลายง่ายที่สุด และยิ่งทำงานนานมากๆก็จะยิ่งมีโอกาสถูกทำลายทิ้งได้ง่ายขึ้น

สำหรับ Foreground Service นั้นถึงแม้ว่าจะเป็น Started Service เหมือนกับ Background Service ก็ตาม แต่เนื่องจากตัวมันเป็น Service ที่ผูก UI ด้วย Notification ทำให้ระบบแอนดรอยด์นับว่า Foreground Service มีความสำคัญกว่า Background Service มาก จนแทบจะไม่ถูกทำลายเลย (เว้นแต่ว่าสุดๆจริงๆ)

สรุป

อย่างที่รู้กันว่า Lifecycle ของ Component ต่างๆบนแอนดรอยด์นั้นสำคัญมาก ซึ่ง Lifecycle ของ Service ก็เช่นกัน นักพัฒนาต้องเข้าใจลักษณะการทำงานของ Service แต่ละรูปแบบก่อน เพื่อให้สามารถเรียกใช้งานได้อย่างถูกต้องนั่นเอง และเมื่อดู Lifecycle ของ Service แล้วก็จะพบว่าเรียบง่ายกว่า Activity มาก

ซึ่งในบทความต่อไปก็จะมาลองสร้าง Service แต่ละแบบกันดูครับ