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 中使用线程的经验或上述解决方案运行良好的任何用例(或不运行)。