ทำความรู้จัก launchMode ทั้งสี่ของ Android Activity: standard, singleTop, singleTask และ singleInstance

Posted on 15 Apr 2015 17:01 | 23308 reads | 0 shares
 

Activity ถือเป็นหนึ่งในสิ่งที่ออกแบบมาได้อย่างยอดเยี่ยมบนแอนดรอยด์เลยก็ว่าได้ ด้วยความสามารถในการจัดการการใช้งานหน่วยความจำได้อย่างมีประสิทธิภาพของมันจนระบบ Multitasking สามารถทำได้อย่างสมบูรณ์แบบบนแอนดรอยด์นั่นเอง

อย่างไรก็ตาม Activity ไม่ได้แค่สามารถนำขึ้นมาแสดงบนหน้าจอได้ หากแต่แต่ยังมีรายละเอียดอื่นๆที่น่าสนใจอีกหลายอย่างเพื่อให้มันทำงานได้ตามความต้องการของเรา หนึ่งในนั้นคือสิ่งที่กำลังจะพูดถึงในบล็อกนี้อย่าง launchMode

เนื่องจากว่า Activity ถูกสร้างมาเพื่อใช้งานที่ต่างกันออกไปตามแต่ละหน้า บ้างก็ถูกสร้างมาเพื่อทำงานกับ Intent ที่ถูกส่งมาโดยเฉพาะ หากมีส่งมา 4 Intents ก็ควรจะสร้างมา 4 หน้าจอ เช่นหน้า Compose Email ของ Email Client หรือบ้างก็ถูกสร้างมาเพื่อเป็นหน้าจอเดียว ไม่ควรสร้างหน้าเพิ่ม ยกตัวอย่างเช่นหน้า Email Inbox นั่นเอง

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

การกำหนด launchMode

เบื้องต้นเราสามารถกำหนด launchMode ใน Tag <activity> ในไฟล์ AndroidManifest.xml ได้เลยดังนี้

<activity
    android:name=".SingleTaskActivity"
    android:label="singleTask launchMode"
    android:launchMode="singleTask">

โดย launchMode มีอยู่ 4 โหมดด้วยกัน ดังต่อไปนี้

standard

โหมดนี้เป็นโหมดที่ถูกกำหนดให้เป็นโหมด Default ในกรณีที่ไม่กำหนด launchMode

ลักษณะการทำงานของ Activity ในโหมดนี้คือ จะสร้าง Activity ใหม่เสมอ ใช้ในกรณีที่ต้องการสร้างหน้าจอให้มาตอบรับการทำงานแตกต่างกันไปทุก Intent ยกตัวอย่างเช่น หน้า Compose Email ที่ควรจะเด้ง Activity อันใหม่เพื่อรับ Intent การพิมพ์อีเมลใหม่เสมอ ถ้ามีส่งมา 10 Intents ก็ควรจะเด้งมา 10 หน้าที่ต่างกันไป ผลคือระบบสามารถมี Activity ชนิด standard กี่ตัวเปิดอยู่ก็ได้นั่นเอง

สิ่งที่เกิดขึ้นบน Android รุ่น pre-Lollipop

Activity ประเภท standard จะไปอยู่บนสุดของ Stack ใน Task ที่เรียก Activity ขึ้นมา ไม่ว่า Task ที่เรียกจะเป็นแอปฯที่เป็นเจ้าของของ Activity นั้นๆหรือไม่ก็ตาม

standardtopstandard

นี่คือตัวอย่างการแชร์ภาพจาก Gallery เข้าสู่ Activity ประเภท standard ที่เขียนขึ้น

standardgallery2

เมื่อเปิด Task Manager จะเห็นเป็นแบบนี้

gallerystandard

หากมีการสลับ Task ไปที่อื่นและกลับมายัง Gallery ก็จะเจอหน้าของ Activity ที่เราเด้งขึ้นมาทับไว้ด้านหน้า ส่งผลให้หากจะทำอะไรต่อบน Gallery ต้องกด Back กลับจาก Activity ที่เด้งขึ้นมาก่อน

สิ่งที่เกิดขึ้นบน Android รุ่น Lollipop

สำหรับกรณีที่คนเรียก Intent เป็นแอปฯตัวเอง จะทำงานเหมือนกับบน pre-Lollipop ทุกประการ

standardstandardl

แต่กรณีที่มีการยิง Intent มาจากแอปฯอื่น ระบบจะแตก Task ใหม่มาให้แทนที่จะทับบน Task เดิม

standardgalleryl

เมื่อกดดู Task Manager ก็จะเป็นแบบนี้

gallerystandardl1

