31 ธันวาคม 2559

[Android Code] ทำไม Android Dev ถึงควรใช้ Parcelable มากกว่า Serializable



        นักพัฒนาแอนดรอยด์ส่วนใหญ่มักจะรู้จักกับ Parcelable มากกว่า Serializable เนอะ ซึ่งบางคนก็รู้แค่ว่าต้องใช้ Parcelable แต่ไม่รู้ว่าทำไม เพราะอะไร ดังนั้นจึงขอหยิบเรื่องนี้มาเขียนเป็นบทความต้อนรับปีใหม่ให้กับนักพัฒนาแอนดรอยด์อ่านเล่นดีกว่า~

รู้จักกับ Serializable กันแบบคร่าวๆ

        Serializable นั้นมีมาตั้งแต่สมัย Java ดั้งเดิมอยู่แล้ว มีจุดประสงค์เพื่อใช้ในการแปลง Model Class ให้อยู่ในรูปของ Byte Stream เพื่อให้สามารถรับ/ส่งข้อมูลระหว่างอุปกรณ์ได้ โดยมีเงื่อนไขว่า Model Class นั้นๆต้องเก็บข้อมูลข้างในเป็น Primitive Data Type ทั้งหมด

รู้จักกับ Parcelable กันแบบคร่าวๆ

        Parcelable ถูกเพิ่มเข้ามาเพื่อใช้งานในแอนดรอยด์ มีหน้าที่คล้ายๆกับ Serializable นั่นแหละ แต่บนแอนดรอยด์ใช้ใน Inter-process communication (IPC) ซึ่งจะคุ้นเคยกันดีเมื่อจะส่งข้อมูลที่เป็น Model Class จาก Activity ตัวหนึ่งไปให้อีกตัวหนึ่งจะต้องทำเป็น Parcelable ทุกครั้ง

        เพิ่มเติม - เรื่อง Parcelable มีคนเขียนบทความเรื่องนี้ไว้นานแล้ว เพื่อไม่ให้เนื้อหาซ้ำซ้อน (ขี้เกียจด้วยแหละ) แนะนำให้ไปอ่านได้ที่ มาเรียนรู้วิธีส่ง Object ระหว่าง Activity ให้ถูกวิธีกันเถอะ [Martroutine] ก่อนที่จะอ่านบทความนี้ต่อ

วิธีสร้าง Model Class ให้เป็น Parcelable และ Serializable

        เจ้าของบล็อกขอยกตัวอย่าง Model Class เป็นคลาสที่ชื่อว่า Post ซึ่งข้างในจะเก็บข้อมูลต่างๆในรูปแบบดังนี้

Post.java
import java.util.List;

public class Post {
    private String title;
    private String content;
    private String id;
    private String date;
    private String author;
    private String url;
    private int readCount;
    private boolean isDraft;
    private List<String> tagist;
    private List<Comment> commentList;
    
    ...

    public static class Comment {
        private String comment;
        private String user;
        private String date;

        ...
    }
}

ทำคลาสให้เป็น Serializable

        ให้คลาสทำการ Implement จาก Serializable ได้เลย รวมไปถึง Inner Class ด้วย

import java.io.Serializable;
import java.util.List;

public class Post implements Serializable {
    private String title;
    private String content;
    private String id;
    private String date;
    private String author;
    private String url;
    private int readCount;
    private boolean isDraft;
    private List<String> tagist;
    private List<Comment> commentList;

    ...

    public static class Comment implements Serializable {
        private String comment;
        private String user;
        private String date;

        ...
    }
}

        จริงๆแล้ว Model Class ตัวไหนที่เป็น Serializable ควรจะประกาศตัวแปร String ที่ชื่อว่า serialVersionUID ไว้ด้วย แต่เนื่องจากเจ้าของบล็อกแค่สร้างขึ้นมาเพื่อทดสอบการทำงานเฉยๆ ดังนั้นจึงไม่ได้ประกาศไว้

ทำคลาสให้เป็น Parcelable

        ให้คลาสทำการ Implement จาก Parcelable ได้เลย รวมไปถึง Inner Class ด้วย

import android.os.Parcelable;

import java.util.List;

public class PostParcelable implements Parcelable {
    private String title;
    private String content;
    private String id;
    private String date;
    private String author;
    private String url;
    private int readCount;
    private boolean isDraft;
    private List<String> tagist;
    private List<Comment> commentList;

