16 May 2017

ว่าด้วยเรื่อง Issue ของ Activity Stack สุดแปลกที่ไม่เคยเจอมาก่อน

Updated on

        เรื่องมีอยู่ว่าเจ้าของบล็อกได้เจอกับ Issue ตัวหนึ่งในโปรเจคที่ทำให้ลูกค้า ซึ่งเป็น Issue ที่มีลักษณะแปลกๆพอสมควร ซึ่งเจ้าของบล็อกก็ได้ไล่ตามหาสาเหตุเพื่อแก้ไขปัญหาตัวนี้และพบว่ามันแปลกประหลาดยิ่งนัก จึงหยิบเรื่องราวของ Issue ตัวนี้มาเล่าให้ฟังเผื่อว่าผู้ที่หลงเข้ามาอ่านคนไหนเจอปัญหาคล้ายๆกันแบบนี้ ก็จะได้ไม่ต้องเสียเวลานานครับ

เบาะแสที่มี

        จากคำบอกเล่าของ Issue ที่เกิดขึ้น

        • ผู้ใช้เข้าใช้งานและเข้าไปที่เมนูหนึ่งที่อยู่ข้างในแอปฯ
        • กดปุ่ม Home เพื่อย่อแอพและไปใช้งานแอพตัวอื่นชั่วคราว
        • กดที่ Icon ใน Home Screen หรือ App Drawer
        • แอพที่ถูกย่อไว้แทนที่จะกลับขึ้นมาทำงานใหม่ กลับเปิดขึ้นมาใหม่ตั้งแต่แรก
        • เมื่อกด Back เพื่อปิดจะย้อนกลับไปหน้าเก่าที่เคยเปิดค้างไว้ตอนแรกสุด
        • เป็นแค่บางเครื่อง และไม่ได้เป็นทุกครั้ง
        • เป็นเฉพาะ Release Build ถ้าทดสอบกับ Debug Build จะปกติสุขดี


        ฟังดูลึกลับดีเนอะ? (ซึ่งแน่นอนว่าบั๊กส่วนใหญ่ที่ลูกค้าเจอก็มักจะเป็นอะไรแปลกๆแบบนี้แหละ)

เริ่มจาก Reproduce ให้ได้ก่อน

        Issue ส่วนใหญ่ถ้า Reproduce ไม่ได้ ก็แก้ไขได้ยากเนอะ และยิ่งเป็น Issue ที่ฟังดูแปลกประหลาดแบบนี้ก็ยิ่งต้อง Reproduce ให้ได้ เพราะจากเบาะแสที่มี มันยากมากที่จะคาดเดาสาเหตุได้

        ก็เลยไปนั่งทดสอบกับตัว Production ดู ซึ่งก็มี Issue นี้อยู่จริงๆ เลยต้องกลับไปดึงโค้ดของ Master Branch ที่ใส่ Tag ของเวอร์ชันนั้นมาลอง Build ดูเพื่อหาสาเหตุ เพราะจะได้ปิด ProGuard และไล่โค้ดได้ง่ายขึ้น

        ตอนแรกก็เข้าใจว่าเป็นที่ Android Manifest ที่ไปกำหนด Launch Mode หรือป่าว แต่จากที่ลอง Decompile APK ที่มีปัญหา (แอปฯของตัวเองแท้ๆ...) ก็พบว่าไม่ได้กำหนดอะไร ไว้เลยซักนิด และไล่เช็คจนครบถ้วนแล้วก็ไม่พบอะไรที่น่าจะเกี่ยวข้อง และระหว่าง Reproduce เพื่อหาสาเหตุก็พบว่ามันเป็นบางครั้งจริงๆ เดี๋ยวก็เป็น เดี๋ยวก็ไม่เป็น

        แต่เบาะแสเหล่านี้ก็ยังไม่เพียงพอที่จะเอาไปค้นหาต่อใน StackOverflow น่ะสิ...

