Android 線程:所有你需要知道的

已發表: 2022-03-11

每個 Android 開發人員,有時都需要處理其應用程序中的線程。

當應用程序在 Android 中啟動時,它會創建第一個執行線程,稱為“主”線程。 主線程負責將事件分派給適當的用戶界面小部件,並與來自 Android UI 工具包的組件進行通信。

為了讓您的應用程序保持響應,必須避免使用主線程來執行任何可能最終導致其阻塞的操作。

網絡操作和數據庫調用,以及某些組件的加載,是應該在主線程中避免的常見操作示例。 在主線程中調用它們時,它們是同步調用的,這意味著 UI 將保持完全無響應,直到操作完成。 出於這個原因,它們通常在單獨的線程中執行,從而避免在執行它們時阻塞 UI(即,它們與 UI 異步執行)。

Android 提供了許多創建和管理線程的方法,並且存在許多使線程管理更加愉快的第三方庫。 然而,手頭有這麼多不同的方法,選擇正確的方法可能會很混亂。

在本文中,您將了解 Android 開發中線程變得必不可少的一些常見場景,以及一些可以應用於這些場景的簡單解決方案等等。

Android中的線程

在 Android 中,您可以將所有線程組件分為兩個基本類別:

  1. 附加到活動/片段的線程這些線程與活動/片段的生命週期相關聯,並在活動/片段被銷毀時終止。
  2. 附加到任何活動/片段的線程:這些線程可以在生成它們的活動/片段(如果有)的生命週期之後繼續運行。

附加到 Activity/Fragment 的線程組件

異步任務

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了。 值得注意的是,即使是像屏幕旋轉這樣簡單的事情也可能導致 Activity 被破壞。

裝載機

裝載機是上述問題的解決方案。 Loaders 可以在 Activity 被銷毀時自動停止,也可以在 Activity 重新創建後自行重啟。

主要有兩種類型的加載器: AsyncTaskLoaderCursorLoader 。 您將在本文後面了解有關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(); } } }

不附加到 Activity/Fragment 的線程組件

服務

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()方法來停止它。

意向服務

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 或加載器

您可以使用AsyncTask或加載程序進行調用,它會起作用。

但是, AsyncTask和加載器都依賴於活動的生命週期。 這意味著您要么需要等待調用執行並嘗試阻止用戶離開活動,要么希望它在活動被銷毀之前執行。

選項 2:服務

Service可能更適合此用例,因為它不附加到任何活動。 因此,即使在活動被破壞後,它也能夠繼續進行網絡調用。 另外,由於不需要來自服務器的響應,因此這裡的服務也不會受到限制。

但是,由於服務將開始在 UI 線程上運行,您仍然需要自己管理線程。 您還需要確保在網絡調用完成後停止服務。

這將需要比這樣一個簡單的動作所需要的更多的努力。

選項 3:IntentService

在我看來,這將是最好的選擇。

由於IntentService不附加到任何活動並且它在非 UI 線程上運行,因此它在這裡完美地滿足了我們的需求。 此外, IntentService會自動停止,因此也無需手動管理它。

用例 2:進行網絡調用,並從服務器獲取響應

這個用例可能更常見一些。 例如,您可能希望在後端調用 API 並使用其響應來填充屏幕上的字段。

選項 1:服務或 IntentService

儘管ServiceIntentService在前面的用例中表現良好,但在這裡使用它們並不是一個好主意。 試圖從ServiceIntentService中獲取數據到主 UI 線程會使事情變得非常複雜。

選項 2:AsyncTask 或加載器

乍一看, AsyncTask或加載器似乎是這裡顯而易見的解決方案。 它們易於使用——簡單明了。

但是,當使用AsyncTask或加載器時,您會注意到需要編寫一些樣板代碼。 此外,錯誤處理成為這些組件的主要工作。 即使是簡單的網絡調用,您也需要了解潛在的異常,捕捉它們並採取相應的行動。 這迫使我們將響應包裝在包含數據的自定義類中,並帶有可能的錯誤信息,並且標誌指示操作是否成功。

每次通話都需要做很多工作。 幸運的是,現在有一個更好、更簡單的解決方案可用:RxJava。

選項 3:RxJava

你可能聽說過 Netflix 開發的庫 RxJava。 這在 Java 中幾乎是魔法。

