ถึงแม้ว่าจะเป็นหัวข้อที่นักพัฒนาหลายๆคนนั้นรู้จักกันดีอยู่แล้วว่า Model ต่างๆที่ใช้ภายในแอป ไม่ควรเก็บไว้ในรูปของ Static Instance หรือว่า Singleton แต่ทว่าก็อาจจะมีบางคนที่ไม่เข้าใจว่าทำไมถึงทำแบบนั้นไม่ได้ล่ะ? ดังนั้นมาดูกันว่าทำไมเราถึงไม่ควรทำเช่นนั้น

บ่อยครั้งที่เจ้าของบล็อกเห็นนักพัฒนาแอนดรอยด์มือใหม่ใช้วิธีเก็บข้อมูลจำพวก Model (ในบทความจะเรียกสั้นๆว่า​”ข้อมูล”) ไว้ใน Singleton หรือ Static Variable เพื่อให้สามารถเรียกใช้งานที่ไหนและเมื่อไรก็ได้ ซึ่งวิธีดังกล่าวก็ไม่ใช่วิธีที่ถูกซักเท่าไร

ข้อมูลต่างๆที่ใช้ภายในแอปควรจะเก็บไว้ในด้วยวิธีใดวิธีหนึ่ง ดังนี้

  • มีการ Save/Restore State ใน Activity, Fragment หรือ View
  • เก็บไว้ใน Shared Preferences
  • เก็บไว้ใน Database ไม่ว่าจะเป็น SQLite หรือ Realm

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

สามารถครอบ Singleton Class เพื่อใช้ในการเรียกข้อมูลได้ แต่ข้อมูลต้องมีการเก็บไว้ในรูปแบบใดรูปแบบหนึ่งที่ไม่ใช่การเก็บไว้ใน Singleton อย่างลอยๆ

เจ้าตายแล้ว!! บึ้ม!!

ที่เจ้าของบล็อกได้พูดถึง “แอปบึ้ม” นั้นหมายถึงแอปมันถูกปิดตัวลงจริงๆ ไม่ได้หมายถึง Lifecycle ของ Acitivty นะ

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

ซึ่งสามารถทดสอบได้โดยการสลับไปเปิดแอปอื่นๆที่ใช้ Memory เยอะๆนั่นเอง ยกตัวอย่างเช่น แอปถ่ายรูปที่ถือว่าเป็นหนึ่งในบรรดาแอปที่ใช้ Memory สูง (อะไรก็ตามที่จัดการกับข้อมูลภาพจะใช้ Memory เยอะหมดแหละ) ยิ่งถ่ายวีดีโอ 4K ได้ยิ่งดี จัดเลย! เปิดสลับวนๆไปเรื่อยๆจนกว่าแอปของผู้ที่หลงเข้ามาอ่านจะบึ้ม

หรือถ้าจะให้ง่ายกว่านั้น หลังจากที่ย่อแอปแล้ว ให้กดที่ Logcat > Terminate application ก็จะให้ผลลัพธ์เดียวกัน (ย้ำว่าต้องย่อแอปก่อนนะ)

จะรู้ได้ไงว่าแอปบึ้ม?

ให้สังเกตอาการบึ้มผ่านหน้าต่าง Android Monitor ใน Android Studio โดยเลือกไปที่แถบ Monitor เพื่อดูการทำงานของแอป

ซึ่ง Monitor ก็จะแสดงข้อมูลอยู่เรื่อยๆ ถึงแม้ว่าแอปจะถูกย่อไว้ก็ตาม ซึ่งจะขึ้นๆลงๆเล็กน้อยตามปกติ

ระหว่างนั้นก็ระดมสลับแอปที่ทำให้เกิดการบึ้มซะ

เมื่อเกิดอาการบึ้ม สิ่งที่เกิดขึ้นใน Android Monitor ก็คือแอปจะมีสถานะเป็น Dead ทันที (ดูที่ชื่อ Process จะมีคำว่า DEAD ต่อท้าย)

