Android Threading: ทั้งหมดที่คุณต้องรู้
เผยแพร่แล้ว: 2022-03-11นักพัฒนา Android ทุกคนต้องจัดการกับเธรดในแอปพลิเคชันของตนไม่ทางใดก็ทางหนึ่ง
เมื่อเปิดตัวแอปพลิเคชันใน Android จะสร้างเธรดแรกของการดำเนินการที่เรียกว่าเธรด "หลัก" เธรดหลักมีหน้าที่รับผิดชอบในการส่งเหตุการณ์ไปยังวิดเจ็ตอินเทอร์เฟซผู้ใช้ที่เหมาะสม รวมถึงการสื่อสารกับส่วนประกอบจากชุดเครื่องมือ Android UI
เพื่อให้แอปพลิเคชันของคุณตอบสนองได้ คุณจำเป็นต้องหลีกเลี่ยงการใช้เธรดหลักเพื่อดำเนินการใดๆ ที่อาจทำให้ถูกบล็อกได้
การทำงานของเครือข่ายและการเรียกฐานข้อมูล รวมถึงการโหลดส่วนประกอบบางอย่าง เป็นตัวอย่างทั่วไปของการดำเนินการที่ควรหลีกเลี่ยงในเธรดหลัก เมื่อถูกเรียกในเธรดหลัก พวกเขาจะถูกเรียกแบบซิงโครนัส ซึ่งหมายความว่า UI จะไม่ตอบสนองอย่างสมบูรณ์จนกว่าการดำเนินการจะเสร็จสิ้น ด้วยเหตุผลนี้ จึงมักจะดำเนินการในเธรดแยกกัน ซึ่งจะช่วยหลีกเลี่ยงการบล็อก UI ในขณะที่ดำเนินการ (กล่าวคือ จะดำเนินการแบบอะซิงโครนัสจาก UI)
Android มีหลายวิธีในการสร้างและจัดการเธรด และมีไลบรารีของบุคคลที่สามมากมายที่ทำให้การจัดการเธรดน่าพึงพอใจยิ่งขึ้น อย่างไรก็ตาม ด้วยวิธีการต่างๆ ที่มีอยู่มากมาย การเลือกวิธีที่ถูกต้องอาจทำให้สับสนได้
ในบทความนี้ คุณจะได้เรียนรู้เกี่ยวกับสถานการณ์ทั่วไปบางอย่างในการพัฒนา Android ที่เธรดกลายเป็นสิ่งจำเป็นและวิธีแก้ปัญหาง่ายๆ บางอย่างที่สามารถนำไปใช้กับสถานการณ์เหล่านั้น และอื่นๆ
เธรดใน Android
ใน Android คุณสามารถจัดหมวดหมู่ส่วนประกอบเธรดทั้งหมดเป็นสองหมวดหมู่พื้นฐาน:
- เธรด ที่ แนบกับกิจกรรม/ส่วนย่อย: เธรดเหล่านี้เชื่อมโยงกับวงจรชีวิตของกิจกรรม/ส่วนย่อย และจะสิ้นสุดลงทันทีที่กิจกรรม/ส่วนย่อยถูกทำลาย
- เธรดที่ ไม่ได้ แนบกับกิจกรรม/ส่วนย่อยใดๆ: เธรดเหล่านี้สามารถทำงานต่อไปได้เกินอายุของกิจกรรม/ส่วนย่อย (ถ้ามี) ที่เกิด
ส่วนประกอบเกลียวที่ยึดติดกับกิจกรรม/ชิ้นส่วน
AsyncTask
AsyncTask
เป็นส่วนประกอบ Android พื้นฐานที่สุดสำหรับการทำเธรด ใช้งานง่ายและดีสำหรับสถานการณ์พื้นฐาน
การใช้ตัวอย่าง:
public class ExampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new MyTask().execute(url); } private class MyTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... params) { String url = params[0]; return doSomeWork(url); } @Override protected void onPostExecute(String result) { super.onPostExecute(result); // do something with result } } }
อย่างไรก็ตาม AsyncTask
นั้นสั้น หากคุณต้องการให้งานที่รอการตัดบัญชีของคุณทำงานเกินอายุของกิจกรรม/ส่วนย่อย เป็นที่น่าสังเกตว่าแม้บางสิ่งที่ง่ายอย่างการหมุนหน้าจอก็อาจทำให้กิจกรรมถูกทำลายได้
รถตัก
รถตักเป็นวิธีการแก้ปัญหาที่กล่าวถึงข้างต้น ตัวโหลดสามารถหยุดโดยอัตโนมัติเมื่อกิจกรรมถูกทำลาย และสามารถรีสตาร์ทตัวเองได้หลังจากกิจกรรมถูกสร้างขึ้นใหม่
ตัวโหลดมีสองประเภทหลัก: AsyncTaskLoader
และ CursorLoader
คุณจะได้เรียนรู้เพิ่มเติมเกี่ยวกับ CursorLoader
ในบทความนี้
AsyncTaskLoader
นั้นคล้ายกับ AsyncTask
แต่ซับซ้อนกว่าเล็กน้อย
การใช้ตัวอย่าง:
public class ExampleActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getLoaderManager().initLoader(1, null, new MyLoaderCallbacks()); } private class MyLoaderCallbacks implements LoaderManager.LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { return new MyLoader(ExampleActivity.this); } @Override public void onLoadFinished(Loader loader, Object data) { } @Override public void onLoaderReset(Loader loader) { } } private class MyLoader extends AsyncTaskLoader { public MyLoader(Context context) { super(context); } @Override public Object loadInBackground() { return someWorkToDo(); } } }
ส่วนประกอบเกลียวที่ไม่ยึดติดกับกิจกรรม/ชิ้นส่วน
บริการ
Service
เป็นส่วนประกอบที่มีประโยชน์สำหรับการดำเนินการที่ยาวนาน (หรืออาจยาวนาน) โดยไม่มี UI
Service
ทำงานในเธรดหลักของกระบวนการโฮสต์ บริการไม่ได้สร้างเธรดของตัวเองและไม่ได้ทำงานในกระบวนการแยกต่างหาก เว้นแต่คุณจะระบุไว้เป็นอย่างอื่น
การใช้ตัวอย่าง:
public class ExampleService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { doSomeLongProccesingWork(); stopSelf(); return START_NOT_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } }
ด้วย Service
เป็นความรับผิดชอบ ของคุณ ที่จะต้องหยุดการทำงานเมื่องานเสร็จสมบูรณ์โดยเรียกใช้ stopSelf()
หรือ stopService()
IntentService
เช่นเดียวกับ Service
IntentService
จะทำงานบนเธรดที่แยกจากกัน และหยุดทำงานโดยอัตโนมัติหลังจากเสร็จสิ้นการทำงาน
โดยปกติแล้ว IntentService
จะใช้สำหรับงานสั้นๆ ที่ไม่จำเป็นต้องแนบกับ UI ใดๆ
การใช้ตัวอย่าง:
public class ExampleService extends IntentService { public ExampleService() { super("ExampleService"); } @Override protected void onHandleIntent(Intent intent) { doSomeShortWork(); } }
รูปแบบเกลียวทั้งเจ็ดใน Android
ใช้กรณีที่ 1: การขอผ่านเครือข่ายโดยไม่ต้องมีการตอบสนองจากเซิร์ฟเวอร์
บางครั้งคุณอาจต้องการส่งคำขอ API ไปยังเซิร์ฟเวอร์โดยไม่ต้องกังวลกับการตอบกลับ ตัวอย่างเช่น คุณอาจส่งโทเค็นการลงทะเบียนแบบพุชไปยังส่วนหลังของแอปพลิเคชันของคุณ
เนื่องจากสิ่งนี้เกี่ยวข้องกับการขอผ่านเครือข่าย คุณควรทำจากเธรดอื่นที่ไม่ใช่เธรดหลัก
ตัวเลือกที่ 1: AsyncTask หรือ loaders
คุณสามารถใช้ AsyncTask
หรือตัวโหลดเพื่อโทรออก และจะใช้งานได้
อย่างไรก็ตาม AsyncTask
และตัวโหลดขึ้นอยู่กับวงจรชีวิตของกิจกรรม ซึ่งหมายความว่าคุณจะต้องรอให้การโทรดำเนินการและพยายามป้องกันไม่ให้ผู้ใช้ออกจากกิจกรรม หรือหวังว่าจะดำเนินการก่อนที่กิจกรรมจะถูกทำลาย
ตัวเลือกที่ 2: บริการ
Service
อาจเหมาะสมกว่าสำหรับกรณีการใช้งานนี้ เนื่องจากไม่ได้เชื่อมโยงกับกิจกรรมใดๆ จึงสามารถโทรผ่านเครือข่ายต่อไปได้แม้กิจกรรมจะถูกทำลายไปแล้ว นอกจากนี้ เนื่องจากไม่ต้องการการตอบสนองจากเซิร์ฟเวอร์ บริการจะไม่ถูกจำกัดที่นี่เช่นกัน
อย่างไรก็ตาม เนื่องจากบริการจะเริ่มทำงานบนเธรด UI คุณยังต้องจัดการเธรดด้วยตัวเอง คุณจะต้องตรวจสอบให้แน่ใจด้วยว่าบริการจะหยุดลงเมื่อการโทรผ่านเครือข่ายเสร็จสิ้น
การดำเนินการนี้จะต้องใช้ความพยายามมากกว่าที่ควรจะเป็นสำหรับการดำเนินการง่ายๆ
ตัวเลือก 3: IntentService
ในความคิดของฉัน นี่จะเป็นตัวเลือกที่ดีที่สุด
เนื่องจาก IntentService
ไม่ได้เชื่อมโยงกับกิจกรรมใดๆ และทำงานบนเธรดที่ไม่ใช่ UI จึงตอบสนองความต้องการของเราได้อย่างสมบูรณ์แบบที่นี่ นอกจากนี้ IntentService
จะหยุดทำงานโดยอัตโนมัติ ดังนั้นจึงไม่จำเป็นต้องจัดการด้วยตนเองเช่นกัน
ใช้กรณีที่ 2: การโทรผ่านเครือข่ายและรับการตอบกลับจากเซิร์ฟเวอร์
กรณีการใช้งานนี้น่าจะเกิดขึ้นได้บ่อยขึ้นเล็กน้อย ตัวอย่างเช่น คุณอาจต้องการเรียกใช้ API ในส่วนแบ็คเอนด์และใช้การตอบสนองเพื่อเติมฟิลด์บนหน้าจอ
ตัวเลือกที่ 1: บริการหรือ IntentService
แม้ว่า Service
หรือ IntentService
ได้ดีสำหรับกรณีการใช้งานก่อนหน้านี้ แต่การใช้สิ่งเหล่านี้ที่นี่ไม่ใช่ความคิดที่ดี การพยายามดึงข้อมูลออกจาก Service
หรือ IntentService
ไปยังเธรด UI หลักจะทำให้สิ่งต่าง ๆ ซับซ้อนมาก
ตัวเลือกที่ 2: AsyncTask หรือ loaders
ในตอนแรกอาย AsyncTask
หรือ loaders ดูเหมือนจะเป็นวิธีแก้ปัญหาที่ชัดเจนที่นี่ ใช้งานง่าย—เรียบง่ายและตรงไปตรงมา
อย่างไรก็ตาม เมื่อใช้ AsyncTask
หรือ loaders คุณจะสังเกตเห็นว่ามีความจำเป็นต้องเขียนโค้ดสำเร็จรูป นอกจากนี้ การจัดการข้อผิดพลาดกลายเป็นงานที่น่าเบื่อสำหรับส่วนประกอบเหล่านี้ แม้แต่กับการโทรแบบเครือข่ายธรรมดา คุณยังต้องตระหนักถึงข้อยกเว้นที่อาจเกิดขึ้น จับมัน และดำเนินการตามนั้น สิ่งนี้บังคับให้เรารวมการตอบสนองของเราในคลาสที่กำหนดเองซึ่งมีข้อมูล พร้อมข้อมูลข้อผิดพลาดที่เป็นไปได้ และแฟล็กระบุว่าการดำเนินการสำเร็จหรือไม่
นั่นเป็นงานที่ต้องทำมากสำหรับการโทรแต่ละครั้ง โชคดีที่ตอนนี้มีวิธีแก้ปัญหาที่ดีกว่าและง่ายกว่ามาก: RxJava
ตัวเลือกที่ 3: RxJava
คุณอาจเคยได้ยินเกี่ยวกับ RxJava ห้องสมุดที่พัฒนาโดย Netflix เกือบจะเป็นเรื่องมหัศจรรย์ใน Java
RxAndroid ให้คุณใช้ RxJava ใน Android และทำให้การจัดการงานอะซิงโครนัสเป็นเรื่องง่าย คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ RxJava บน Android ได้ที่นี่
RxJava จัดเตรียมสององค์ประกอบ: Observer
และ Subscriber
ผู้สังเกตการณ์ เป็นองค์ประกอบที่มีการกระทำบางอย่าง จะดำเนินการดังกล่าวและส่งคืนผลลัพธ์หากสำเร็จหรือเกิดข้อผิดพลาดหากล้มเหลว
ในทางกลับกัน Subscriber เป็นองค์ประกอบที่สามารถรับผลลัพธ์ (หรือข้อผิดพลาด) จากสิ่งที่สังเกตได้ โดยสมัครรับข้อมูล
ด้วย RxJava คุณต้องสร้างสิ่งที่สังเกตได้ก่อน:
Observable.create((ObservableOnSubscribe<Data>) e -> { Data data = mRestApi.getData(); e.onNext(data); })
เมื่อสร้างการสังเกตได้แล้ว คุณสามารถสมัครรับข้อมูลได้
ด้วยไลบรารี RxAndroid คุณสามารถควบคุมเธรดที่คุณต้องการดำเนินการในสิ่งที่สังเกตได้ และเธรดที่คุณต้องการรับการตอบสนอง (เช่น ผลลัพธ์หรือข้อผิดพลาด)
คุณเชื่อมโยงสิ่งที่สังเกตได้กับสองหน้าที่เหล่านี้:
.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()
ตัวจัดกำหนดการเป็นส่วนประกอบที่ดำเนินการกับเธรดบางตัว AndroidSchedulers.mainThread()
เป็นตัวกำหนดตารางเวลาที่เกี่ยวข้องกับเธรดหลัก
ระบุว่าการเรียก API ของเราคือ mRestApi.getData()
และส่งคืนอ็อบเจ็กต์ Data
การเรียกพื้นฐานอาจมีลักษณะดังนี้:
Observable.create((ObservableOnSubscribe<Data>) e -> { try { Data data = mRestApi.getData(); e.onNext(data); } catch (Exception ex) { e.onError(ex); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(match -> Log.i(“rest api, "success"), throwable -> Log.e(“rest api, "error: %s" + throwable.getMessage()));
คุณจะเห็นได้ว่า RxJava ช่วยให้เราเขียนโค้ดที่ครบถ้วนสมบูรณ์มากขึ้นได้อย่างไร โดยไม่ต้องใช้ RxJava ในด้านอื่นๆ ด้วยซ้ำ โดยที่ไม่ต้องพูดถึงความซับซ้อนของการทำเธรด
ใช้กรณีที่ 3: การต่อสายเครือข่าย
สำหรับการเรียกเครือข่ายที่ต้องดำเนินการตามลำดับ (เช่น โดยที่การดำเนินการแต่ละครั้งขึ้นอยู่กับการตอบสนอง/ผลลัพธ์ของการดำเนินการครั้งก่อน) คุณต้องระมัดระวังเป็นพิเศษเกี่ยวกับการสร้างรหัสปาเก็ตตี้
ตัวอย่างเช่น คุณอาจต้องทำการเรียก API ด้วยโทเค็นที่คุณต้องการดึงข้อมูลก่อนผ่านการเรียก API อื่น
ตัวเลือกที่ 1: AsyncTask หรือ loaders
การใช้ AsyncTask
หรือตัวโหลดจะนำไปสู่รหัสปาเก็ตตี้อย่างแน่นอน ฟังก์ชันการทำงานโดยรวมจะเป็นเรื่องยากที่จะทำให้ถูกต้อง และจะต้องใช้รหัสสำเร็จรูปที่ซ้ำซ้อนจำนวนมากตลอดโครงการของคุณ
ตัวเลือกที่ 2: RxJava โดยใช้ flatMap
ใน RxJava ตัวดำเนินการ flatMap
จะใช้ค่าที่ปล่อยออกมาจากแหล่งที่มาที่สังเกตได้และส่งกลับค่าที่สังเกตได้อื่น คุณสามารถสร้างสิ่งที่สังเกตได้ แล้วสร้างสิ่งที่สังเกตได้อีกอย่างหนึ่งโดยใช้ค่าที่ปล่อยออกมาจากค่าแรก ซึ่งโดยทั่วไปจะโยงค่าเหล่านี้
ขั้นตอนที่ 1 สร้างสิ่งที่สังเกตได้ซึ่งดึงโทเค็น:
public Observable<String> getTokenObservable() { return Observable.create(subscriber -> { try { String token = mRestApi.getToken(); subscriber.onNext(token); } catch (IOException e) { subscriber.onError(e); } }); }
ขั้นตอนที่ 2 สร้างสิ่งที่สังเกตได้ซึ่งรับข้อมูลโดยใช้โทเค็น:
public Observable<String> getDataObservable(String token) { return Observable.create(subscriber -> { try { Data data = mRestApi.getData(token); subscriber.onNext(data); } catch (IOException e) { subscriber.onError(e); } }); }
ขั้นตอนที่ 3 เชื่อมโยงทั้งสองสิ่งที่สังเกตได้เข้าด้วยกันและสมัคร:
getTokenObservable() .flatMap(new Function<String, Observable<Data>>() { @Override public Observable<Data> apply(String token) throws Exception { return getDataObservable(token); } }) .subscribe(data -> { doSomethingWithData(data) }, error -> handleError(e));
โปรดทราบว่าการใช้วิธีการนี้ไม่จำกัดเฉพาะการโทรในเครือข่าย มันสามารถทำงานกับชุดของการกระทำใดๆ ที่จำเป็นต้องรันในลำดับแต่ในเธรดที่แยกจากกัน
กรณีการใช้งานทั้งหมดข้างต้นค่อนข้างง่าย การสลับไปมาระหว่างเธรดจะเกิดขึ้นหลังจากแต่ละเธรดเสร็จสิ้นเท่านั้น สถานการณ์ขั้นสูงมากขึ้น—ตัวอย่างเช่น ที่ซึ่งเธรดตั้งแต่สองเธรดขึ้นไปจำเป็นต้องสื่อสารกันอย่างแข็งขัน—สามารถได้รับการสนับสนุนโดยวิธีการนี้เช่นกัน
ใช้กรณีที่ 4: สื่อสารกับเธรด UI จากเธรดอื่น
พิจารณาสถานการณ์ที่คุณต้องการอัปโหลดไฟล์และอัปเดตอินเทอร์เฟซผู้ใช้เมื่อเสร็จสิ้น
เนื่องจากการอัปโหลดไฟล์อาจใช้เวลานาน จึงไม่จำเป็นต้องให้ผู้ใช้รอ คุณสามารถใช้บริการ และอาจเป็น IntentService
เพื่อใช้งานฟังก์ชันที่นี่
อย่างไรก็ตาม ในกรณีนี้ ความท้าทายที่ใหญ่กว่าคือการเรียกใช้เมธอดบนเธรด UI หลังจากการอัปโหลดไฟล์ (ซึ่งดำเนินการในเธรดแยกต่างหาก) เสร็จสมบูรณ์
ตัวเลือกที่ 1: RxJava ภายในบริการ
RxJava ไม่ว่าจะด้วยตัวมันเองหรือใน IntentService
อาจไม่เหมาะ คุณจะต้องใช้กลไกการเรียกกลับเมื่อสมัครรับ Observable
และ IntentService
สร้างขึ้นเพื่อทำการโทรแบบซิงโครนัสอย่างง่าย ไม่ใช่การเรียกกลับ

ในทางกลับกัน ด้วย Service
คุณจะต้องหยุดบริการด้วยตนเอง ซึ่งต้องใช้การทำงานมากขึ้น
ตัวเลือก 2: BroadcastReceiver
Android มีส่วนประกอบนี้ ซึ่งสามารถฟังเหตุการณ์ทั่วโลก (เช่น เหตุการณ์แบตเตอรี่ เหตุการณ์ในเครือข่าย ฯลฯ) รวมถึงเหตุการณ์ที่กำหนดเอง คุณสามารถใช้องค์ประกอบนี้เพื่อสร้างเหตุการณ์ที่กำหนดเองซึ่งจะถูกทริกเกอร์เมื่อการอัปโหลดเสร็จสิ้น
ในการทำเช่นนี้ คุณต้องสร้างคลาสแบบกำหนดเองที่ขยาย BroadcastReceiver
ลงทะเบียนในรายการ และใช้ Intent
และ IntentFilter
เพื่อสร้างเหตุการณ์ที่กำหนดเอง หากต้องการทริกเกอร์เหตุการณ์ คุณจะต้องใช้เมธอด sendBroadcast
ประจักษ์:
<receiver android:name="UploadReceiver"> <intent-filter> <action android:name="com.example.upload"> </action> </intent-filter> </receiver>
ผู้รับ:
public class UploadReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getBoolean(“success”, false) { Activity activity = (Activity)context; activity.updateUI(); } }
ผู้ส่ง:
Intent intent = new Intent(); intent.setAction("com.example.upload"); sendBroadcast(intent);
แนวทางนี้เป็นทางเลือกที่ได้ผล แต่อย่างที่คุณสังเกตเห็น มันเกี่ยวข้องกับงานบางอย่าง และการออกอากาศมากเกินไปอาจทำให้สิ่งต่างๆ ช้าลงได้
ตัวเลือก 3: การใช้ตัวจัดการ
ตัว Handler
เป็นส่วนประกอบที่สามารถแนบไปกับเธรด และทำขึ้นเพื่อดำเนินการบางอย่างกับเธรดนั้นผ่านข้อความธรรมดาหรืองานที่สามารถ Runnable
ได้ มันทำงานร่วมกับองค์ประกอบอื่นคือ Looper
ซึ่งรับผิดชอบการประมวลผลข้อความในเธรดเฉพาะ
เมื่อ Handler
ถูกสร้างขึ้น มันสามารถรับอ็อบเจ็กต์ Looper
ในตัวสร้าง ซึ่งระบุว่าเธรดใดที่ตัวจัดการนั้นเชื่อมต่ออยู่ หากคุณต้องการใช้ตัวจัดการที่แนบมากับเธรดหลัก คุณต้องใช้ looper ที่เชื่อมโยงกับเธรดหลักโดยเรียก Looper.getMainLooper()
ในกรณีนี้ ในการอัปเดต UI จากเธรดพื้นหลัง คุณสามารถสร้างตัวจัดการที่แนบมากับเธรด UI แล้วโพสต์การดำเนินการเป็น Runnable
:
Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { // update the ui from here } });
วิธีนี้ดีกว่าวิธีแรกมาก แต่มีวิธีที่ง่ายกว่านั้นในการทำเช่นนี้...
ตัวเลือก 3: การใช้ EventBus
EventBus
ห้องสมุดยอดนิยมโดย GreenRobot ช่วยให้ส่วนประกอบต่างๆ สามารถสื่อสารกันได้อย่างปลอดภัย เนื่องจากกรณีการใช้งานของเราเป็นกรณีหนึ่งที่เราต้องการอัปเดต UI เท่านั้น นี่จึงเป็นทางเลือกที่ง่ายและปลอดภัยที่สุด
ขั้นตอนที่ 1 สร้างคลาสเหตุการณ์ เช่น UIEvent
ขั้นตอนที่ 2 สมัครสมาชิกกิจกรรม
@Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {/* Do something */}; register and unregister eventbus : @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onStop() { super.onStop(); EventBus.getDefault().unregister(this); }
ขั้นตอนที่ 3 โพสต์กิจกรรม: EventBus.getDefault().post(new UIEvent());
ด้วยพารามิเตอร์ ThreadMode
ในหมายเหตุประกอบ คุณกำลังระบุเธรดที่คุณต้องการสมัครรับข้อมูลสำหรับเหตุการณ์นี้ ในตัวอย่างของเราที่นี่ เรากำลังเลือกเธรดหลัก เนื่องจากเราต้องการให้ผู้รับของเหตุการณ์สามารถอัปเดต UI ได้
คุณสามารถจัดโครงสร้างคลาส UIEvent
ของคุณให้มีข้อมูลเพิ่มเติมได้ตามความจำเป็น
ในการบริการ:
class UploadFileService extends IntentService { // … Boolean success = uploadFile(File file); EventBus.getDefault().post(new UIEvent(success)); // ... }
ในกิจกรรม/ส่วนย่อย:
@Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {//show message according to the action success};
การใช้ EventBus library
การสื่อสารระหว่างเธรดจะง่ายขึ้นมาก
ใช้กรณีที่ 5: การสื่อสารแบบสองทางระหว่างเธรดตามการกระทำของผู้ใช้
สมมติว่าคุณกำลังสร้างเครื่องเล่นสื่อและต้องการให้สามารถเล่นเพลงต่อไปได้แม้ในขณะที่หน้าจอแอปพลิเคชันปิดอยู่ ในสถานการณ์นี้ คุณจะต้องให้ UI สามารถสื่อสารกับเธรดสื่อ (เช่น เล่น หยุดชั่วคราว และการดำเนินการอื่นๆ) และจะต้องการให้เธรดสื่ออัปเดต UI ตามเหตุการณ์บางอย่าง (เช่น ข้อผิดพลาด สถานะบัฟเฟอร์ ฯลฯ)
ตัวอย่างโปรแกรมเล่นสื่อแบบเต็มอยู่นอกเหนือขอบเขตของบทความนี้ อย่างไรก็ตาม คุณสามารถหาบทแนะนำที่ดีได้ที่นี่และที่นี่
ตัวเลือกที่ 1: การใช้ EventBus
คุณสามารถใช้ EventBus
ได้ที่นี่ อย่างไรก็ตาม โดยทั่วไปแล้วจะไม่ปลอดภัยที่จะโพสต์กิจกรรมจากเธรด UI และรับในบริการ เนื่องจากคุณไม่มีทางรู้ว่าบริการกำลังทำงานอยู่เมื่อคุณส่งข้อความหรือไม่
ตัวเลือกที่ 2: การใช้ BoundService
BoundService
คือ Service
ที่เชื่อมโยงกับกิจกรรม/ส่วนย่อย ซึ่งหมายความว่ากิจกรรม/ส่วนย่อยจะทราบเสมอว่าบริการกำลังทำงานอยู่หรือไม่ และนอกจากนี้ยังสามารถเข้าถึงวิธีการสาธารณะของบริการได้อีกด้วย
ในการใช้งาน คุณต้องสร้าง Binder
แบบกำหนดเองภายในบริการและสร้างวิธีการที่ส่งคืนบริการ
public class MediaService extends Service { private final IBinder mBinder = new MediaBinder(); public class MediaBinder extends Binder { MediaService getService() { // Return this instance of LocalService so clients can call public methods return MediaService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } }
ในการผูกกิจกรรมกับบริการ คุณต้องใช้ ServiceConnection
ซึ่งเป็นคลาสที่ตรวจสอบสถานะบริการ และใช้เมธอด bindService
เพื่อทำการเชื่อม:
// in the activity MediaService mService; // flag indicates the bound status boolean mBound; @Override protected void onStart() { super.onStart(); // Bind to LocalService Intent intent = new Intent(this, MediaService.class); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { MediaBinder binder = (MediaBinder) service; mService = binder.getService(); mBound = true; } @Override public void onServiceDisconnected(ComponentName arg0) { mBound = false; } };
คุณสามารถดูตัวอย่างการใช้งานแบบเต็มได้ที่นี่
ในการสื่อสารกับบริการเมื่อผู้ใช้แตะที่ปุ่มเล่นหรือหยุดชั่วคราว คุณสามารถผูกกับบริการแล้วเรียกวิธีการสาธารณะที่เกี่ยวข้องในบริการ
เมื่อมีเหตุการณ์สื่อและคุณต้องการสื่อสารกลับไปที่กิจกรรม/ส่วนย่อย คุณสามารถใช้หนึ่งในเทคนิคก่อนหน้านี้ (เช่น BroadcastReceiver
, Handler
หรือ EventBus
)
ใช้กรณีที่ 6: การดำเนินการแบบคู่ขนานและรับผลลัพธ์
สมมติว่าคุณกำลังสร้างแอปท่องเที่ยวและต้องการแสดงสถานที่ท่องเที่ยวบนแผนที่ที่ดึงมาจากหลายแหล่ง (ผู้ให้บริการข้อมูลต่างกัน) เนื่องจากแหล่งที่มาทั้งหมดอาจไม่น่าเชื่อถือ คุณอาจต้องเพิกเฉยต่อแหล่งที่มาที่ล้มเหลวและแสดงผลแผนที่ต่อไป
ในการทำให้กระบวนการขนานกัน การเรียก API แต่ละครั้งต้องเกิดขึ้นในเธรดอื่น
ตัวเลือกที่ 1: การใช้ RxJava
ใน RxJava คุณสามารถรวมสิ่งที่สังเกตได้หลายตัวเป็นหนึ่งเดียวโดยใช้ตัวดำเนินการ merge()
หรือ concat()
จากนั้นคุณสามารถสมัคร "ผสาน" ที่สังเกตได้และรอผลทั้งหมด
อย่างไรก็ตาม วิธีการนี้ใช้ไม่ได้ผลตามที่คาดไว้ ถ้าการเรียก API ล้มเหลว การสังเกตที่ผสานจะรายงานความล้มเหลวโดยรวม
ตัวเลือกที่ 2: การใช้ส่วนประกอบ Java ดั้งเดิม
ExecutorService
ใน Java สร้างจำนวนเธรดคงที่ (กำหนดค่าได้) และรันงานบนเธรดเหล่านั้นพร้อมกัน บริการส่งคืนอ็อบเจ็กต์ Future
ที่ส่งคืนผลลัพธ์ทั้งหมดผ่าน invokeAll()
ในที่สุด
งานแต่ละงานที่คุณส่งไปยัง ExecutorService
ควรมีอยู่ในอินเทอร์เฟซ Callable
ซึ่งเป็นอินเทอร์เฟซสำหรับการสร้างงานที่สามารถส่งข้อยกเว้นได้
เมื่อคุณได้ผลลัพธ์จาก invokeAll()
แล้ว คุณสามารถตรวจสอบทุกผลลัพธ์และดำเนินการตามนั้น
ตัวอย่างเช่น สมมติว่าคุณมีสถานที่ท่องเที่ยวสามประเภทที่มาจากปลายทางที่แตกต่างกันสามจุด และคุณต้องการโทรแบบขนานสามประเภท:
ExecutorService pool = Executors.newFixedThreadPool(3); List<Callable<Object>> tasks = new ArrayList<>(); tasks.add(new Callable<Object>() { @Override public Integer call() throws Exception { return mRest.getAttractionType1(); } }); // ... try { List<Future<Object>> results = pool.invokeAll(tasks); for (Future result : results) { try { Object response = result.get(); if (response instance of AttractionType1... {} if (response instance of AttractionType2... {} ... } catch (ExecutionException e) { e.printStackTrace(); } } } catch (InterruptedException e) { e.printStackTrace(); }
ด้วยวิธีนี้ คุณจะเรียกใช้การดำเนินการทั้งหมดพร้อมกัน ดังนั้น คุณสามารถตรวจสอบข้อผิดพลาดในแต่ละการกระทำแยกกัน และละเว้นความล้มเหลวแต่ละรายการตามความเหมาะสม
วิธีนี้ง่ายกว่าการใช้ RxJava ง่ายกว่า สั้นกว่า และไม่ล้มเหลวในการดำเนินการทั้งหมดเนื่องจากมีข้อยกเว้นเพียงข้อเดียว
ใช้กรณี #7: การสืบค้นฐานข้อมูล SQLite ในเครื่อง
เมื่อจัดการกับฐานข้อมูล SQLite ในพื้นที่ ขอแนะนำให้ใช้ฐานข้อมูลจากเธรดพื้นหลัง เนื่องจากการเรียกฐานข้อมูล (โดยเฉพาะอย่างยิ่งกับฐานข้อมูลขนาดใหญ่หรือการสืบค้นที่ซับซ้อน) อาจใช้เวลานาน ส่งผลให้ UI ค้าง
เมื่อทำการสอบถามข้อมูล SQLite คุณจะได้รับออบเจ็กต์ Cursor
ที่สามารถใช้ดึงข้อมูลจริงได้
Cursor cursor = getData(); String name = cursor.getString(<colum_number>);
ตัวเลือกที่ 1: การใช้ RxJava
คุณสามารถใช้ RxJava และรับข้อมูลจากฐานข้อมูล เช่นเดียวกับที่เราได้รับข้อมูลจากส่วนหลังของเรา:
public Observable<Cursor> getLocalDataObservable() { return Observable.create(subscriber -> { Cursor cursor = mDbHandler.getData(); subscriber.onNext(cursor); }); }
คุณสามารถใช้สิ่งที่สังเกตได้ส่งคืนโดย getLocalDataObservable()
ดังต่อไปนี้:
getLocalDataObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(cursor -> String name = cursor.getString(0), throwable -> Log.e(“db, "error: %s" + throwable.getMessage()));
แม้ว่าวิธีนี้จะเป็นแนวทางที่ดี แต่ก็มีวิธีที่ดีกว่า เนื่องจากมีส่วนประกอบที่สร้างขึ้นสำหรับสถานการณ์นี้โดยเฉพาะ
ตัวเลือกที่ 2: การใช้ CursorLoader + ContentProvider
Android มี CursorLoader
ซึ่งเป็นองค์ประกอบดั้งเดิมสำหรับการโหลดข้อมูล SQLite และจัดการเธรดที่เกี่ยวข้อง เป็น Loader
ที่ส่งคืน Cursor
ซึ่งเราสามารถใช้เพื่อรับข้อมูลโดยการเรียกวิธีการง่ายๆ เช่น getString()
, getLong()
เป็นต้น
public class SimpleCursorLoader extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor> { public static final String TAG = SimpleCursorLoader.class.getSimpleName(); private static final int LOADER_ID = 0x01; private TextView textView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_cursor_loader); textView = (TextView) findViewById(R.id.text_view); getSupportLoaderManager().initLoader(LOADER_ID, null, this); } public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { return new CursorLoader(this, Uri.parse("content://com.github.browep.cursorloader.data") , new String[]{"col1"}, null, null, null); } public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { if (cursor != null && cursor.moveToFirst()) { String text = textView.getText().toString(); while (cursor.moveToNext()) { text += "<br />" + cursor.getString(1); cursor.moveToNext(); } textView.setText(Html.fromHtml(text) ); } } public void onLoaderReset(Loader<Cursor> cursorLoader) { } }
CursorLoader
ทำงานร่วมกับคอมโพเนนต์ ContentProvider
ส่วนประกอบนี้มีคุณสมบัติฐานข้อมูลแบบเรียลไทม์มากมาย (เช่น การแจ้งเตือนการเปลี่ยนแปลง ทริกเกอร์ ฯลฯ) ที่ช่วยให้นักพัฒนาสามารถใช้ประสบการณ์ผู้ใช้ที่ดีขึ้นได้ง่ายขึ้นมาก
ไม่มีวิธีแก้ปัญหา Silver Bullet สำหรับเธรดใน Android
Android มีหลายวิธีในการจัดการและจัดการเธรด แต่ไม่มีวิธีใดที่เป็นสัญลักษณ์แสดงหัวข้อย่อยสีเงิน
การเลือกวิธีการเธรดที่เหมาะสม ขึ้นอยู่กับกรณีการใช้งานของคุณ สามารถสร้างความแตกต่างในการทำให้โซลูชันโดยรวมง่ายต่อการใช้งานและทำความเข้าใจ ส่วนประกอบดั้งเดิมนั้นเข้ากันได้ดีสำหรับบางกรณี แต่ไม่ใช่สำหรับทุกคน เช่นเดียวกับโซลูชันบุคคลที่สามแฟนซี
ฉันหวังว่าคุณจะพบว่าบทความนี้มีประโยชน์เมื่อทำงานในโครงการ Android ครั้งต่อไปของคุณ แบ่งปันประสบการณ์ของคุณในการสร้างเธรดใน Android หรือกรณีการใช้งานใดๆ ที่โซลูชันด้านบนทำงานได้ดีหรือไม่สำหรับเรื่องนั้นในความคิดเห็นด้านล่าง