ทั้งนี้เป็นเพราะระบบ Task Management บน Lollipop ถูกปรับแก้ให้การทำงานดู Make Sense ขึ้นนั่นเอง และคราวนี้หากต้องการจะทำอะไรกับ Gallery ก็สามารถกดกลับมาทำได้ทันทีโดยไม่กระทบต่อ Activity ที่เรียกขึ้นมาเพราะอยู่คนละ Task นอกจากนั้นเรายังสามารถกดแชร์จาก Gallery เพิ่มอีกได้ทันที แล้วจะแตกเป็น Task เพิ่มอีกเช่นกัน

gallerystandardl2

ตัวอย่างการใช้งาน Activity โหมด standard ก็เช่นหน้า Compose Email หน้าโพสต์ข้อความลง Social Network เป็นต้น หากคิดถึง Activity ที่แต่ละหน้าต่างคนต่างทำงานตาม Intent ที่ได้รับเข้ามา ให้คิดถึงโหมด standard ครับ

singleTop

โหมดต่อไปคือโหมด singleTop ที่โดยรายละเอียดแล้วจะคล้ายกับ standard คือสามารถมีได้มากกว่า 1 Activity รันในระบบ ทำงานต่างกันไปตาม Intent ที่ส่งเข้ามา แต่ต่างกันตรงที่ถ้า Task นั้นๆมี Activity ที่จะเรียกอยู่บนตำแหน่งบนสุดของ Stack อยู่แล้ว ก็จะไม่สร้าง Activity ใหม่อีก ส่วน Intent ที่ยิงมาจะถูกส่งไปยังคำสั่ง onNewIntent() ใน Activity ที่มีอยู่แล้วแทน

singletop

ด้วยเหตุนี้ ใน Activity ที่ถูกกำหนดให้ทำงานในโหมด singleTop เราจึงจำเป็นต้อง Handle Intent ทั้งจาก onCreate() และ onNewIntent() เพื่อให้ทำงานได้อย่างสมบูรณ์

ตัวอย่างการใช้งานโหมดนี้ที่เห็นบ่อยก็คือฟังก์ชั่น Search เมื่อเราจะทำการค้นหาอะไรหลังจากพิมพ์ข้อความแล้ว ตอนกดค้นหาก็จะเด้ง SearchActivity ขึ้นมา และใน SearchActivity นี้ก็มีช่องให้กรอกเพื่อค้นหาซ้ำได้ แต่หากต้องเด้ง SearchActivity ใหม่ขึ้นมาอีกตอนพิมพ์เสร็จก็คงไม่ดี มิฉะนั้นอาจจะเกิดเหตุการณ์ SearchActivity ซ้อนกัน 10 ชั้นก็เป็นไปได้ คงไม่งามตอนกด Back แน่ๆ โหมด singleTop จึงมีเอาไว้ใช้ในกรณีแบบนี้นั่นเอง เพื่อยิง Intent เข้าหา Activity ตัวเองแล้วอัพเดตผลการทำงานแทนที่จะเด้ง Activity ใหม่

อย่างไรก็ตาม singleTop ทำงานได้เฉพาะภายใน Task ตัวเองเท่านั้น หากมีการส่ง Intent จากแอปฯอื่นมา ถึงแม้จะมี Activity อยู่ตำแหน่งบนสุดของ Stack ใน Task ใด Task หนึ่งที่เปิดอยู่ ก็ไม่ถือว่าเข้าข่ายโหมด singleTop แต่อย่างใด Activity จะถูกสร้างใหม่เสมอด้วย Behavior เดียวกับ standard คือถ้าเป็น pre-Lollipop จะถูกแปะทับไว้บน Task นั้นๆ แต่ถ้าเป็น Lollipop จะแยกเป็น Task ใหม่

singleTask

โหมดนี้จะถือว่าแตกต่างจาก standard และ singleTop ไปพอสมควรเพราะ โหมด singleTask นั้นจะอนุญาตให้มี Activity นั้นๆแค่เพียง 1 อันในระบบ (เทียบได้กับ Singleton) หากมี Activity นั้นๆเปิดอยู่ใน Task ใด Task หนึ่ง ระบบจะนำ Activity พร้อมทั้ง Task ที่ถือ Activity อยู่ทั้ง Task มาแสดงบนหน้าจอทันที พร้อมยิง Intent เข้า onNewIntent() แต่ถ้าไม่มี ก็จะสร้างใหม่ให้พร้อมแตก Task ใหม่ให้ทันทีในกรณีที่เป็นการยิง Intent มาจากแอปฯอื่น

การทำงานภายในแอปฯตัวเอง