ทำให้ Android Monitor แสดงสถานะอะไรไม่ได้ชั่วคราว หลังจากนั้นแอปก็จะถูกเรียกขึ้นมาใหม่และทำงานต่ออีกครั้ง (แต่ Process ID จะเป็นคนละตัวกับของเก่า)

ถ้าดูภาพประกอบแล้วไม่เข้าใจ ลองดูภาพข้างล่างนี้ก็ได้

นั่นล่ะครับ อาการแอปบึ้ม โดยที่ Process เดิม (20793) ถูกทำลายลง และเมื่อเปิดใหม่อีกครั้งก็จะสร้าง Process ใหม่ (27117) และกลับมาทำงานต่อจากเดิม

แล้วแอปบึ้มมันส่งผลกับข้อมูลยังไง?

การบึ้มของแอปจะทำให้ค่าในตัวแปรต่างๆถูกเคลียร์ทิ้งทั้งหมดครับ ถึงแม้ว่าจะเป็น Singleton หรือ Static Variable ก็ตาม

แต่ทว่าแอปยังคงเก็บ State ต่างๆของ Activity/Fragment/View ไว้เหมือนเดิม และก็ทำงานต่อไปตาม Lifecycle เหมือนไม่มีอะไรเกิดขึ้น

ดังนั้นค่าที่ถูกเคลียร์ทิ้งก็จะมีแค่ตัวแปรที่ประกาศไว้ลอยๆทั้งหมด เพราะค่าที่ Save State ไว้ก็จะถูก Restore กลับมาและใช้งานได้ปกติ (ส่วน Shared Preference กับ Database ไม่หายอยู่แล้ว จึงสามารถดึงข้อมูลมาใช้งานต่อได้เลย)

มาทดลองจริงๆกันเถอะ

เนื่องจากว่างมากพอ เจ้าของบล็อกจึงไปนั่งเขียนแอปแบบง่ายๆเพื่อจำลองการสร้างข้อมูลในแบบต่างๆ โดยมี 3 แบบดังนี้

  • เก็บข้อมูลไว้ใน Singleton
  • เก็บข้อมูลไว้ใน Static
  • เก็บข้อมูลไว้ใน Activity แต่ไม่ Save/Restore State
  • เก็บข้อมูลไว้ใน Activity โดยมีการ Save/Restore State

สำหรับข้อมูลที่อยู่ใน Singleton จะเป็นแบบนี้

// SingletonCurrentUser.kt
object SingletonCurrentUser {
    var name: String? = null
}

สำหรับข้อมูลที่อยู่ใน Static Variable จะเป็นแบบนี้

// StaticCurrentUser.kt
class StaticCurrentUser {
    companion object {
        var name: String? = null
    }
}

ส่วนข้อมูลที่อยู่ใน Activity จะเป็นแบบนี้ ตัวหนึ่งจะ Save/Restore State ให้ด้วย แต่อีกตัวจะประกาศไว้อย่างลอยๆ

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private var handleStateName: String? = null
    private var regularName: String? = null

    companion object {
        private const val KEY_NAME = "key_name"
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        handleStateName = savedInstanceState.getString(KEY_NAME)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString(KEY_NAME, handleStateName)
    }
    ...
}

ตัวแปรทั้ง 4 ตัวที่ใช้ในการทดสอบก็จะได้เป็นภาพแบบนี้

ส่วนการทดสอบก็จะสร้างปุ่มขึ้นมา 2 ปุ่ม โดยให้ปุ่มแรกเป็นปุ่ม Save สำหรับกำหนดค่าลงไปในตัวแปรทั้ง 4 ตัว และอีกปุ่มเป็นปุ่ม Show สำหรับแสดงค่าที่เก็บไว้ในตัวแปรทั้ง 4 ตัว

