Android Threading: ทั้งหมดที่คุณต้องรู้

เผยแพร่แล้ว: 2022-03-11

นักพัฒนา Android ทุกคนต้องจัดการกับเธรดในแอปพลิเคชันของตนไม่ทางใดก็ทางหนึ่ง

เมื่อเปิดตัวแอปพลิเคชันใน Android จะสร้างเธรดแรกของการดำเนินการที่เรียกว่าเธรด "หลัก" เธรดหลักมีหน้าที่รับผิดชอบในการส่งเหตุการณ์ไปยังวิดเจ็ตอินเทอร์เฟซผู้ใช้ที่เหมาะสม รวมถึงการสื่อสารกับส่วนประกอบจากชุดเครื่องมือ Android UI

เพื่อให้แอปพลิเคชันของคุณตอบสนองได้ คุณจำเป็นต้องหลีกเลี่ยงการใช้เธรดหลักเพื่อดำเนินการใดๆ ที่อาจทำให้ถูกบล็อกได้

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

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

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

เธรดใน Android

ใน Android คุณสามารถจัดหมวดหมู่ส่วนประกอบเธรดทั้งหมดเป็นสองหมวดหมู่พื้นฐาน:

  1. เธรด ที่ แนบกับกิจกรรม/ส่วนย่อย: เธรดเหล่านี้เชื่อมโยงกับวงจรชีวิตของกิจกรรม/ส่วนย่อย และจะสิ้นสุดลงทันทีที่กิจกรรม/ส่วนย่อยถูกทำลาย
  2. เธรดที่ ไม่ได้ แนบกับกิจกรรม/ส่วนย่อยใดๆ: เธรดเหล่านี้สามารถทำงานต่อไปได้เกินอายุของกิจกรรม/ส่วนย่อย (ถ้ามี) ที่เกิด

ส่วนประกอบเกลียวที่ยึดติดกับกิจกรรม/ชิ้นส่วน

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 หรือกรณีการใช้งานใดๆ ที่โซลูชันด้านบนทำงานได้ดีหรือไม่สำหรับเรื่องนั้นในความคิดเห็นด้านล่าง