หากยังไม่มี Activity ที่ถูกกำหนดเป็น singleTask ใน Task ใดๆ Activity จะถูกสร้างใหม่และนำมาไว้บนสุดตามปกติ

singleTask1

แต่หากมี Activity singleTask เปิดไว้อยู่แล้ว Activity ทั้งหมดที่อยู่เหนือ Activity singleTask จะถูกทำลายทิ้งตาม Lifecycle เพื่อให้ Activity ที่เป็น singleTask อยู่ด้านบนสุดของ Stack ส่วน Intent ก็จะถูกส่งมาที่คำสั่ง onNewIntent() บน singleTask Activity ที่เปิดอยู่

singleTaskD

อย่างไรก็ตาม Document มีเขียนไว้ว่า

The system creates a new task and instantiates the activity at the root of the new task. (ระบบจะสร้าง Task ใหม่และใส่ Activity ไว้เป็น Root Activity ใน Task ใหม่นั้น)

แต่จากการทดลองพบว่ามันไม่เป็นเหมือนที่เขียนไว้ใน Document ตัว singleTask Activity ยังคงถูกแปะไว้บนสุดของ Task เดิม ยืนยันได้จากสิ่งที่คำสั่ง dumpsys activity พ่นออกมา

Task id #239
  TaskRecord{428efe30 #239 A=com.thecheesefactory.lab.launchmode U=0 sz=2}
  Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
    Hist #1: ActivityRecord{429a88d0 u0 com.thecheesefactory.lab.launchmode/.SingleTaskActivity t239}
      Intent { cmp=com.thecheesefactory.lab.launchmode/.SingleTaskActivity }
      ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}
    Hist #0: ActivityRecord{425fec98 u0 com.thecheesefactory.lab.launchmode/.StandardActivity t239}
      Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
      ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}

แต่ถ้าต้องการให้มันทำงานตามที่ระบุไว้ในเอกสาร เราสามารถทำได้ด้วยการกำหนดค่า taskAffinity ให้กับ singleTask Activity ดังนี้

<activity
    android:name=".SingleTaskActivity"
    android:label="singleTask launchMode"
    android:launchMode="singleTask"
    android:taskAffinity="">

และนี่คือผลการทำงานหลังจากเราพยายามเรียก SingleTaskActivity ขึ้นมาครับ

singleTaskTaskAffinity

screenshot17

คราวนี้ก็ต้องพิจารณาจากการทำงานเองว่าเราควรจะใส่ taskAffinity หรือเปล่าตามแต่ลักษณะของ Activity ที่สร้างขึ้นมาครับ

การทำงานร่วมกับแอปฯอื่น

เมื่อมีการกดส่ง Intent มาจากแอปฯอื่น หากยังไม่มี Activity นั้นๆอยู่และยังไม่มี Task ของแอปฯเจ้าของ Activity อยู่ ระบบจะสร้าง Task ขึ้นมาใหม่ให้ทันทีพร้อมแปะ singleTask Activity นั้นไว้เป็น Root Activity ของ Task ใหม่

singleTaskAnotherApp1

singletaskfromapp2

แต่หากมี Task ของแอปฯนั้นๆเปิดทิ้งไว้อยู่แล้ว singleTask Activity จะถูกนำไปแปะไว้ด้านบนของ Task Stack

singleTaskAnotherApp2

แต่ในกรณีที่มี Activity ถูกเปิดไว้ใน Task ใด Task หนึ่งเป็นที่เรียบร้อยแล้ว Task ทั้ง Stack นั้นจะถูกดึงมาอยู่ด้านหน้า พร้อม finish ทุก Activity ที่อยู่เหนือ singleTask Activity ทิ้งทั้งหมด หลังจากนั้นหากมีการกด Back ก็จะต้องถอยหลังผ่าน Stack ของ Task ที่ถูกดึงมาจนหมดก่อนจะกลับไปยัง Task ก่อนหน้า

singleTaskAnotherApp3

ตัวอย่างการใช้งานของโหมดนี้ก็คือหน้าที่เราตั้งไว้เป็น Entry Point เช่น หน้า Inbox ของ Email Client หรือหน้า Timeline ของ Social Network อย่าง Facebook เป็นต้น ซึ่งหน้าพวกนี้ถ้าจะยอมให้เปิดได้หลาย Activity ก็ดูแปลกอยู่ ดังนั้น singleTask จึงถือเป็นอีกโหมดที่ใช้บ่อยพอสมควรแต่ก็ต้องใช้อย่างระวังเพราะเป็นโหมดที่มีเปิดโอกาสให้ Activity ที่อยู่เหนือ singleTask ถูกทำลายทิ้งได้