โดยโค้ดที่ใช้ในทั้ง 2 ปุ่มจะเป็นแบบนี้

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private var handleStateName: String? = null
    private var regularName: String? = null

    companion object {
        private const val KEY_NAME = "key_name"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        buttonSave.setOnClickListener { onSaveButtonClick() }
        buttonShow.setOnClickListener { onShowButtonClick() }
    }

    private fun onSaveButtonClick() {
        val dummyName = "Akexorcist"
        SingletonCurrentUser.name = dummyName
        StaticCurrentUser.name = dummyName
        regularName = dummyName
        handleStateName = dummyName
        Toast.makeText(this, "Updated", Toast.LENGTH_SHORT).show()
    }

    private fun onShowButtonClick() {
        val message = """
            Singleton Name : ${SingletonCurrentUser.name}
            Static Name : ${StaticCurrentUser.name}
            Regular Name : $regularName
            Handle State Name : $handleStateName
            """.trimIndent()
        val alertDialog =
            AlertDialog.Builder(this)
                .setTitle("Result")
                .setMessage(message)
                .setCancelable(true)
                .create()
        alertDialog.show()
    }
    ...
}

สำหรับโค้ดตัวอย่างที่ใช้ในการทดสอบ สามารถดูแบบเต็มๆได้ที่ KeepDataTesting [GitHub]

akexorcist/KeepDataTesting
[Android] In-memory data storing for state changes testing in Android - akexorcist/KeepDataTesting

เริ่มทำการทดลอง

เมื่อกดปุ่ม Save ก็จะเก็บค่า String เป็นคำว่า Akexorcist ลงในตัวแปรทั้ง 4 ตัว และเมื่อกดปุ่ม Show ต่อก็จะแสดง Dialog ที่จะแสดงค่าในตัวแปรทั้ง 4 ตัว

จะเห็นว่าค่าในตัวแปรทั้ง 4 ตัวแสดงค่าเหมือนกันทั้งหมด ดูปกติสุขดี

ต่อไปให้กดย่อแอปแล้วทำให้แอปบึ้มซะ!!

… กำลังทำให้แอปบึ้ม …

… แอปบึ้มเรียบร้อย …

จากนั้นให้กลับมาเปิดแอปแล้วกดที่ปุ่ม Show ใหม่อีกครั้งเพื่อดูว่าค่าที่อยู่ข้างในตัวแปรทั้ง 4 ตัวจะเป็นยังไงบ้าง

อ๊ะ ตัวแปร 3 ตัวแรกกลายเป็น null แล้ว เพราะว่าแอปบึ้มทำให้ตัวแปรเหล่านั้นถูกเคลียร์ค่าทิ้งนั้นเอง จนเหลือแต่ตัวแปรตัวสุดท้ายที่ทำ Save/Restore State ไว้ จึงอยู่ปกติสุขดี

สรุป

นั่นละฮะ ทำไมผู้ที่หลงเข้ามาอ่านถึงไม่ควรเก็บค่าไว้ใน Singleton หรือ Static Valirable หรือประกาศตัวแปรไว้ลอยๆโดยไม่มีการ Save/Restore State เพราะมันจะหายไปเมื่อแอปบึ้มนั่นเอง ซึ่งการที่แอปบึ้มนั้นเป็นเรื่องที่เกิดขึ้นได้ปกติจากการใช้งานของ User ทั่วไป และนักพัฒนาส่วนใหญ่ก็มักจะมองข้ามไปมัน (ถ้าเป็นไปได้ก็ควรเทสแอปด้วยการทำให้มันบึ้มด้วยนะ)

ดังนั้นผู้ที่หลงเข้ามาอ่านจึงควรเก็บข้อมูลไว้อย่างถูกต้อง เพื่อไม่ให้แอป Force Close เพียงเพราะไปเก็บค่าไว้เป็น Singleton หรือ Static Variable เถอะนะ