ในบทความนี้จะมาพูดถึงเกี่ยวกับ Bound Service ซึ่งเป็นหนึ่งในรูปแบบการทำงานของ Service ที่นักพัฒนาควรรู้จัก เพราะในการทำงานจริงๆนั้นเมื่อมีการเรียกใช้งาน Service เพื่อทำงานไปพร้อมๆกับ Component มีการส่งข้อมูลไปมาด้วย เจ้า Bound Service นี่แหละที่จะตอบโจทย์การทำงานในรูปแบบนี้
บทความที่เกี่ยวข้อง
• [ตอนที่ 1] พื้นฐานของ Service• [ตอนที่ 2] Lifecycle ของ Service
• [ตอนที่ 3] เจาะลึกการเรียกใช้งาน Service และ Intent Service
• [ตอนที่ 4] มาสร้าง Foreground Service กันเถอะ
• [ตอนที่ 5] มาสร้าง Bound Service กันเถอะ
เพราะว่า Bound Service นั้นเป็น Service ที่สร้างขึ้นมาเพื่อติดต่อกับ Component ได้ตลอดเวลา ซึ่งต่างจาก Started Service ที่ทำงานแล้วจบภายในตัวเองที่ไม่ได้ต้องการส่งข้อมูลอะไรให้ Component เลย โดย Bound Service สามารถสร้างเป็น Background Service หรือ Foreground Service ก็ได้ จะสร้างจากคลาส Service หรือ Intent Service ก็ได้ ขึ้นอยู่กับความต้องการของผู้ที่หลงเข้ามาอ่านเลยจ้า
สร้าง Bound Service ยังไง?
ในบทความนี้เจ้าของบล็อกขอยกตัวอย่างการสร้าง Bound Service ให้เป็นแบบ Background Service โดยใช้คลาส Service นะ
หัวใจสำคัญของการสร้าง Bound Service จะอยู่ที่ onBind(...) เพราะว่าเวลาสร้าง Started Service จะกำหนดให้ Method นี้ส่งค่าเป็น Null ตลอด แต่ในคราวนี้เจ้าของบล็อกจะต้องส่งค่าบางอย่างออกไปแทน
สิ่งที่ต้องทำก็คือสร้าง Binder เป็น Inner Class ไว้ข้างในแล้วสร้าง Binder ตัวนั้นไว้ที่ Global และใน onBind(...) ก็ส่ง Binder ตัวนั้นไป
และการประกาศ Bound Service ใน Android Manifest ก็ทำเหมือนกับ Service ทั่วไปเลย
ส่วนการทำงานอื่นๆใน Service ตัวนี้เดี๋ยวค่อยพูดถึงทีหลัง ขอโฟกัสที่คำสั่งตอนเรียกใช้งาน Bound Service ก่อน เพราะว่าเวลาเรียกใช้งานเนี่ย ไม่ได้ใช้คำสั่ง startService(...) เหมือนเดิมแล้วนะ แต่ว่าจะต้องใช้คำสั่ง bindService(...) แทน
เมื่อเรียกคำสั่ง bindService(...) ก็จะพบว่าคำสั่งนี้ต้องใช้ ServiceConnection ด้วย ซึ่งเป็น Interface ที่คอยดูการเชื่อมต่อกันระหว่าง Component กับ Bound Service โดยจะบอกให้รู้เมื่อการเชื่อมต่อกับ Service หรือหยุดเชื่อมต่อกับ Service
ยังไม่จบนะ เพราะว่าคำสั่ง bindService(...) จะต้องกำหนด Flag เป็น Integer อีกตัวด้วย เจ้าของบล็อกก็เลยไปนั่งหาข้อมูลเพิ่มเติมแล้วก็พบว่ามันมีทั้งหมดดังนี้
• BIND_ADJUST_WITH_ACTIVITY
• BIND_ALLOW_OOM_MANAGEMENT
• BIND_AUTO_CREATE
• BIND_DEBUG_UNBIND
• BIND_EXTERNAL_SERVICE
• BIND_IMPORTANT
• BIND_NOT_FOREGROUND
• BIND_WAIVE_PRIORITY
เยอะชะมัด... เยอะจนขี้เกียจอธิบายแต่ละตัวแฮะ เพราะงั้นขอข้ามไปแบบดื้อๆเลยละกัน ซึ่งจริงๆแล้ว Flag แต่ละตัวจะเป็นตัวกำหนดรูปแบบการทำงานของ Bound Service ครับ แต่การใช้งานทั่วไปจะใช้ BIND_AUTO_CREATE เป็นหลัก
ดังนั้นเวลาที่ Component เรียกใช้งาน Bound Service ก็จะออกมาในรูปแบบนี้
ให้ดูตรงที่ onServiceConnected(...) จะเห็นว่ามีการดึง AwesomeBoundService มาเก็บไว้ใน Global เพื่อให้ Activity สามารถเรียกคำสั่งต่างๆใน Bound Service ผ่านตัวแปร awesomeBoundService ได้เลย
และเมื่อใช้งาน Bound Service แล้ว ก็ควรสั่งหยุดทำงานด้วยนะ โดยใช้คำสั่ง unbindService(serviceConnection: ServiceConnection)
ถ้าจะให้เรียกใช้งานแบบง่ายที่สุดก็คือ Bind ตอน onStart() และ Unbind ตอน onStop() หรือจะเปลี่ยนเป็นที่อื่นก็ได้ตามใจชอบ แต่ควรจะ Bind ตอนที่ Component พร้อมทำงานหรือทำงานอยู่ และ Unbind ตอนที่หยุดทำงาน
เวลา Component อยากจะเรียกใช้งาน Bound Service ก็เรียกได้ตรงๆเลย
ซึ่งอันนี้คือตัวอย่างเบื้องต้นของการที่ทำให้ Bound Service สามารถติดต่อกับ Component ได้ครับ แต่วิธีแบบนี้ไม่ค่อยเหมาะไปใช้งานจริงซักเท่าไร เพราะ Bound Service ถูกออกแบบมาให้ทำงานบางอย่างที่ควรจะอยู่ใน Background Thread แต่การเรียกใช้งานตรงๆแบบนี้ก็ทำให้คำสั่งใน Bound Service ทำงานอยู่บน Main Thread อยู่ดี
และการทำแบบนี้จะทำให้ Bound Service ถูกเรียกใช้งานได้เฉพาะ Component ที่อยู่ในแอปฯตัวเดียวกันเท่านั้น (อยู่ใน Process เดียวกัน) ทั้งๆที่สามารถสร้าง Bound Service เพื่อเป็น Service กลางให้แอปฯตัวอื่นๆเรียกใช้งานได้ (ลองนึกภาพเวลาเรียกใช้งาน Location API ของ Google Play Services ดูสิ)
ดังนั้นมาทำให้มันถูกต้องกว่านี้กันเถอะ
คลาส Messenger ถูกออกแบบมาเพื่อเป็นตัวกลางในการสื่อสารข้อมูล ซึ่งความเท่ของมันคือมันสามารถใช้สื่อสารระหว่างแอปฯหรือ Process กันได้ด้วย จึงทำให้ Bound Service ไม่ได้ถูกจำกัดแค่ว่าจะต้องถูกเรียกใช้งานจากภายในแอปฯเท่านั้น แต่สามารถเรียกใช้งานจากแอปฯตัวอื่นๆได้อีกด้วย โดยใช้ Messenger นี่แหละ
เบื้องหลังของ Messenger ก็คือ Wrapper ที่ไปครอบคลาส Handler หรือ IBinder อีกทีหนึ่ง โดย Handler เป็นเสมือนตัวกลางที่ใช้ในการส่งข้อมูลกันไปมา ส่วน IBinder เป็นตัวในการเชื่อมต่อกันระหว่าง Component กับ Bound Service (ดังนั้น Messenger ถือว่าเป็นตัวกลางที่รวมตัวส่งข้อมูลและตัวเชื่อมต่อไว้ในตัวเดียวกัน และทำให้ข้อมูลสามารถส่งข้ามไปมาระหว่างแอปฯหรือ Process ด้วย)
ดังนั้นเวลาที่ Component กับ Bound Service เชื่อมต่อกันผ่าน Messenger ก็จะมีลักษณะหน้าตาแบบนี้ครับ
เมื่อกลับมาดูที่โค้ดใน Bound Service ที่เจ้าของบล็อกทำไว้ในตอนแรก สิ่งที่ต้องทำใหม่ก็คือทำให้ Bound Service ใช้ Messenger ซะ
ลาก่อน LocalBinder เพราะนายไม่ดีพอที่เราจะใช้งาน...
Messenger จะถูกสร้างขึ้นใน Bound Service ที่สร้างขึ้นมาจาก Handler ที่ถูกเตรียมไว้เพื่อรับข้อมูลที่ Component จะส่งเข้ามา โดยข้อมูลจะส่งเข้ามาที่ handleMessage(...) ส่วนใน onBind(...) ก็จะส่ง IBinder ของ Messenger แทน
Message ประกอบไปด้วยข้อมูลต่างๆดังนี้
• handle: Handler
• what: Int
• arg1: Int
• arg2: Int
• obj: Object
• data: Bundle
• target: Handler
• replyTo: Messenger
ผู้ที่หลงเข้ามาอ่านสามารถยัดข้อมูลลงใน what, arg1, arg2, data และ obj ได้ตามต้องการ โดยที่ Message ถูกออกแบบมาให้สร้างด้วยคำสั่ง obtain(...) เท่านั้น
หรือจะสร้าง Message ขึ้นมาเปล่าๆแล้วค่อยกำหนดค่าทีหลังก็ได้เช่นกัน
และเนื่องจาก Message ถูกออกแบบมาเพื่อใช้ส่งข้อมูลระหว่าง Component ในแอนดรอยด์ ผู้ที่หลงเข้ามาอ่านจึงสามารถส่งข้อมูลที่อยู่ในรูปแบบ Bundle ได้เลย โดยแนบไปในชื่อ data นั่นแหละ ซึ่งเจ้าของบล็อกแนะนำให้ส่งข้อมูลผ่าน data มากกว่า obj นะ เพราะ Bundle ถูกควบคุมด้วย Key ซึ่งจัดการที่ปลายทางได้ง่ายกว่าการส่งเป็น Object ที่ต้องมานั่ง Cast Class ซึ่งไม่เหมาะกับข้อมูลที่เป็น Model Class ที่สร้างขึ้นมาเอง (ถ้าเรียกข้ามแอปฯ)
จะเห็นว่าข้อมูลใน Message ไม่จำเป็นต้องกำหนดให้ครบทั้งหมดก็ได้ อยู่ที่ว่าจะส่งอะไรไปบ้าง
เมื่อใดก็ตามที่ Component อยากจะสั่งให้ Bound Service ทำงาน ก็ให้สร้าง Message ขึ้นมาแล้วส่งข้อมูลด้วย Messenger ที่เก็บไว้ใน Global นั่นเอง
บ่อยครั้งที่ Service สามารถสั่งให้ทำอะไรได้มากกว่าหนึ่งอย่าง ดังนั้นจึงนิยมแนบค่าไว้ใน what เพื่อเป็นตัวกำหนดว่าจะให้ทำอะไร เพื่อที่ Service จะได้เช็คค่าดังกล่าวแล้วเรียกคำสั่งต่างๆตามที่กำหนดไว้
ถ้ามีข้อมูลส่งมาด้วย ก็ต้องรู้ด้วยว่าควรดึงข้อมูลจากค่าไหนไปใช้งาน ซึ่งคนกำหนดควรจะเป็นฝั่ง Service นะ แล้ว Component ส่งเข้ามาให้ตรงตามที่กำหนดไว้
จะเห็นว่าในคำสั่ง obtain(...) ค่า Handler ซึ่งเป็นการกำหนด Handler ปลายทางที่จะส่งข้อมูลไปให้ แต่ที่เจ้าของบล็อกกำหนดเป็น Null ไว้ ก็เพราะว่า Message ถูกเรียกผ่าน Messenger อยู่แล้ว เดี๋ยว Messenger จะกำหนด Handler ให้เอง
Concept ของการใช้ Messenger ก็มีประมาณนี้แหละ ไม่ยากเกินไปเนอะ?
ดังนั้นการจะสื่อสารแบบ Two-way จะต้องให้ฝั่งใดฝั่งหนึ่งส่ง Handler มาให้ก่อน แล้วอีกฝั่งค่อยส่งกลับไปให้ตอนส่งข้อมูลแทน โดยแนบค่า replyTo มาให้ด้วย จะได้รู้ว่าควรจะส่งข้อมูลกลับไปให้ Component ตัวไหน
ดังนั้นถ้าอยากให้การส่งข้อมูลเป็นแบบ Two-way ก็จะเป็นแบบนี้แทน
เวลา Service จะส่งข้อมูลกลับให้ดึงค่า replyTo จาก Message ด้วย ซึ่งค่าดังกล่าวอยู่ในรูปของคลาส Messenger ที่เป็นของฝั่ง Component ที่เรียกใช้งาน Service ดังนั้นเมื่อ Service ทำคำสั่งจนเสร็จแล้วอยากจะส่งข้อมูลกลับไปให้ ก็ให้เรียกคำสั่ง send(...) จาก replyTo ได้เลย
จากตัวอย่างเจ้าของบล็อกต้องการส่งพิกัดที่เป็นค่า Double ไป แต่ว่าไม่สามารถแนบผ่าน arg1 หรือ arg2 ได้ เพราะ 2 ตัวนั้นเป็น Integer ก็เลยแนบข้อมูลพิกัดผ่าน Bundle แทน
และซึ่งค่า replyTo เนี่ย ฝั่ง Component ต้องกำหนดเองด้วยนะ ไม่ใช่ว่าอยู่ดีๆก็มีให้เลย ดังนั้นตอนที่ Component สร้าง Message ขึ้นมาเพื่อส่งไปให้ Service ก็ต้องกำหนดค่าดังกล่าวด้วย โดยสร้าง Messenger ขึ้นมาจาก Handler ที่อยากจะให้ส่งข้อมูลกลับไปได้เลย
จากโค้ดตัวอย่างนี้ การทำงานแบบ Two-way ก็จะมีลักษณแบบนี้
อย่างแรกเลยก็คือต้องกำหนด Bound Service เพื่อให้สามารถเรียกใช้งานจากแอปฯภายนอกได้โดยกำหนดค่า Export ให้เป็น True
ส่วนการเรียกใช้งาน Bound Service จากแอปฯอื่นจะใช้วิธีกำหนดจาก Component Name แทน เพราะไม่สามารถอ้างอิงถึงคลาสโดยตรงได้
เพราะว่าอ้างอิงค่าต่างๆจาก Bound Service ไม่ได้ เนื่องจากเป็นคนละแอปฯกัน ดังนั้นจึงต้องสร้าง Constant เพื่อเก็บค่าต่างๆที่ต้องใช้ใน Bound Service ไว้ที่ Component ตัวนี้ด้วย
เรียบร้อย~ ส่วนใหญ่นั้นยังคงเหมือนเดิม แค่ว่าค่าตัวแปรต่างๆที่อ้างอิงโดยตรงไม่ได้ก็ใช้วิธีกำหนดค่าขึ้นมาเองซะ (ต้องตรงกับใน Bound Service ด้วยนะ เดี๋ยวคุยกันไม่รู้เรื่อง)
โดยในบทความนี้ได้ยกตัวอย่าง Bound Service ในรูปแบบของ Background Service ซึ่งในการทำงานจริงๆก็ควรใช้ JobScheduler ซะมากกว่า (ไม่งั้นเดี๋ยวจะซวยใน Android 8.0 Oreo) แต่ก็สามารถสร้าง Bound Service ให้ทำงานเป็น Foreground Service ได้เหมือนกันนะ เพียงแค่ใช้คำสั่ง startForeground(...) ใน Bound Service แล้วผูกเข้ากับ Notification เท่านั้นเอง
อย่าลืมว่า Service แต่ละแบบนั้นมีจุดประสงค์ในการใช้งานแตกต่างกันไป ดังนั้นถ้าต้องการจะเขียน Service ซักตัวเพื่อใช้งานในแอปฯ ก็ควรพิจารณาก่อนว่ารูปแบบการทำงานที่ต้องการนั้นควรจะเขียน Service ในรูปแบบไหน
// AwesomeBoundService.kt
import android.app.Service
import android.content.Intent
import android.os.IBinder
class AwesomeBoundService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return ...
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) {
return ...
}
override fun onCreate() {
...
}
override fun onDestroy() {
...
}
}
หัวใจสำคัญของการสร้าง Bound Service จะอยู่ที่ onBind(...) เพราะว่าเวลาสร้าง Started Service จะกำหนดให้ Method นี้ส่งค่าเป็น Null ตลอด แต่ในคราวนี้เจ้าของบล็อกจะต้องส่งค่าบางอย่างออกไปแทน
สิ่งที่ต้องทำก็คือสร้าง Binder เป็น Inner Class ไว้ข้างในแล้วสร้าง Binder ตัวนั้นไว้ที่ Global และใน onBind(...) ก็ส่ง Binder ตัวนั้นไป
// AwesomeBoundService.kt
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
class AwesomeBoundService : Service() {
private val binder = LocalBinder()
override fun onBind(intent: Intent?): IBinder? {
return binder
}
...
private inner class LocalBinder : Binder() {
fun getService(): AwesomeBoundService = this@AwesomeBoundService
}
}
และการประกาศ Bound Service ใน Android Manifest ก็ทำเหมือนกับ Service ทั่วไปเลย
// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
...
<application ...>
...
<service
android:name="AwesomeBoundService"
android:enabled="true" />
</application>
</manifest>
ส่วนการทำงานอื่นๆใน Service ตัวนี้เดี๋ยวค่อยพูดถึงทีหลัง ขอโฟกัสที่คำสั่งตอนเรียกใช้งาน Bound Service ก่อน เพราะว่าเวลาเรียกใช้งานเนี่ย ไม่ได้ใช้คำสั่ง startService(...) เหมือนเดิมแล้วนะ แต่ว่าจะต้องใช้คำสั่ง bindService(...) แทน
bindService(intent: Intent, serviceConnection: ServiceConnection, flags: Int)
เมื่อเรียกคำสั่ง bindService(...) ก็จะพบว่าคำสั่งนี้ต้องใช้ ServiceConnection ด้วย ซึ่งเป็น Interface ที่คอยดูการเชื่อมต่อกันระหว่าง Component กับ Bound Service โดยจะบอกให้รู้เมื่อการเชื่อมต่อกับ Service หรือหยุดเชื่อมต่อกับ Service
private val serviceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
// Do something when service disconnected
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// Do something when service connected
}
}
ยังไม่จบนะ เพราะว่าคำสั่ง bindService(...) จะต้องกำหนด Flag เป็น Integer อีกตัวด้วย เจ้าของบล็อกก็เลยไปนั่งหาข้อมูลเพิ่มเติมแล้วก็พบว่ามันมีทั้งหมดดังนี้
• BIND_ADJUST_WITH_ACTIVITY
• BIND_ALLOW_OOM_MANAGEMENT
• BIND_AUTO_CREATE
• BIND_DEBUG_UNBIND
• BIND_EXTERNAL_SERVICE
• BIND_IMPORTANT
• BIND_NOT_FOREGROUND
• BIND_WAIVE_PRIORITY
เยอะชะมัด... เยอะจนขี้เกียจอธิบายแต่ละตัวแฮะ เพราะงั้นขอข้ามไปแบบดื้อๆเลยละกัน ซึ่งจริงๆแล้ว Flag แต่ละตัวจะเป็นตัวกำหนดรูปแบบการทำงานของ Bound Service ครับ แต่การใช้งานทั่วไปจะใช้ BIND_AUTO_CREATE เป็นหลัก
ดังนั้นเวลาที่ Component เรียกใช้งาน Bound Service ก็จะออกมาในรูปแบบนี้
// MainActivity.kt
class MainActivity : AppCompatActivity() {
...
lateinit var awesomeBoundService: AwesomeBoundService
...
private fun bindAwesomeBoundService() {
val intent = Intent(this, AwesomeBoundService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
if (service is AwesomeBoundService.LocalBinder) {
this@MainActivity.awesomeBoundService = service.getService()
}
}
}
}
ให้ดูตรงที่ onServiceConnected(...) จะเห็นว่ามีการดึง AwesomeBoundService มาเก็บไว้ใน Global เพื่อให้ Activity สามารถเรียกคำสั่งต่างๆใน Bound Service ผ่านตัวแปร awesomeBoundService ได้เลย
และเมื่อใช้งาน Bound Service แล้ว ก็ควรสั่งหยุดทำงานด้วยนะ โดยใช้คำสั่ง unbindService(serviceConnection: ServiceConnection)
ถ้าจะให้เรียกใช้งานแบบง่ายที่สุดก็คือ Bind ตอน onStart() และ Unbind ตอน onStop() หรือจะเปลี่ยนเป็นที่อื่นก็ได้ตามใจชอบ แต่ควรจะ Bind ตอนที่ Component พร้อมทำงานหรือทำงานอยู่ และ Unbind ตอนที่หยุดทำงาน
// MainActivity.kt
class MainActivity : AppCompatActivity() {
...
lateinit var service: AwesomeBoundService
...
override fun onStart() {
super.onStart()
val intent = Intent(this, AwesomeBoundService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
super.onStop()
unbindService(serviceConnection)
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
if (service is AwesomeBoundService.LocalBinder) {
this@MainActivity.service = service.getService()
}
}
}
}
เวลา Component อยากจะเรียกใช้งาน Bound Service ก็เรียกได้ตรงๆเลย
// MainActivity.kt
class MainActivity : AppCompatActivity() {
...
lateinit var awesomeBoundService: AwesomeBoundService
...
fun doSomething() {
awesomeBoundService.doSomething()
}
}
ซึ่งอันนี้คือตัวอย่างเบื้องต้นของการที่ทำให้ Bound Service สามารถติดต่อกับ Component ได้ครับ แต่วิธีแบบนี้ไม่ค่อยเหมาะไปใช้งานจริงซักเท่าไร เพราะ Bound Service ถูกออกแบบมาให้ทำงานบางอย่างที่ควรจะอยู่ใน Background Thread แต่การเรียกใช้งานตรงๆแบบนี้ก็ทำให้คำสั่งใน Bound Service ทำงานอยู่บน Main Thread อยู่ดี
และการทำแบบนี้จะทำให้ Bound Service ถูกเรียกใช้งานได้เฉพาะ Component ที่อยู่ในแอปฯตัวเดียวกันเท่านั้น (อยู่ใน Process เดียวกัน) ทั้งๆที่สามารถสร้าง Bound Service เพื่อเป็น Service กลางให้แอปฯตัวอื่นๆเรียกใช้งานได้ (ลองนึกภาพเวลาเรียกใช้งาน Location API ของ Google Play Services ดูสิ)
ดังนั้นมาทำให้มันถูกต้องกว่านี้กันเถอะ
ใช้ Messenger เป็นตัวกลางในการสื่อสารระหว่าง Component กับ Bound Service
ไม่ได้หมายถึงแอปฯ Messenger นะ อย่าเข้าใจผิด ฮา
เบื้องหลังของ Messenger ก็คือ Wrapper ที่ไปครอบคลาส Handler หรือ IBinder อีกทีหนึ่ง โดย Handler เป็นเสมือนตัวกลางที่ใช้ในการส่งข้อมูลกันไปมา ส่วน IBinder เป็นตัวในการเชื่อมต่อกันระหว่าง Component กับ Bound Service (ดังนั้น Messenger ถือว่าเป็นตัวกลางที่รวมตัวส่งข้อมูลและตัวเชื่อมต่อไว้ในตัวเดียวกัน และทำให้ข้อมูลสามารถส่งข้ามไปมาระหว่างแอปฯหรือ Process ด้วย)
ดังนั้นเวลาที่ Component กับ Bound Service เชื่อมต่อกันผ่าน Messenger ก็จะมีลักษณะหน้าตาแบบนี้ครับ
เมื่อกลับมาดูที่โค้ดใน Bound Service ที่เจ้าของบล็อกทำไว้ในตอนแรก สิ่งที่ต้องทำใหม่ก็คือทำให้ Bound Service ใช้ Messenger ซะ
// AwesomeBoundService.ky
class AwesomeBoundService : Service() {
private val messenger = Messenger(IncomingHandler())
override fun onBind(intent: Intent?): IBinder? {
return messenger.binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) {
return ...
}
override onCreate() {
...
}
override onDestroy() {
...
}
@SupressLint("HandlerLeak")
private inner class IncomingHandler : Handler() {
override fun handleMessage(message: Message) {
// Do something when received the incoming message
}
}
}
ลาก่อน LocalBinder เพราะนายไม่ดีพอที่เราจะใช้งาน...
Messenger จะถูกสร้างขึ้นใน Bound Service ที่สร้างขึ้นมาจาก Handler ที่ถูกเตรียมไว้เพื่อรับข้อมูลที่ Component จะส่งเข้ามา โดยข้อมูลจะส่งเข้ามาที่ handleMessage(...) ส่วนใน onBind(...) ก็จะส่ง IBinder ของ Messenger แทน
โครงสร้างของ Message
ถ้า Handler คือตัวกลางในการส่งข้อมูล ตัวข้อมูลที่ถูกส่งไปมาก็คือ Message นั่นเอง ดังนั้นการที่จะส่งข้อมูลไปมาระหว่าง Component กับ Bound Service ก็ต้องเข้าใจโครงสร้างของ Message กันเสียก่อนMessage ประกอบไปด้วยข้อมูลต่างๆดังนี้
• handle: Handler
• what: Int
• arg1: Int
• arg2: Int
• obj: Object
• data: Bundle
• target: Handler
• replyTo: Messenger
ผู้ที่หลงเข้ามาอ่านสามารถยัดข้อมูลลงใน what, arg1, arg2, data และ obj ได้ตามต้องการ โดยที่ Message ถูกออกแบบมาให้สร้างด้วยคำสั่ง obtain(...) เท่านั้น
Message.obtain(handler: Handler?)
Message.obtain(handler: Handler?, what: Int)
Message.obtain(handler: Handler?, what: Int, obj: Any)
Message.obtain(handler: Handler?, what: Int, arg1: Int, arg2: Int)
Message.obtain(handler: Handler?, what: Int, arg1: Int, arg2: Int, obj: Any)
หรือจะสร้าง Message ขึ้นมาเปล่าๆแล้วค่อยกำหนดค่าทีหลังก็ได้เช่นกัน
val message = Message.obtain()
message.what = ...
message.arg1 = ...
message.arg2 = ...
message.obj = ..(Object/Any)...
message.data = ..(Bundle)..
message.target = ..(Handler)..
message.replyTo = ..(Messenger)..
และเนื่องจาก Message ถูกออกแบบมาเพื่อใช้ส่งข้อมูลระหว่าง Component ในแอนดรอยด์ ผู้ที่หลงเข้ามาอ่านจึงสามารถส่งข้อมูลที่อยู่ในรูปแบบ Bundle ได้เลย โดยแนบไปในชื่อ data นั่นแหละ ซึ่งเจ้าของบล็อกแนะนำให้ส่งข้อมูลผ่าน data มากกว่า obj นะ เพราะ Bundle ถูกควบคุมด้วย Key ซึ่งจัดการที่ปลายทางได้ง่ายกว่าการส่งเป็น Object ที่ต้องมานั่ง Cast Class ซึ่งไม่เหมาะกับข้อมูลที่เป็น Model Class ที่สร้างขึ้นมาเอง (ถ้าเรียกข้ามแอปฯ)
จะเห็นว่าข้อมูลใน Message ไม่จำเป็นต้องกำหนดให้ครบทั้งหมดก็ได้ อยู่ที่ว่าจะส่งอะไรไปบ้าง
เมื่อใช้เป็น Message แล้ว Component เรียกใช้ Bound Service ยังไง?
Component ก็เรียกผ่านคำสั่ง bindService(...) เหมือนเดิมน่ะแหละ แต่เวลาที่ onServiceConnected(...) ทำงาน จะได้ IBinder ส่งมาด้วย ก็เอาไปสร้างเป็น Messenger เก็บไว้ที่ฝั่ง Component ซะ// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var messenger: Messenger? = null
...
override fun onStart() {
super.onStart()
val intent = Intent(this, AwesomeBoundService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
super.onStop()
unbindService(serviceConnection)
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
messenger = null
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
messenger = Messenger(service)
}
}
}
เมื่อใดก็ตามที่ Component อยากจะสั่งให้ Bound Service ทำงาน ก็ให้สร้าง Message ขึ้นมาแล้วส่งข้อมูลด้วย Messenger ที่เก็บไว้ใน Global นั่นเอง
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var messenger: Messenger? = null
...
fun doSomethingInBoundService() {
messenger?.let {
val message = Message.obtain(...)
it.send(message)
}
}
}
บ่อยครั้งที่ Service สามารถสั่งให้ทำอะไรได้มากกว่าหนึ่งอย่าง ดังนั้นจึงนิยมแนบค่าไว้ใน what เพื่อเป็นตัวกำหนดว่าจะให้ทำอะไร เพื่อที่ Service จะได้เช็คค่าดังกล่าวแล้วเรียกคำสั่งต่างๆตามที่กำหนดไว้
// AwesomeBoundService.kt
class AwesomeBoundService : Service() {
companion object {
const val COMMAND_FETCH_LOCATION = 0
const val COMMAND_CLEAR_LOCATION_HISTORY = 1
const val COMMAND_SET_LOCATION_RANGE = 2
}
...
@SuppressLint("HandlerLeak")
private inner class IncomingHandler : Handler() {
override fun handleMessage(message: Message) {
when (message.what) {
COMMAND_FETCH_LOCATION -> fetchLocation()
COMMAND_CLEAR_LOCATION_HISTORY -> clearLocationHistory()
COMMAND_SET_LOCATION_RANGE -> setLocationRange(message.arg1)
else -> super.handleMessage(message)
}
}
}
}
ถ้ามีข้อมูลส่งมาด้วย ก็ต้องรู้ด้วยว่าควรดึงข้อมูลจากค่าไหนไปใช้งาน ซึ่งคนกำหนดควรจะเป็นฝั่ง Service นะ แล้ว Component ส่งเข้ามาให้ตรงตามที่กำหนดไว้
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var messenger: Messenger? = null
...
fun fetchLocation() {
messenger?.let {
val message = Message.obtain(null, AwesomeBoundService.COMMAND_FETCH_LOCATION)
it.send(message)
}
}
}
จะเห็นว่าในคำสั่ง obtain(...) ค่า Handler ซึ่งเป็นการกำหนด Handler ปลายทางที่จะส่งข้อมูลไปให้ แต่ที่เจ้าของบล็อกกำหนดเป็น Null ไว้ ก็เพราะว่า Message ถูกเรียกผ่าน Messenger อยู่แล้ว เดี๋ยว Messenger จะกำหนด Handler ให้เอง
Concept ของการใช้ Messenger ก็มีประมาณนี้แหละ ไม่ยากเกินไปเนอะ?
กลับมาต่อที่เรื่องการส่งข้อมูลระหว่าง Component กับ Bound Service
การส่งข้อมูลผ่าน Handler ที่ผ่านมานั้นจะเป็นแบบ One-way นะ ซึ่งหมายความว่าโค้ดในตัวอย่างก่อนหน้านี้จะทำได้แค่ Component สั่งงาน Bound Service เท่านั้นดังนั้นการจะสื่อสารแบบ Two-way จะต้องให้ฝั่งใดฝั่งหนึ่งส่ง Handler มาให้ก่อน แล้วอีกฝั่งค่อยส่งกลับไปให้ตอนส่งข้อมูลแทน โดยแนบค่า replyTo มาให้ด้วย จะได้รู้ว่าควรจะส่งข้อมูลกลับไปให้ Component ตัวไหน
ดังนั้นถ้าอยากให้การส่งข้อมูลเป็นแบบ Two-way ก็จะเป็นแบบนี้แทน
// AwesomeBoundService.kt
class AwesomeBoundService : Service() {
companion object {
...
const val RESULT_FETCH_LOCATION = 0
const val RESULT_CLEAR_LOCATION_HISTORY = 1
const val RESULT_SET_LOCATION_RANGE = 2
const val EXTRA_LATITUDE = "latitude"
const val EXTRA_LONGITUDE = "longitude"
}
...
@SuppressLint("HandlerLeak")
private inner class IncomingHandler : Handler() {
override fun handleMessage(message: Message) {
when (message.what) {
COMMAND_FETCH_LOCATION -> {
val replyMessenger = message.replyTo
fetchLocation(object : AwesomeBoundService.Callback {
override fun onLocationFetched(lat: Double, lng: Double) {
val resultMessage = Message.obtain(null, RESULT_FETCH_LOCATION)
val bundle = Bundle()
bundle.putDouble(EXTRA_LATITUDE, lat)
bundle.putDouble(EXTRA_LONGITUDE, lng)
resultMessage.data = bundle
replyMessenger.sendMessage(resultMessage)
}
})
}
...
}
}
}
}
เวลา Service จะส่งข้อมูลกลับให้ดึงค่า replyTo จาก Message ด้วย ซึ่งค่าดังกล่าวอยู่ในรูปของคลาส Messenger ที่เป็นของฝั่ง Component ที่เรียกใช้งาน Service ดังนั้นเมื่อ Service ทำคำสั่งจนเสร็จแล้วอยากจะส่งข้อมูลกลับไปให้ ก็ให้เรียกคำสั่ง send(...) จาก replyTo ได้เลย
จากตัวอย่างเจ้าของบล็อกต้องการส่งพิกัดที่เป็นค่า Double ไป แต่ว่าไม่สามารถแนบผ่าน arg1 หรือ arg2 ได้ เพราะ 2 ตัวนั้นเป็น Integer ก็เลยแนบข้อมูลพิกัดผ่าน Bundle แทน
และซึ่งค่า replyTo เนี่ย ฝั่ง Component ต้องกำหนดเองด้วยนะ ไม่ใช่ว่าอยู่ดีๆก็มีให้เลย ดังนั้นตอนที่ Component สร้าง Message ขึ้นมาเพื่อส่งไปให้ Service ก็ต้องกำหนดค่าดังกล่าวด้วย โดยสร้าง Messenger ขึ้นมาจาก Handler ที่อยากจะให้ส่งข้อมูลกลับไปได้เลย
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var messenger: Messenger? = null
...
fun fetchLocation() {
messenger?.let {
val message = Message.obtain(null, AwesomeBoundService.COMMAND_FETCH_LOCATION)
message.replyTo = Messenger(resultHandler)
it.send(message)
}
}
@SuppressLint("HandlerLeak")
private val resultHandler = object : Handler() {
override fun handleMessage(message: Message) {
when (message.what) {
AwesomeBoundService.RESULT_FETCH_LOCATION -> {
message.data?.let {
val latitude = it.getDouble(AwesomeBoundService.EXTRA_LATITUDE)
val longitude = it.getDouble(AwesomeBoundService.EXTRA_LONGITUDE)
// Do something
}
}
else -> super.handleMessage(message)
}
}
}
}
จากโค้ดตัวอย่างนี้ การทำงานแบบ Two-way ก็จะมีลักษณแบบนี้
เรียก Bound Service จากแอปฯตัวอื่น
จากที่อธิบายไปว่า Messenger เกิดมาเพื่อใช้ส่งข้อมูลระหว่าง Component โดยไม่จำเป็นต้องอยู่ในแอปฯหรือ Process เดียวกันด้วยซ้ำ ดังนั้นมาลองเรียกใช้งาน Bound Service กันดีกว่าอย่างแรกเลยก็คือต้องกำหนด Bound Service เพื่อให้สามารถเรียกใช้งานจากแอปฯภายนอกได้โดยกำหนดค่า Export ให้เป็น True
// BoundServiceApplication
// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
...
<application ...>
...
<service
android:name="AwesomeBoundService"
android:enabled="true"
android:exported="true" />
</application>
</manifest>
ส่วนการเรียกใช้งาน Bound Service จากแอปฯอื่นจะใช้วิธีกำหนดจาก Component Name แทน เพราะไม่สามารถอ้างอิงถึงคลาสโดยตรงได้
// ExternalApplication
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var messenger: Messenger? = null
...
override fun onStart() {
super.onStart()
val intent = Intent().apply {
component = ComponentName("com.akexorcist.service", "com.akexorcist.service.AwesomeBoundService")
}
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
super.onStop()
unbindService(serviceConnection)
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
messenger = null
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
messenger = Messenger(service)
}
}
}
เพราะว่าอ้างอิงค่าต่างๆจาก Bound Service ไม่ได้ เนื่องจากเป็นคนละแอปฯกัน ดังนั้นจึงต้องสร้าง Constant เพื่อเก็บค่าต่างๆที่ต้องใช้ใน Bound Service ไว้ที่ Component ตัวนี้ด้วย
// ExternalApplication
// MainActivity.kt
class MainActivity : AppCompatActivity() {
companion object {
const val COMMAND_FETCH_LOCATION = 0
const val RESULT_FETCH_LOCATION = 0
const val EXTRA_LATITUDE = "latitude"
const val EXTRA_LONGITUDE = "longitude"
}
private var messenger: Messenger? = null
...
fun fetchLocation() {
messenger?.let {
val message = Message.obtain(null, COMMAND_FETCH_LOCATION)
message.replyTo = Messenger(resultHandler)
it.send(message)
}
}
@SuppressLint("HandlerLeak")
private val resultHandler = object : Handler() {
override fun handleMessage(message: Message) {
when (message.what) {
RESULT_FETCH_LOCATION -> {
message.data?.let {
val latitude = it.getDouble(EXTRA_LATITUDE)
val longitude = it.getDouble(EXTRA_LONGITUDE)
// Do something
}
}
else -> super.handleMessage(message)
}
}
}
...
}
เรียบร้อย~ ส่วนใหญ่นั้นยังคงเหมือนเดิม แค่ว่าค่าตัวแปรต่างๆที่อ้างอิงโดยตรงไม่ได้ก็ใช้วิธีกำหนดค่าขึ้นมาเองซะ (ต้องตรงกับใน Bound Service ด้วยนะ เดี๋ยวคุยกันไม่รู้เรื่อง)
จริงๆแล้วยังสามารถส่งข้อมูลผ่าน AIDL ได้ด้วยนะ
AIDL มีชื่อเต็มๆคือ Android Interface Definition Language เป็นช่องทางที่ระบบแอนดรอยด์ได้ทำขึ้นมาเพื่อใช้สื่อสารข้อมูลระหว่าง Process ซึ่งนักพัฒนาทั่วไปไม่ค่อยได้ใช้กันซักเท่าไร จริงๆใน Bound Service ก็สามารถใช้ AIDL ได้เหมือนกัน แต่ว่าขอข้ามเรื่องนี้ไปละกันเนอะ เพราะมันมีขั้นตอนเยอะพอสมควรเลยล่ะสรุป
จุดเด่นของ Bound Service นั้นคือการที่ Service สามารถสื่อสารกับ Component ได้ต่อเนื่อง ซึ่งต่างจาก Started Service ที่ทำงานเสร็จเมื่อไรก็จบๆกันไป และลักษณะการเรียกใช้งานก็จะแตกต่างด้วยเช่นกัน ต้องมีโค้ดสำหรับ IBinder และ Handler เพิ่มเข้ามา เพราะทั้ง 2 ตัวนี้เป็นหัวใจสำคัญในการทำงานของ Bound Serviceโดยในบทความนี้ได้ยกตัวอย่าง Bound Service ในรูปแบบของ Background Service ซึ่งในการทำงานจริงๆก็ควรใช้ JobScheduler ซะมากกว่า (ไม่งั้นเดี๋ยวจะซวยใน Android 8.0 Oreo) แต่ก็สามารถสร้าง Bound Service ให้ทำงานเป็น Foreground Service ได้เหมือนกันนะ เพียงแค่ใช้คำสั่ง startForeground(...) ใน Bound Service แล้วผูกเข้ากับ Notification เท่านั้นเอง
อย่าลืมว่า Service แต่ละแบบนั้นมีจุดประสงค์ในการใช้งานแตกต่างกันไป ดังนั้นถ้าต้องการจะเขียน Service ซักตัวเพื่อใช้งานในแอปฯ ก็ควรพิจารณาก่อนว่ารูปแบบการทำงานที่ต้องการนั้นควรจะเขียน Service ในรูปแบบไหน