Android 线程:所有你需要知道的
已发表: 2022-03-11每个 Android 开发人员,有时都需要处理其应用程序中的线程。
当应用程序在 Android 中启动时,它会创建第一个执行线程,称为“主”线程。 主线程负责将事件分派给适当的用户界面小部件,并与来自 Android UI 工具包的组件进行通信。
为了让您的应用程序保持响应,必须避免使用主线程来执行任何可能最终导致其阻塞的操作。
网络操作和数据库调用,以及某些组件的加载,是应该在主线程中避免的常见操作示例。 在主线程中调用它们时,它们是同步调用的,这意味着 UI 将保持完全无响应,直到操作完成。 出于这个原因,它们通常在单独的线程中执行,从而避免在执行它们时阻塞 UI(即,它们与 UI 异步执行)。
Android 提供了许多创建和管理线程的方法,并且存在许多使线程管理更加愉快的第三方库。 然而,手头有这么多不同的方法,选择正确的方法可能会很混乱。
在本文中,您将了解 Android 开发中线程变得必不可少的一些常见场景,以及一些可以应用于这些场景的简单解决方案等等。
Android中的线程
在 Android 中,您可以将所有线程组件分为两个基本类别:
- 附加到活动/片段的线程:这些线程与活动/片段的生命周期相关联,并在活动/片段被销毁时终止。
- 未附加到任何活动/片段的线程:这些线程可以在生成它们的活动/片段(如果有)的生命周期之后继续运行。
附加到 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 重新创建后自行重启。
主要有两种类型的加载器: 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(); } } }
不附加到 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
尽管Service
或IntentService
在前面的用例中表现良好,但在这里使用它们并不是一个好主意。 试图从Service
或IntentService
中获取数据到主 UI 线程会使事情变得非常复杂。
选项 2:AsyncTask 或加载器
乍一看, AsyncTask
或加载器似乎是这里显而易见的解决方案。 它们易于使用——简单明了。
但是,当使用AsyncTask
或加载器时,您会注意到需要编写一些样板代码。 此外,错误处理成为这些组件的主要工作。 即使是简单的网络调用,您也需要了解潜在的异常,捕捉它们并采取相应的行动。 这迫使我们将响应包装在包含数据的自定义类中,并带有可能的错误信息,并且标志指示操作是否成功。
每次通话都需要做很多工作。 幸运的是,现在有一个更好、更简单的解决方案可用:RxJava。
选项 3:RxJava
你可能听说过 Netflix 开发的库 RxJava。 这在 Java 中几乎是魔法。
RxAndroid 让你在 Android 中使用 RxJava,让处理异步任务变得轻而易举。 您可以在此处了解有关 Android 上的 RxJava 的更多信息。
RxJava 提供了两个组件: Observer
和Subscriber
。
观察者是一个包含一些动作的组件。 它执行该操作,如果成功则返回结果,如果失败则返回错误。
另一方面, 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
的自定义类,将其注册到清单中,并使用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
对象,这个对象表示这个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; } };
您可以在此处找到完整的实现示例。
要在用户点击播放或暂停按钮时与服务通信,您可以绑定到服务,然后在服务上调用相关的公共方法。
当有媒体事件并且您想将其传达回活动/片段时,您可以使用较早的技术之一(例如BroadcastReceiver
、 Handler
或EventBus
)。
用例 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 数据和管理相应线程的原生组件。 它是一个返回Cursor
的Loader
,我们可以通过调用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
组件一起使用。 该组件提供了大量实时数据库功能(例如,更改通知、触发器等),使开发人员能够更轻松地实现更好的用户体验。
Android 中的线程没有灵丹妙药的解决方案
Android 提供了许多处理和管理线程的方法,但它们都不是灵丹妙药。
根据您的用例选择正确的线程方法,可以使整个解决方案易于实施和理解。 本机组件非常适合某些情况,但并非适用于所有情况。 这同样适用于花哨的第三方解决方案。
我希望您在处理下一个 Android 项目时会发现这篇文章很有用。 在下面的评论中与我们分享您在 Android 中使用线程的经验或上述解决方案运行良好的任何用例(或不运行)。