singleInstance

เป็นโหมดที่ใกล้เคียงกับ singleTask คือ Activity มีได้แค่ตัวเดียวในระบบ แต่ที่ต่างกันคือ Task ที่ถือครอง Activity ประเภทนี้จะสามารถถือ Activity ได้แค่ตัวเดียวเท่านั้น หากมีการเรียก Activity อื่นเพิ่ม ระบบจะสร้าง Task ใหม่ขึ้นมาทันที หรือถ้าเดิม Task มีอยู่หลาย Activity แล้วมาเรียก singleInstance Activity ระบบก็จะแตก Task ใหม่ให้เช่นกัน

อย่างไรก็ตาม ผลที่เกิดขึ้นค่อนข้างประหลาดคือถึงจะแตกออกเป็น 2 Tasks เมื่อดูจาก dumpsys แต่ใน Task Manager กลับเห็นแค่ตัวเดียว ขึ้นอยู่กับว่า Task ไหนที่ถูกสร้างขึ้นมาล่าสุด ส่วนอีก Task ถึงจะมองไม่เห็น แต่ทุกอย่างก็ทำงานอยู่ และไม่สามารถสลับกลับมาได้ด้วยนอกจากจะกดเปิดแอปฯจาก Launcher ใหม่ ส่งผลให้การทำงานแปลกออกไปและขัดต่อความรู้สึกเอามากๆ

ยกตัวอย่างเช่นกรณีที่เปิดไว้หลาย Activity ก่อนจะเปิด singleInstance Activity สิ่งที่เกิดขึ้นจริงคือแบบนี้

singleInstance

แต่พอเปิด Task Manager จะเห็นแค่เพียง

singleInstances

และเนื่องจาก Task นี้มีได้แค่ Activity เดียว เราจึงไม่สามารถสลับกลับไปยัง Task Stack อีกตัวได้เลย

อย่างไรก็ตาม เพื่อให้ทุกอย่างทำงานได้ดีขึ้น เราสามารถกำหนด taskAffinity ได้เช่นเดียวกับ singleTask Activity เพื่อให้ Task Manager สามารถแสดง Task ที่แยกออกมาได้

<activity
    android:name=".SingleInstanceActivity"
    android:label="singleInstance launchMode"
    android:launchMode="singleInstance"
    android:taskAffinity="">

ผลการทำงานครับ

screenshot18

โหมดนี้เป็นโหมดที่แทบไม่เคยได้ใช้ จะมีที่ได้ใช้จริงกันบ้างก็คือการทำ Launcher Activity หรือแอปฯที่มี Activity เดียว (ซึ่งมีน้อยมาก) แนะนำว่าอย่าใช้โหมดนี้ถ้าไม่จำเป็นครับเนื่องจากอาจจะเกิดปัญหาได้มากมาย

Intent Flags

ที่กล่าวมาข้างบนคือโหมดทั้งสี่ที่เราสามารถใช้กำหนดรูปแบบการสร้าง Activity ได้ ซึ่งนอกจากเราจะกำหนดใน AndroidManifest.xml ได้แล้ว เรายังสามารถกำหนดรูปแบบเพิ่มเติมแบบ Real Time ตอนสร้าง Intent ได้ด้วย ด้วยการกำหนด Flag เพิ่มเข้าไปดังนี้

Intent intent = new Intent(StandardActivity.this, StandardActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);

ก็จะเป็นการเปลี่ยนให้การ Launch StandardActivity ครั้งนี้ ทำในเงื่อนไขของ singleTop ทันที

โดยมี Flag ให้เลือกใช้เยอะมากและสามารถทำงานร่วมกันด้วยการ OR ได้อีกด้วย สามารถอ่านเพิ่มเติมเกี่ยวกับ Flag เหล่านี้ได้ที่ Intent ครับผม

หวังว่าบทความนี้จะมีประโยชน์ต่อทุกท่านนะคร้าบ สวัสดีครับ =)

ผู้เขียน: nuuneoi (Android GDE, CTO & CEO at The Cheese Factory)
นักพัฒนาแบบ Full-Stack ที่มีประสบการณ์ในการพัฒนาแอพฯแอนดรอยด์มากว่า 6 ปีและอยู่ในวงการพัฒนาแอพฯมือถือมากว่า 12 ปี มีความสนใจทางด้าน Infrastucture, Service Side, Design, UI&UX, Hardware, Optimization, Cooking, Photographing, Blogging, Training, Public Speaking และรักที่จะแชร์เรื่องราวให้ผู้คนได้อ่านได้ฟังกันผ่าน Blog