RxAndroid 讓你在 Android 中使用 RxJava,讓處理異步任務變得輕而易舉。 您可以在此處了解有關 Android 上的 RxJava 的更多信息。

RxJava 提供了兩個組件: ObserverSubscriber

觀察者是一個包含一些動作的組件。 它執行該操作,如果成功則返回結果,如果失敗則返回錯誤。

另一方面, subscriber是一個組件,它可以通過訂閱從 observable 接收結果(或錯誤)。

使用 RxJava,你首先創建一個 observable:

 Observable.create((ObservableOnSubscribe<Data>) e -> { Data data = mRestApi.getData(); e.onNext(data); })

創建 observable 後,您可以訂閱它。

借助 RxAndroid 庫,您可以控制要在 observable 中執行操作的線程,以及要在其中獲取響應(即結果或錯誤)的線程。

您可以使用以下兩個函數鏈接可觀察對象:

 .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 或加載器

使用AsyncTask或加載器幾乎肯定會導致意大利麵條代碼。 整體功能將很難正確完成,並且在整個項目中需要大量冗餘的樣板代碼。

選項 2:使用 flatMap 的 RxJava

在 RxJava 中, flatMap運算符從源 observable 獲取一個發出的值並返回另一個 observable。 您可以創建一個可觀察對象,然後使用第一個發出的值創建另一個可觀察對象,這基本上會將它們鏈接起來。

步驟 1.創建獲取令牌的 observable:

 public Observable<String> getTokenObservable() { return Observable.create(subscriber -> { try { String token = mRestApi.getToken(); subscriber.onNext(token); } catch (IOException e) { subscriber.onError(e); } }); }

步驟 2.創建使用令牌獲取數據的 observable:

 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.將兩個 observable 鏈接在一起並訂閱:

 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:廣播接收器

Android 提供了這個組件,它可以監聽全局事件(例如,電池事件、網絡事件等)以及自定義事件。 您可以使用此組件創建上傳完成時觸發的自定義事件。

為此,您需要創建一個擴展BroadcastReceiver的自定義類,將其註冊到清單中,並使用IntentIntentFilter創建自定義事件。 要觸發事件,您將需要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對象,這個對象表示這個Handler附著在哪個線程上。 如果要使用附加到主線程的處理程序,則需要通過調用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; } };

您可以在此處找到完整的實現示例。

要在用戶點擊播放或暫停按鈕時與服務通信,您可以綁定到服務,然後在服務上調用相關的公共方法。

當有媒體事件並且您想將其傳達回活動/片段時,您可以使用較早的技術之一(例如BroadcastReceiverHandlerEventBus )。

用例 6:並行執行操作並獲得結果

假設您正在構建一個旅遊應用程序,並且您想在從多個來源(不同數據提供者)獲取的地圖上顯示景點。 由於並非所有來源都是可靠的,因此您可能希望忽略失敗的來源並繼續渲染地圖。

為了使流程並行化,每個 API 調用都必須在不同的線程中進行。

選項 1:使用 RxJava

在 RxJava 中,您可以使用merge()concat()運算符將多個 observable 組合為一個。 然後,您可以訂閱“合併”的 observable 並等待所有結果。

但是,這種方法不會按預期工作。 如果一個 API 調用失敗,合併後的 observable 將報告整體失敗。

選項 2:使用本機 Java 組件

Java 中的ExecutorService創建固定(可配置)數量的線程並同時在它們上執行任務。 該服務返回一個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()返回的 observable,如下所示:

 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 數據和管理相應線程的原生組件。 它是一個返回CursorLoader ,我們可以通過調用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) { } }

CursorLoaderContentProvider組件一起使用。 該組件提供了大量實時數據庫功能(例如,更改通知、觸發器等),使開發人員能夠更輕鬆地實現更好的用戶體驗。

Android 中的線程沒有靈丹妙藥的解決方案

Android 提供了許多處理和管理線程的方法,但它們都不是靈丹妙藥。

根據您的用例選擇正確的線程方法,可以使整個解決方案易於實施和理解。 本機組件非常適合某些情況,但並非適用於所有情況。 這同樣適用於花哨的第三方解決方案。

我希望您在處理下一個 Android 項目時會發現這篇文章很有用。 在下面的評論中與我們分享您在 Android 中使用線程的經驗或上述解決方案運行良好的任何用例(或不運行)。