"ความตั้งใจสร้างได้ทุกอย่าง ความไม่ตั้งใจทำลายได้ทุกสิ่ง"
สารพันเรื่องราวของ "Thread" บนแอนดรอยด์ การปะทะกันของ Thread, AsyncTask, AsyncTaskLoader และ IntentService
4 Dec 2014 03:11   [22896 views]

เดี๋ยวจะโดนบ่นว่า Blog Geek น้อยไป วันนี้กลับมาแบบ Geek กันไปเลยละกันสำหรับเรื่อง Thread บนแอนดรอยด์ที่มีให้เลือกใช้เยอะเหลือเกิน ทั้ง Thread, AsyncTask, AsyncTaskLoader จนถึง IntentService วันนี้จะเอามาบอกเล่าให้ฟังว่าแต่ละตัวควรใช้หรือไม่ควรใช้อย่างไร และในสถานการณ์ไหน

เริ่มจาก Basic สุด

Thread

ชาว Java คงคุ้นชินกับการใช้ Thread แบบปกติ ตามคำสั่งนี้

public void onClick(View v) {    new Thread(new Runnable() {        public void run() {            Bitmap b = loadImageFromNetwork("http://example.com/image.png");            mImageView.setImageBitmap(b);        }    }).start();}

จริงอยู่ที่วิธีนี้สามารถใช้ได้บนแอนดรอยด์ แต่ในแง่ปฏิบัติแล้ว เป็นวิธีที่ไม่แนะนำ สาเหตุเหมือนกำปั้นทุบดิน แต่สำคัญนะเออ โค้ดไม่สวย

เพราะในแง่ Thread แล้ว บ่อยครั้งที่เราจะต้องติดต่อกลับมายัง Main Thread (UI Thread) ซึ่งถ้าเราไปสั่งอัพเดต UI บน Background Thread (ใน run()) ก็จะระเบิดบู้มมมทันที วิธีที่ทำก็คือต้องครอบด้วยคำสั่ง .post(new Runnable() ... ) อีกทีหนึ่งเพื่อให้คำสั่งนั้นๆรันบน UI Thread แบบนี้

