20 January 2017

ทำไมจึงไม่ควรเก็บข้อมูลทิ้งไว้ใน Singleton หรือ Static Variable

Updated on

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

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

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

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


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

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

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

        ที่เจ้าของบล็อกได้พูดถึง "แอปฯบึ้ม" นั้นหมายถึงแอปฯมันบึ้มจริงๆ ไม่ได้หมายถึง Life Cycle ของ Acitivty นะ

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

        ซึ่งอาการบึ้มที่เจ้าของทดสอบบ่อยๆก็คือเหล่าแอปฯที่ใช้ Memory เยอะๆนั่นเอง
        • เกมอย่าง Mobius Final Fantasy, Pokemon GO และ Assassins Creed : Identity
        • แอปอย่าง Google Photos และ Camera

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

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

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


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

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

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


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

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



        นั่นล่ะครับ อาการแอปฯบึ้ม

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

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

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

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

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

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

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

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

SingletonCurrentUser.java
public class SingletonCurrentUser {
    private static SingletonCurrentUser singletonCurrentUser;

    public static SingletonCurrentUser getInstance() {
        if (singletonCurrentUser == null) {
            singletonCurrentUser = new SingletonCurrentUser();
        }
        return singletonCurrentUser;
    }

    private String name;

    public SingletonCurrentUser() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


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

StaticCurrentUser.java
public class StaticCurrentUser {
    private static String name;

    public static String getName() {
        return name;
    }

    public static void setName(String name) {
        StaticCurrentUser.name = name;
    }
}


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

MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String KEY_NAME = "key_name";
    private String handleStateName;
    private String regularName;

    ...

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        handleStateName = savedInstanceState.getString(KEY_NAME);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(KEY_NAME, handleStateName);
    }

    ...
    
}

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


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


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

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
    ...

    private Button btnSave;
    private Button btnShow;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnSave = (Button) findViewById(R.id.btn_save);
        btnShow = (Button) findViewById(R.id.btn_show);
        btnSave.setOnClickListener(this);
        btnShow.setOnClickListener(this);
    }

    ...

    @Override
    public void onClick(View v) {
        if (v == btnSave) {
            onSaveButtonClick();
        } else if (v == btnShow) {
            onShowButtonClick();
        }
    }

    private void onSaveButtonClick() {
        String dummyName = "Akexorcist";
        SingletonCurrentUser.getInstance().setName(dummyName);
        StaticCurrentUser.setName(dummyName);
        this.regularName = dummyName;
        this.handleStateName = dummyName;
        Toast.makeText(this, "Updated", Toast.LENGTH_SHORT).show();
    }

    private void onShowButtonClick() {
        String message = "Singleton Name : " + SingletonCurrentUser.getInstance().getName() + "\n" +
                "Static Name : " + StaticCurrentUser.getName() + "\n" +
                "Regular Name : " + regularName + "\n" +
                "Handle State Name : " + handleStateName;
        AlertDialog alertDialog = new AlertDialog.Builder(this)
                .setTitle("Result")
                .setMessage(message)
                .setCancelable(true)
                .create();
        alertDialog.show();
    }
}

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

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

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


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

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

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

        ... แอปฯบึ้มเรียบร้อย ...

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


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

สรุป

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

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