Android Threading - All You Need to Know

Android Threading - All You Need to Know
Android 提供了许多创建和管理线程的方法,而第三方库的存在使这变得更加容易。然而,有这么多的选择,选择正确的方法可能会相当混乱。在本文中,Toptal 自由职业者软件工程师 Eliran Goshen 讨论了 Android 开发中涉及线程的一些常见场景以及如何处理每个场景。
作者
Eliran 拥有扎实的计算机科学学士学位,已经用 Java 和 React Native 构建了许多专业的 Android 应用程序和 Android/iOS 应用程序。
每个 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
装载机是上述问题的解决方案。Loaders 可以在 Activity 被销毁时自动停止,也可以在 Activity 重新创建后自行重启。
主要有两种类型的 LOADERS: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()
。
INTENTSERVICE
Like 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:SERVICE 或 INTENTSERVICE
尽管 Service
或IntentService
在前面的用例中表现良好,但在这里使用它们并不是一个好主意。试图从 aService
或 an中获取数据IntentService
到主 UI 线程会使事情变得非常复杂。
选项 2:ASYNCTASK 或 LOADERS
乍一看,AsyncTask
装载机似乎是这里显而易见的解决方案。它们易于使用——简单明了。
但是,当使用AsyncTask
or LOADERS时,您会注意到需要编写一些样板代码。此外,错误处理成为这些组件的主要工作。即使是简单的网络调用,您也需要了解潜在的异常,捕捉它们并采取相应的行动。这迫使我们将响应包装在包含数据的自定义类中,并带有可能的错误信息,并且标志指示操作是否成功。
每次通话都需要做很多工作。幸运的是,现在有一个更好、更简单的解决方案可用:RxJava。
选项 3:RXJAVA
您可能听说过RxJava,这是由 Netflix 开发的库。这在 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 或 LOADERS
使用AsyncTask
或 LOADERS几乎肯定会导致意大利面条代码。整体功能将很难正确完成,并且在整个项目中需要大量冗余的样板代码。
选项 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:SERVICE 内的 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
AHandler
是一个组件,可以附加到线程,然后通过简单的消息或Runnable
任务在该线程上执行一些操作。它与另一个组件一起工作,该组件Looper
负责特定线程中的消息处理。
在Handler
创建a时,它可以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
ABoundService
是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
merge()
在 RxJava 中,您可以使用or运算符将多个 observable 组合为一个concat()
。然后,您可以订阅“合并”的 observable 并等待所有结果。
但是,这种方法不会按预期工作。如果一个 API 调用失败,合并后的 observable 将报告整体失败。
选项 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
返回 a的 a 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
组件一起使用。该组件提供了大量实时数据库功能(例如,更改通知、触发器等),使开发人员能够更轻松地实现更好的用户体验。
Android 中的线程没有灵丹妙药的解决方案
Android 提供了许多处理和管理线程的方法,但它们都不是灵丹妙药。
根据您的用例选择正确的线程方法,可以使整个解决方案易于实施和理解。本机组件非常适合某些情况,但并非适用于所有情况。这同样适用于花哨的第三方解决方案。
我希望您在处理下一个 Android 项目时会发现这篇文章很有用。在下面的评论中与我们分享您在 Android 中使用线程的经验或上述解决方案运行良好的任何用例(或不运行)。