public void onClick(View v) {    new Thread(new Runnable() {        public void run() {            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");            mImageView.post(new Runnable() {                public void run() {                    mImageView.setImageBitmap(bitmap);                }            });        }    }).start();}

ซึ่ง ... ไม่สวยงะ โค้ดต้องกองๆรวมกันแบบนี้ ยิ่งพอโค้ดยาวๆนี่ลำบากลำบนมาก

เพื่อให้มันสวยงามขึ้น แอนดรอยด์จึงจัดสิ่งที่เรียกว่า Handler มาให้

Handler

เมื่อการติดต่อกลับไปยัง UI Thread นั้นไม่สวย แอนดรอยด์จึงจัดวิธี Message Queue ที่ชื่อว่า Handler (ซึ่งเป็นส่วนหนึ่งของ Looper ที่เราจะไม่พูดถึง) ในการส่งข้อมูลกลับไปยัง UI Thread ในบรรทัดเดียว แล้วพวกการอัพเดต UI ก็ไปกองๆรวมกันที่เดียว จะได้อ่านโค้ดง่ายขึ้น

private Bitmap bitmap;private final Handler handler = new Handler() {    public void handleMessage(Message msg) {        // Do something here in UI Thread        mImageView.setImageBitmap(bitmap);    }};public void onClick(View v) {    new Thread(new Runnable() {        public void run() {            bitmap = loadImageFromNetwork("http://example.com/image.png");            handler.sendEmptyMessage(0);        }    }).start();}

คราวนี้โค้ดก็จะสวยขึ้น ไม่ต้องมานั่ง new Runnable อะไรบ่อยๆ ไม่ต้องลำบาก GC นั่งคอยเก็บ แถมประโยชน์ที่พ่วงมาจากการเป็น Message Queue ก็คือความสามารถในการ Delay Message เช่นผ่านไป 5 วิ ค่อยให้ Message ไปถึงนะ ไม่ต้องทำงานทันที ทำให้เราสามารถ Apply ความสามารถตรงนี้มาทำเป็น Timer ได้อีกด้วย

นอกจากนี้ Handler ยังมีประโยชน์ในแง่อื่นๆ เพราะ Handler ยังสามารถทำงาน (handleMessage) ใน Background Thread ได้อีกด้วย พอดึงเอาความสามารถของ Message Queue มาใช้ ก็จะทำให้เกิด Background Thread แบบตั้งเวลาได้ พลิกแพลงไปได้หลายอย่าง แต่ถ้าจะทำอะไรแบบนั้น ต้องไปอ่านเรื่อง Looper ก่อนด้วยนะ ไม่งั้นจะงง หน้าตาจะเป็นแบบนี้

HandlerThread thr = new HandlerThread();thr.start();private final Handler handler = new Handler(thr.getLooper());

แต่ถึงโค้ดจะดูสวยขึ้น แต่เอาเข้าจริงก็ยังไม่สวยซะทีเดียว แค่ new Thread(new Runnable()) ก็ดูรกๆยังไงก็ไม่รู้ละ (แต่ก็ต้องใช้ ถือเป็น Best Practices แบบนึง) แถมมี Overhead ในการสร้าง Thread อีก

ด้วยเหตุนี้ AsyncTask จึงเกิดขึ้นตอน API Level 3 ...

AsyncTask

ก็ถ้าโค้ดมันไม่สวย ก็ทำแบบใหม่ แล้วจัดโครงสร้างให้มันชัดเจนไปเลยว่าตรงไหนรันบน Background Thread ตรงไหนรันบน UI Thread !

โดย AsyncTask เหมือนเครื่องจักรสายพาน โยน Input เข้าไป ระหว่างทำงานก็สามารถพ่นหา UI Thread ให้ไปอัพเดตอะไรบางอย่างได้ จนเสร็จก็เอาผลออกมา หน้าตาเป็นแบบนี้

public void onClick(View v) {    new DownloadImageTask().execute("http://example.com/image.png");}private class DownloadImageTask extends AsyncTask<String, Integer, Bitmap> {    /** The system calls this to perform work in a worker thread and      * delivers it the parameters given to AsyncTask.execute() */    protected Bitmap doInBackground(String... urls) {        // Call publishProgress(percent) to trig onProgressUpdate;        return loadImageFromNetwork(urls[0]);    }    protected void onProgressUpdate(Integer... progress) {    }        /** The system calls this to perform work in the UI thread and delivers      * the result from doInBackground() */    protected void onPostExecute(Bitmap result) {        mImageView.setImageBitmap(result);    }}

ใน doInBackground ก็จะทำใน Background Thread ไป ถ้าระหว่างนั้นอยากจะส่งให้ UI Thread ทำอะไร ก็ยิงผ่านคำสั่ง publishProgress แล้วมันจะเรียก onProgressUpdate ซึ่งทำงานบน UI Thread อีกทีนึง

ประโยชน์ใหญ่ๆของการใช้งาน AsyncTask ที่ใช้กันโดยแพร่หลายคือการทำงานแบบให้เราเห็น Progress เช่นตอนเราโหลดไฟล์อะไรบางอย่าง แล้วเราเห็น Progress Bar วิ่งๆว่ากี่ % แล้ว การทำงานจริงๆเราก็ต้องไปโหลดใน Background Thread แล้วยิงส่งมายัง UI Thread เพื่ออัพเดต Progress Bar นั่นเอง

AsyncTask มีการใช้งานเยอะมาก เรียกได้ว่าทุกแอพฯใช้ AsyncTask แน่นอน เพราะเป็น Best Practices มาช้านาน

แต่ AsyncTask ที่เกิดมาหลายปีแล้ว ก็ถือว่าเก่าพอสมควร ไม่แปลกที่จะมีปัญหาที่คาดไม่ถึงอยู่มากเหมือนกัน ดังนี้


- AsyncTask จะล็อคพวกตัวแปรที่เราอาจจะมีเรียกใช้บน UI Thread ไว้ (เช่นตัวอย่างข้างบนคือ mImageView) ซึ่งส่งผลให้ Activity จะไม่ถูกทำลายโดยสมบูรณ์จนกว่า AsyncTask นั้นๆจะทำงานเสร็จ (Garbage Collectors' rule) ดังนั้นจึงอาจเกิด Memory Leak ได้ ดังนั้น การใช้ AsyncTask จึงเหมาะกับ Task เล็กๆสั้นๆ ห้ามให้ Task นั้นๆรันนานเกิน 10 วินาที ไม่งั้นอาจมีปัญหาสะสมจน Crash ได้ (ก็มีวิธีแก้คือถ้าเกิด Activity ถูกทำลาย ก็สั่งทำลาย AsyncTask นั้นๆทิ้งด้วย แต่บางทีมันก็ไม่ถูกทำลายนี่สิ)

- ปัญหาตอน Configuration Change (เช่นหมุนหน้าจอ) อาจมีบางครั้งที่ถ้า Handle ไม่ดี แล้ว Thread จะตายไปเลย เราต้องเริ่มต้น Thread ใหม่ หรือถ้าไม่เข้าใจการทำงาน แอพฯก็ Crash ไปเลย

- ทำงานได้ทีละ Task เพราะ AsyncTask หลายๆตัวไม่สามารถทำงานพร้อมกันได้ (หรือจะให้ถูกคือ บน Android แต่ละเวอร์ชั่นทำงานไม่เหมือนกัน บางรุ่นก็ได้ บางรุ่นก็ไม่ได้ สุดท้ายต้องแฮคเพื่อรันบน Thread Pool ถึงจะทำงานแบบรันพร้อมกันได้บนทุกรุ่นอย่างไม่มีปัญหา หรือถ้าเป็น Android 3.0 ขึ้นไป ก็ใช้ executeOnExecutor โลด -- จึงไม่ใช่ปัญหามาก ถือว่ามีทางแก้ไขเป็นชิ้นเป็นอัน)


ก็มี Workaround เพื่อแก้ปัญหาข้างต้นกันอยู่บ้าง เช่นการใส่ใน Fragment ที่ setRetainInstance(true) ไว้ ตอนหมุนจอก็จะไม่มีปัญหาเพราะ Fragment ไม่ได้ถูกทำลาย

แต่โดยรวมแล้ว ต้องยอมรับว่า AsyncTask ไม่ใช่สิ่งที่ดีเท่าไหร่นัก แต่ก็พอฝืนใช้กันไปได้ เจอปัญหาก็แก้กันไป

แต่แรก AsyncTask ถูกออกแบบมาเพื่อให้ทำงานควบคู่กันไปกับ UI ทำไป อัพเดต UI ไป แล้วแป๊บๆก็เสร็จ (มันถึงชื่อ AsyncTask ไง Task ที่ทำงานคู่กันไปแบบ Asynchronous) แต่พอเอาเข้าจริง มันไม่มี Thread ดีๆตัวอื่นให้ใช้ สรุปทุกคนก็ใช้ AsyncTask แทน Thread ทุกชนิดในโลกกันไป Task จะสั้นจะยาวยังไงก็ไม่สน ยังไงก็จะใช้ AsyncTask ผลคือแอพฯมีปัญหากันกระหน่ำจากความไม่เข้าใจปัญหาข้างต้น

แต่จะทำยังไงได้ ก็โลกมันมาทางนี้แล้ว ... ก็มี Workaround ออกมาหลายๆแบบ เช่น


- ถ้าต้องการทำงานสั้นๆ - ใช้ไปเลย ไม่ต้องคิด Memory ไม่ทันจะ Leak เดี๋ยว AsyncTask ก็ทำงานจบแล้ว อย่าไปซีเรียส

- ถ้าต้องการทำงานยาวๆ แต่หยุดงานได้ถ้า Activity ถูกทำลาย (เช่นกด Back) - อันนี้ให้ไปดักใน onDestroy แล้วสั่ง cancel AsyncTask ในนั้นซะ (ซึ่งไม่ชัวร์ว่าจะ Cancel สำเร็จ)

- ถ้าต้องการทำงานยาวๆ และงานจะต้องเสร็จ - ให้มั่นใจว่าไม่มีการเรียกใช้ตัวแปร UI ของ Activity ไม่งั้นจะโดนล็อคหมู่ เกิดเป็น Memory Leak ถ้าเกิดไม่มีเรียกใช้อยู่แล้วก็ไม่ต้องทำอะไร ไม่ต้องไป Cancel มัน เดี๋ยวมันทำงานเสร็จก็จบๆไป แต่ในกรณีที่มีการอัพเดต UI หลังจาก AsyncTask ทำงานเสร็จหรือในตอนอัพเดต Progress ก็ต้องหลีกเลี่ยงการเรียกตัวแปรโดยตรง ต้องใช้อะไรบางอย่างคั่นกลาง เช่น Broadcast Receiver หรือ Otto (เป็นการฝืนและทำให้ยุ่งยากขึ้นมาก แต่ถ้าคิดจะใช้ AsyncTask แบบนี้ ก็ต้องใช้วิธีนี้)


อย่างไรก็ตาม AsyncTask ก็จะเป็นสิ่งที่ถูกใช้เยอะที่สุดอยู่ดี no matter what ดังนั้น ... ทำตัวให้ชินกับมัน เข้าใจมันให้ดีอย่างถ่องแท้เถิดจะเกิดผล

และหลังจากที่ Honeycomb (Android 3.0) ออกมา ก็มีตัวมาพยายามแทน AsyncTask ในแนวคิดเดียวกันแล้ว (แต่ไม่ค่อยมีใครใช้) ... มันคือ ... Loader

หรือถ้าจะให้ตรงงานกับที่เราพูดถึงอยู่ตอนนี้ก็คงเป็น AsyncTaskLoader

AsyncTaskLoader

AsyncTaskLoader เป็นชุดคำสั่งในตระกูล Loader ซึ่งออกแบบมาด้วยแนวคิดคล้ายๆกับ AsyncTask คือ การทำงานคู่กับ UI แต่ความพิเศษของมันคือ เวลา Activity ถูกทำลาย มันจะถูกทำลายไปเองโดยอัตโนมัติด้วยเช่นกัน

วิธีการเรียกจึงมีรูปแบบเฉพาะของมันผ่าน LoaderManager แบบนี้ (แนวคิดการเรียกจะคล้ายๆ Fragment คือการทำผ่าน Manager ให้ระบบ Manage ให้)

    private class TestLoader extends AsyncTaskLoader<String> {        public TestLoader(Context context) {            super(context);        }        @Override        public String loadInBackground() {            // Do something            return "Data: " + System.currentTimeMillis();        }        @Override        public void deliverResult(String data) {            if (isReset()) {                // The Loader has been reset; ignore the result and invalidate the data.                return;            }            if (isStarted()) {                super.deliverResult(data);            }        }    }
        // Call in Activity or Fragment        getSupportLoaderManager().initLoader(1, null, new LoaderManager.LoaderCallbacks() {            @Override            public Loader onCreateLoader(int i, Bundle bundle) {                return new TestLoader(MainActivity.this);            }            @Override            public void onLoadFinished(Loader objectLoader, String result) {                // Use result here            }            @Override            public void onLoaderReset(Loader objectLoader) {            }        });

สังเกตดู หน้าตาคล้าย AsyncTask มาก แต่ก็เปลี่ยนหน้าตาไปนิดหน่อย ข้อดีของ AsyncTaskLoader คือ

- ถูกทำลายทิ้งทันทีที่ Activity หรือ Fragment ที่เรียกถูกทำลาย

- เพราะมันทำงานผ่าน Manager ตอน Configuration Change (เช่นหมุนจอ) ตัว AsyncTaskLoader จะไม่ถูกทำลายไปด้วย การ Handle จึงง่ายขึ้นมาก

แต่ข้อจำกัดก็มีคือ

- เรียกได้จาก Activity หรือ Fragment เท่านั้น (เพราะมันออกแบบมาเพื่อการนี้)

- เนื่องจากมันถูกทำลายทิ้งทันทีที่ Activity/Fragment ถูกทำลาย มันจึงไม่เหมาะกับงานที่ต้องการทำให้เสร็จแน่ๆ (เช่นส่ง Request ไป Server แบบหวังผล)

- ไม่มีช่องทางการติดต่อกลับระหว่าง Background Thread ทำงาน ถ้าจะติดต่อกลับต้องยิงผ่าน Broadcast Receiver หรือ Otto กลับเอาเอง

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

และมันก็ยังไม่หมดแค่นั้น ... ศึก Thread ยังไม่จบ เพราะยังมี Candidate อีกตัวนามว่า IntentService ที่สำคัญไม่แพ้กัน

IntentService

IntentService เป็นสิ่งที่ค่อนข้างแหวกแนวกว่าชาวบ้านเค้า ด้วยแนวคิด "ยิง Intent เพื่อสร้าง Background Task"

หน้าตาของ IntentService จะเป็นประมาณนี้

public class RSSPullService extends IntentService {    @Override    protected void onHandleIntent(Intent workIntent) {        // Gets data from the incoming Intent        String dataString = workIntent.getDataString();        ...        // Do work here, based on the contents of dataString        ...    }}

และต้องประกาศใน AndroidManifest.xml ด้วย

<service android:name=".RSSPullService"         android:exported="false"/>

เวลาเรียกใช้ ก็เรียกผ่าน Intent ได้เลยแบบนี้

mServiceIntent = new Intent(getActivity(), RSSPullService.class);mServiceIntent.setData(Uri.parse(dataUrl));getActivity().startService(mServiceIntent);

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

ดังนั้นจึงไม่มีวิธีติดต่อกลับไปยัง UI Thread หรือผู้เรียกอย่างเป็นทางการ แต่ถ้าจะติดต่อกลับมาก็ทำได้เช่นกัน ด้วย Broadcast Receiver หรือ Otto นั่นเอง (Otto จงเจริญญญ)

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

ดังนั้นมันไม่เกิดมาเพื่อเป็น Thread ที่ทำงานพร้อมกันหลายๆตัวโดยสมบูรณ์แบบ แต่มันเกิดมาเพื่อเป็น Worker Queue นั่นเอง

สรุป

AsyncTask, AsyncTaskLoader และ IntentService ถึงจะเป็น Thread เหมือนกัน แต่ล้วนมีนิสัยการทำงานที่ต่างกัน ต้องเลือกใช้ให้ถูกตัว

แต่ถามว่าตัวไหนที่ใช้แล้วครอบคลุมที่สุด? ... ก็ยังหนีไม่พ้น AsyncTask อีกนั่นแหละ

ดังนั้น ... จงทำความเข้าใจ AsyncTask ให้ดีครับ ไปๆมาๆ Combination ของ AsyncTask กับ Otto ก็เป็นตัวเลือกที่ดี ถึงแม้จะน่าหงุดหงิดก็ตามเพราะ AsyncTask ไม่ได้เกิดมาเพื่องานยาวๆ แต่มันไม่มีวิธีอื่นที่จะรันหลายๆ Thread พร้อมกันโดยไม่มีปัญหาอีกแล้วนอกจากวิธีนี้ คราวนี้งานยาวงานสั้นก็จะกลายไปตกอยู่ที่ AsyncTask หมดอย่างหลีกเลี่ยงไม่ได้

แล้วก็อย่าลืมศึกษาวิธีการใช้ AsyncTask ให้ไม่เกิดปัญหากับการหมุนจอด้วย เป็นปัญหา Common ที่เจอกันเยอะมาก (ข้อแนะนำคือยัด AsyncTask ไว้ใน Fragment แล้วสั่ง setRetainInstance เป็น true จากนั้น AsyncTask จะน่ารักขึ้นมาทันที น่าจะเป็น Best Practices ละ ... Fragment จงเจริญญญญ เย้ เย)

หรืออีกอันที่ครอบคลุมไม่แพ้กันก็คงเป็น Thread ธรรมดาดื้อๆ แต่ไม่ค่อยแนะนำเท่าไหร่ เพราะมันไม่สวย และต้อง Manage Thread เอง ให้ AsyncTask ไปจัดการด้วย Thread Pool ให้ดีกว่า

ก็ตามนี้ครับ =)


จบจ้า ใครอ่านรู้เรื่องถือว่าสอบผ่านนน ปิ๊งป่องงง

บทความที่เกี่ยวข้อง

Dec 13, 2014, 14:48
13627 views
[Tips] วิธีง่ายๆในการเพิ่มความเร็วการ Build แอพฯบน Android Studio
Dec 3, 2014, 17:03
5837 views
บันทึกการสอนแอนดรอยด์สองคลาสแรก จุดเริ่มต้นของความฝัน =)
0 Comment(s)
Loading