หา Pattern ของ Issue ตัวนั้น

        เมื่อรายละเอียดของ Issue ที่เกิดขึ้นยังคลุมเครืออยู่จนไม่สามารถหาข้อมูลได้ เจ้าของบล็อกจึงต้องหา Pattern ของ Issue นี้ให้ได้ ก็เลยต้องใช้เวลาอยู่พักใหญ่เหมือนกัน ถึงจะสังเกตได้ว่า Issue นี้ เกิดขึ้นเฉพาะตอนที่ดาวน์โหลดแอปฯมาติดตั้งและเปิดใช้งานครั้งแรกเท่านั้น ถ้าเปิดแอปฯครั้งต่อไปหรือแม้แต่ Clear Data ก็จะไม่เจอ Issue ตัวนี้เลย ต้องติดตั้งใหม่เท่านั้น (ลบออกแล้วลงใหม่ก็เจอเช่นกัน)

และสาเหตุก็คือ...

        หลังจากได้เบาะแสมากพอที่จะใช้เป็นคีย์เวิร์ดในการค้นหาข้อมูล ก็พบว่าเป็น Issue ที่เจอได้ยากพอสมควร (ซึ่งก็ยังไม่รู้เหมือนกันว่าเพราะอะไรถึงเจอ ฮาๆ)

        ซึ่งเจ้าของบล็อกไปเจออยู่ใน Issue Tracker ของ Google

        • Activity stack behaves incorrectly during the first run of an app when started from Eclipse
        • Activity stack confused when launching app from browser

        ที่น่าสังเกตก็คือ Issue นี้เกิดขึ้นตั้งแต่สมัย Android เวอร์ชันเก่าๆมานมนานแล้ว แต่กลับยังไม่ได้ถูกแก้ไขจนถึงทุกวันนี้ เครื่องที่เจ้าของบล็อกทดสอบแล้วยังเจอปัญหานี้อยู่ก็เป็น Android 7.1 น่ะแหละ

        ซึ่งปัญหาที่เกิดขึ้นก็คือ Activity Stack ทำงานไม่ถูกต้อง ซึ่งเกิดขึ้นเฉพาะตอนใช้งานครั้งแรกหลังจากที่ติดตั้งแอปฯเท่านั้น (และต้องเป็น Release Build ด้วย) ในระหว่างใช้งานอยู่นั้น ถ้ากดปุ่ม Home เพื่อย่อแอปฯและกลับมาเปิดใช้งานต่อด้วยการกดที่ Icon ที่อยู่ใน Home Screen หรือ App Drawer จะทำให้ตัวระบบแอนดรอยด์เรียก Activity ของหน้าแรกสุดขึ้นมาใหม่แทน โดยที่หน้าเก่าๆที่เคยเปิดไว้อยู่ก็จะเก็บไว้ใน BackStack (จึงทำให้กด Back แล้วกลับไปหน้าเดิมที่เคยเปิดไว้)

        น่าเศร้าชะมัด...

จะแก้ไขปัญหานี้ยังไงดีล่ะ?

        เมื่อเป็นปัญหาจากตัวแอนดรอยด์ตั้งแต่แรก (และยังไม่ได้แก้ไขซะที) นักพัฒนาอย่างเราๆจึงต้องแก้ไขด้วยการแก้ปัญหาดังกล่าวด้วยโค้ดแทนฮะ

        แล้วจะเช็คยังไงล่ะว่า แอปฯถูกเปิดใช้งานครั้งแรก + แอปฯกลับมาทำงานต่อโดยที่ผู้ใช้กดเปิดจากไอคอนใน Home Screen หรือ App Drawer?

        คำตอบคือ... จะไปเช็คแบบนั้นให้ยุ่งยากทำไมล่ะ ผู้ที่หลงเข้ามาอ่านสามารถใช้ประโยชน์จากคำสั่งของคลาส Activity ที่ชื่อว่า isTaskRoot() ซึ่งเป็นคำสั่งที่ใช้เช็คว่า Activity นั้นๆ เคยถูกสร้างขึ้นมาและอยู่ใน BackStack หรือป่าว

        จึงเอาคำสั่งดังกล่าวไปใช้งานใน Activity ตัวแรกสุดของแอปฯ เพื่อเช็คว่าเกิด Issue ดังกล่าวหรือไม่ได้เลย ด้วยคำสั่งแบบนี้

class SplashScreenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        checkActivityStackWorkCorrectly()
    }

    private fun checkActivityStackWorkCorrectly() {
        if (!isTaskRoot) {
            finish()
        }
    }
}

        ถ้า Activity ตัวนี้เคยถูกสร้างมาก่อนหน้านี้และเก็บไว้ใน BackStack อยู่ (Is not task root) ก็แปลว่า Activity Stack ทำงานผิดอยู่ ดังนั้นให้ปิด Activity ตัวนี้ทิ้งซะ เดี๋ยวแอปฯก็จะดึง Activity ที่เก็บไว้ใน BackStack มาทำงานต่อจากของเดิมเอง

        ซึ่งคำสั่งนี้ก็เกือบจะสมบูรณ์แล้วครับ ถ้าแอปฯของผู้ที่หลงเข้ามาอ่านกำหนดให้ Activity ตัวดังกล่าวทำงานแค่ครั้งเดียวเท่านั้น (อย่างเช่นพวก Splash Screen) แต่บางแอปฯถูกออกแบบมาให้หน้าแรกสุดของแอปฯนั้นสามารถถูกเรียกให้ทำงานได้ตลอดเวลา ดังนั้นการใช้คำสั่ง isTaskRoot() น่าจะทำให้เกิดความชิบหายมากกว่าการแก้ปัญหา

        ดังนั้นเจ้าของบล็อกจึงต้องเพิ่มคำสั่งเพื่อให้เงื่อนไขรัดกุมมากขึ้น

class SplashScreenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        checkActivityStackWorkCorrectly()
    }

    private fun checkActivityStackWorkCorrectly() {
        if (!isTaskRoot &&
            intent?.hasCategory(Intent.CATEGORY_LAUNCHER) == true &&
            intent?.action == Intent.ACTION_MAIN) {
            finish()
        }
    }
}

        เจ้าของบล็อกเพิ่มคำสั่งเพื่อเช็คเงื่อนไขว่า Intent ที่ส่งมามีการกำหนดค่า CATEGORY_LAUNCHER และ ACTION_MAIN มาให้หรือป่าว ซึ่งเป็นค่าที่ถูกส่งมาเมื่อกดเปิดแอปฯจากไอคอนที่อยู่ใน Home Screen หรือ App Drawer เท่านั้น

ทำไมต้องเช็ค Category และ Action ของ Intent ด้วย?

        ถ้านึกไม่ออกให้ลองย้อนกลับไปดูใน Android Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

    <application ...>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        ...

    </application>

</manifest>

        เมื่ออยากจะให้ Activity ทำงานเป็นตัวแรกสุดเมื่อกดที่ไอคอนแอปฯ ก็ให้กำหนด Action เป็น Main และ Category เป็น Launcher

        ดังนั้นผู้ที่หลงเข้ามาอ่านจึงสามารถใช้ค่าเหล่านี้มาเช็คได้นั่นเอง

สรุป

        เป็นหนึ่งใน Issue ที่แปลกประหลาดมาก ที่ต้องบอกเลยว่าเจ้าของบล็อกเองก็เพิ่งจะเคยเจอ ซึ่งยังหาสาเหตุไม่ได้ว่าเพราะคำสั่งอะไรถึงทำให้เกิด Issue ดังกล่าว เนื่องจากเป็นกับบางแอปฯเท่านั้น

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

แหล่งข้อมูลอ้างอิง

        • Activity stack behaves incorrectly during the first run of an app when started from Eclipse
        • Activity stack confused when launching app from browser
        • App restarts rather than resumes
        • App completely restarting when launched by icon press in launcher