    ...

    public static class Comment implements Parcelable {
        private String comment;
        private String user;
        private String date;

        ...
    }
}

        แต่ยังไม่เสร็จนะ เพราะว่าการทำเป็น Parcelable นั้นจะต้องเพิ่มคำสั่งบางอย่างเข้าไปด้วย ซึ่งต้องทำแบบนี้กับทุกคลาสที่เป็น Parcelable ซึ่งจะได้เป็นโค้ดยาวๆแบบนี้

import android.os.Parcel;
import android.os.Parcelable;

import java.util.List;

public class PostParcelable implements Parcelable {
    private String title;
    private String content;
    private String id;
    private String date;
    private String author;
    private String url;
    int readCount;
    boolean isDraft;
    List<String> tagist;
    List<Comment> commentList;

    public PostParcelable() {
    }

    ...

    protected PostParcelable(Parcel in) {
        title = in.readString();
        content = in.readString();
        id = in.readString();
        date = in.readString();
        author = in.readString();
        url = in.readString();
        readCount = in.readInt();
        isDraft = in.readByte() != 0;
        tagist = in.createStringArrayList();
    }

    public static final Creator<PostParcelable> CREATOR = new Creator<PostParcelable>() {
        @Override
        public PostParcelable createFromParcel(Parcel in) {
            return new PostParcelable(in);
        }

        @Override
        public PostParcelable[] newArray(int size) {
            return new PostParcelable[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(title);
        parcel.writeString(content);
        parcel.writeString(id);
        parcel.writeString(date);
        parcel.writeString(author);
        parcel.writeString(url);
        parcel.writeInt(readCount);
        parcel.writeByte((byte) (isDraft ? 1 : 0));
        parcel.writeStringList(tagist);
    }

    public static class Comment implements Parcelable {
        private String comment;
        private String user;
        private String date;

        public Comment() {
        }

        ...

        protected Comment(Parcel in) {
            comment = in.readString();
            user = in.readString();
            date = in.readString();
        }

        public static final Creator<Comment> CREATOR = new Creator<Comment>() {
            @Override
            public Comment createFromParcel(Parcel in) {
                return new Comment(in);
            }

            @Override
            public Comment[] newArray(int size) {
                return new Comment[size];
            }
        };

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel parcel, int i) {
            parcel.writeString(comment);
            parcel.writeString(user);
            parcel.writeString(date);
        }
    }
}

        นั่นล่ะฮะ ที่เค้าบอกว่าเวลาทำ Parcelable มันจะต้องเจอ Boilerplate Code อย่างเลี่ยงไม่ได้ เพราะต้องพิมพ์โค้ดที่มีรูปแบบเดิมๆแบบนี้ทุกครั้ง

        ถ้าไม่อยากพิมพ์เอง เจ้าของบล็อกมีวิธีง่ายๆอยู่นะ

สร้าง Parcelable ง่ายๆแบบไม่ต้องพิมพ์เอง

        เนื่องจาก Android Studio มีเครื่องมือช่วยสร้างคลาสที่เป็น Parcelable ได้อยู่แล้ว จึงไม่จำเป็นต้องพิมพ์เองทั้งหมดเสมอไป สามารถดูขั้นตอนได้จากวีดีโอนี้เลย


        หมายเหตุ - ถ้าไม่อยากทำขั้นตอนทั้งหมดนี้ เจ้าของบล็อกแนะนำว่าให้ลองใช้ไลบรารี Parceler ก็ได้นะ (สามารถดูวิธีการใช้ Parceler ได้จาก [Dev] ห่อให้ด้วย~!! แนะนำการใช้งาน Parceler Library สำหรับ Android)

แล้ว Parcelable มันดีกว่า Serializable ยังไง?

        จากบทความ มาเรียนรู้วิธีส่ง Object ระหว่าง Activity ให้ถูกวิธีกันเถอะ [Martroutine] ได้อธิบายไปแล้วว่า Serializable นั้นใช้วิธี Reflection ซึ่งรู้กันอยู่แล้วว่า Performance มันไม่ค่อยดีซักเท่าไร จึงทำให้ทีมแอนดรอยด์สร้าง Parcelable ขึ้นมาเพื่อใช้วิธีอื่นแทน แต่ก็ต้องแลกด้วย Boilerplate Code อย่างที่เห็นในตัวอย่างนั่นแหละ

