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 中,您可以将所有线程组件分为两个基本类别:

  1. **附加到活动/片段的线程:这些*线程***与活动/片段的生命周期相关联,并在活动/片段被销毁时终止。
  2. *未*附加到任何活动/片段的线程:这些线程可以在生成它们的活动/片段(如果有)的生命周期之后继续运行。

附加到 Activity/Fragment 的线程组件

异步任务

AsyncTask是最基本的Android线程组件。它使用简单,适用于基本场景。

示例用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:AsyncTaskLoaderCursorLoader. CursorLoader您将在本文后面了解更多信息。

AsyncTaskLoader类似于AsyncTask,但稍微复杂一些。

示例用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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在其托管进程的主线程中运行;该服务不会创建自己的线程,也不会在单独的进程中运行,除非您另外指定。

示例用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

使用ServicestopSelf()有责任在其工作完成时通过调用或方法来停止它stopService()

INTENTSERVICE

Like Service,IntentService在单独的线程上运行,并在完成工作后自动停止。

IntentService通常用于不需要附加到任何 UI 的短任务。

示例用法:

1
2
3
4
5
6
7
8
9
10
11
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

尽管 ServiceIntentService在前面的用例中表现良好,但在这里使用它们并不是一个好主意。试图从 aService或 an中获取数据IntentService到主 UI 线程会使事情变得非常复杂。

选项 2:ASYNCTASK 或 LOADERS

乍一看,AsyncTask装载机似乎是这里显而易见的解决方案。它们易于使用——简单明了。

但是,当使用AsyncTaskor LOADERS时,您会注意到需要编写一些样板代码。此外,错误处理成为这些组件的主要工作。即使是简单的网络调用,您也需要了解潜在的异常,捕捉它们并采取相应的行动。这迫使我们将响应包装在包含数据的自定义类中,并带有可能的错误信息,并且标志指示操作是否成功。

每次通话都需要做很多工作。幸运的是,现在有一个更好、更简单的解决方案可用:RxJava。

选项 3:RXJAVA

您可能听说过RxJava,这是由 Netflix 开发的库。这在 Java 中几乎是魔法。

RxAndroid让你在 Android 中使用 RxJava,让处理异步任务变得轻而易举。您可以在此处了解有关 Android 上 RxJava 的更多信息。

RxJava 提供了两个组件:ObserverSubscriber.

观察者是一个包含一些动作的组件。它执行该操作,如果成功则返回结果,如果失败则返回错误。

另一方面,subscriber是一个组件,它可以通过订阅从 observable 接收结果(或错误)。

使用 RxJava,你首先创建一个 observable:

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

创建 observable 后,您可以订阅它。

借助 RxAndroid 库,您可以控制要在 observable 中执行操作的线程,以及要在其中获取响应(即结果或错误)的线程。

您可以使用以下两个函数链接可观察对象:

1
2
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()

调度程序是在某个线程中执行操作的组件。AndroidSchedulers.mainThread()是与主线程关联的调度程序。

鉴于我们的 API 调用是mRestApi.getData()并且它返回一个Data对象,基本调用可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
public Observable<String> getTokenObservable() {
return Observable.create(subscriber -> {
try {
String token = mRestApi.getToken();
subscriber.onNext(token);

} catch (IOException e) {
subscriber.onError(e);
}
});
}

**步骤 2.**创建使用令牌获取数据的 observable:

1
2
3
4
5
6
7
8
9
10
11
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 链接在一起并订阅:

1
2
3
4
5
6
7
8
9
10
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其注册到清单中,并使用IntentIntentFilter创建自定义事件。要触发事件,您将需要该sendBroadcast方法。

显现:

1
2
3
4
5
6
7
8
<receiver android:name="UploadReceiver">

<intent-filter>
<action android:name="com.example.upload">
</action>
</intent-filter>

</receiver>

接收者:

1
2
3
4
5
6
7
8
9
10
public class UploadReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
if (intent.getBoolean(“success”, false) {
Activity activity = (Activity)context;
activity.updateUI();
}

}

发件人:

1
2
3
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

1
2
3
4
5
6
7
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.**订阅活动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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根据需要构建您的类以包含其他信息。

在服务中:

1
2
3
4
5
6
class UploadFileService extends IntentService {
// …
Boolean success = uploadFile(File file);
EventBus.getDefault().post(new UIEvent(success));
// ...
}

在活动/片段中:

1
2
@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

ABoundServiceService绑定到活动/片段的。这意味着活动/片段始终知道服务是否正在运行,此外,它还可以访问服务的公共方法。

要实现它,您需要Binder在服务内部创建一个自定义并创建一个返回服务的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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

merge()在 RxJava 中,您可以使用or运算符将多个 observable 组合为一个concat()。然后,您可以订阅“合并”的 observable 并等待所有结果。

但是,这种方法不会按预期工作。如果一个 API 调用失败,合并后的 observable 将报告整体失败。

选项 2:使用本机 JAVA 组件

ExecutorServiceJava 中创建固定(可配置)数量的线程并同时在它们上执行任务。该服务返回一个Future对象,该对象最终通过该invokeAll()方法返回所有结果。

您发送到的每个任务都ExecutorService应该包含在Callable接口中,该接口是用于创建可以抛出异常的任务的接口。

从 获得结果后invokeAll(),您可以检查每个结果并相应地继续。

例如,假设您有来自三个不同端点的三个景点类型,并且您想要进行三个并行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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对象,然后可以使用该对象获取实际数据。

1
2
Cursor cursor = getData();
String name = cursor.getString(<colum_number>);

选项 1:使用 RXJAVA

您可以使用 RxJava 从数据库中获取数据,就像我们从后端获取数据一样:

1
2
3
4
5
6
public Observable<Cursor> getLocalDataObservable() {
return Observable.create(subscriber -> {
Cursor cursor = mDbHandler.getData();
subscriber.onNext(cursor);
});
}

您可以使用返回的可观察对象getLocalDataObservable(),如下所示:

1
2
3
4
5
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()等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 中使用线程的经验或上述解决方案运行良好的任何用例(或不运行)。

原文地址


Android Threading - All You Need to Know
https://blog.funkt.cn/2022/02/15/1493405804760432641/
作者
anjiemo
发布于
2022年2月15日
许可协议
未经作者授权,禁止转载