        แล้วมันดีกว่าแค่ไหนล่ะ?

        นั่นสิ Performance มันดีมากถึงขนาดที่ต้องแลกกับ Boilerplate Code เลยหรอ? ดังนั้นเจ้าของบล็อกจึงลองทดสอบแบบง่ายๆดูเพื่อเทียบว่า Parcelable มันเร็วกว่า Serializable มากแค่ไหน

        เพื่อไม่ให้สับสนระหว่างคลาสที่เป็น Parcelable กับ Serializable ดังนั้นเจ้าของบล็อกจึงแยกคลาส Post ออกเป็น 2 ตัวคือ PostParcelable กับ PostSerializable โดยทั้ง 2 คลาสเก็บข้อมูลเหมือนกันทั้งหมด

        และในการทดสอบจะมีขั้นตอนดังนี้

        • สร้าง Post ขึ้นมาเพื่อทดสอบ
        • สร้าง Bundle ขึ้นมาแล้วเก็บ Post ลงใน Bundle
        • ให้ Bundle เรียกคำสั่ง writeToParcel(Parcel parcel, int i)
        • ดึงข้อมูล Post จาก Bundle ออกมา

// Parcelable
Bundle bundle = new Bundle();
bundle.putParcelable("Post", createPostParcelable());
bundle.writeToParcel(Parcel.obtain(), 0);
PostParcelable post = bundle.getParcelable("Post");

// Serializable
Bundle bundle = new Bundle();
bundle.putSerializable("Post", createPostSerializable());
bundle.writeToParcel(Parcel.obtain(), 0);
PostSerializable post = (PostSerializable) bundle.getSerializable("Post");

        สามารถดูโค้ดที่ใช้ทดสอบได้ที่ Parcelable Serializable Test [GitHub]

        เจ้าของบล็อกจะทดสอบทั้งหมด 10,000 ครั้ง เพื่อดูว่าแต่ละวิธีนั้นใช้เวลาในการทำงานเฉลี่ยเท่าไร โดยทดสอบบน 2 เครื่องดังนี้

        • Samsung Galaxy Note 5 (Android 6.0)
        • Moto X 1st Generation (Android 5.1)

        และได้ผลลัพธ์ดังนี้


        Samsung Galaxy Note 5
        • Parcelable - 0.0640 ms
        • Serializable - 0.1859 ms
        เร็วกว่า 2.90 เท่า

        Moto X 1st Generation
        • Parcelable - 0.2613 ms
        • Serializable - 0.8283 ms
        เร็วกว่า 3.16 เท่า

        ต่างกันเยอะพอสมควรเลยนะเนี่ย

สรุป 

        เดิมทีนั้น Serializable ถูกสร้างขึ้นมาเพื่อใช้งานบน Java มาตั้งแต่แรกอยู่แล้ว แต่เนื่องจากใช้ Reflection เป็นเบื้องหลังการทำงานจึงทำให้มี Performance ไม่ค่อยปลื้มมากนัก ทีมแอนดรอยด์จึงพัฒนา Parcelable ขึ้นมาเพื่อใช้งานทดแทน โดยแลกกับการเขียน Boilerplate Code ใน Model Class ทุกครั้ง (แก้ปัญหาด้วยไลบรารี Parceler ได้)

        เมื่อทดสอบความเร็วในการทำงานของ Parcelable กับ Serializable ก็พบว่าการใช้ Parcelable มีประสิทธิภาพดีกว่า Serializable อย่างเห็นได้ชัด จึงไม่แปลกใจว่าทำไมการใช้ Parcelable เพื่อส่งข้อมูลผ่าน Bundle จึงเป็น Best Practice สำหรับนักพัฒนาแอนดรอยด์

        ถึงแม้ว่าจะใช้ Serializable ได้อยู่ก็จริง แต่ก็อย่าลืมว่ามันมี Cost ที่แพงกว่า Parcelable นะ

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

        • Parcelable vs Serializable [Developer Phil]
        • มาเรียนรู้วิธีส่ง Object ระหว่าง Activity ให้ถูกวิธีกันเถอะ [Martroutine]
        • [Dev] ห่อให้ด้วย~!! แนะนำการใช้งาน Parceler Library สำหรับ Android)




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium