本文概述
每个Android开发人员都需要处理其应用程序中的线程。
在Android中启动应用程序时, 它会创建第一个执行线程, 称为”主”线程。主线程负责将事件调度到适当的用户界面小部件, 并与Android UI工具包中的组件进行通信。
为了保持应用程序的响应速度, 必须避免使用主线程执行任何可能最终导致其阻塞的操作。
网络操作和数据库调用, 以及某些组件的加载, 是应在主线程中避免的常见操作示例。在主线程中调用它们时, 它们将被同步调用, 这意味着UI将保持完全无响应, 直到操作完成。因此, 它们通常在单独的线程中执行, 从而避免了在执行UI时阻塞UI(即, 它们是与UI异步执行的)。
Android提供了许多创建和管理线程的方法, 并且存在许多第三方库, 这些线程使线程管理更加轻松。但是, 由于手头有许多不同的方法, 因此选择正确的方法可能会造成很大的混乱。
在本文中, 你将了解一些在Android开发中常见的场景, 其中线程成为必不可少的部分, 以及一些简单的解决方案可以应用于这些场景以及更多其他场景。
Android中的线程
在Android中, 你可以将所有线程组件分为两个基本类别:
- 附加到活动/片段的线程:这些线程与活动/片段的生命周期相关联, 并在活动/片段被破坏时终止。
- 未附加到任何活动/片段的线程:这些线程可以继续在其产生的活动/片段(如果有)的生命周期之外继续运行。
附加到活动/片段的线程组件
异步任务
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不够。值得注意的是, 即使是屏幕旋转这样简单的操作也可能导致活动被破坏。
装载机
装载机是解决上述问题的解决方案。销毁活动后, 加载程序可以自动停止, 也可以在重新创建活动后重新启动自身。
加载程序主要有两种: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();
}
}
}
线程化未附加到活动/片段的组件
服务
服务是一个组件, 可用于在没有任何UI的情况下执行长时间(或可能很长时间)的操作。
服务在其托管过程的主线程中运行;除非你另外指定, 否则该服务不会创建自己的线程, 也不会在单独的进程中运行。
用法示例:
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:服务
服务可能不适合任何使用情况, 因为它没有附加到任何活动中。因此, 即使活动被破坏, 也将能够继续进行网络呼叫。另外, 由于不需要服务器的响应, 因此服务也不受限制。
但是, 由于服务将开始在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。
观察者是包含一些动作的组件。它执行该操作, 如果成功则返回结果, 如果失败则返回错误。
另一方面, 订户是可以通过订阅从观察对象接收结果(或错误)的组件。
使用RxJava, 你首先要创建一个可观察的对象:
Observable.create((ObservableOnSubscribe<Data>) e -> {
Data data = mRestApi.getData();
e.onNext(data);
})
一旦创建可观察对象, 你就可以订阅它。
使用RxAndroid库, 你可以在可观察对象中控制要在其中执行操作的线程以及要在其中获得响应的线程(即结果或错误)。
你可以通过以下两个功能链接到可观察对象:
.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运算符从可观察的源中获取发射值, 然后返回另一个可观察的值。你可以创建一个可观察对象, 然后使用第一个对象的发射值创建另一个可观察对象, 这基本上将它们链接在一起。
步骤1.创建可获取令牌的可观察对象:
public Observable<String> getTokenObservable() {
return Observable.create(subscriber -> {
try {
String token = mRestApi.getToken();
subscriber.onNext(token);
} catch (IOException e) {
subscriber.onError(e);
}
});
}
步骤2.创建使用令牌获取数据的可观察对象:
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.将两个可观察对象链接在一起并订阅:
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旨在执行简单的同步调用, 而不是回调。
另一方面, 对于服务, 你将需要手动停止该服务, 这需要更多的工作。
选项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:使用处理程序
处理程序是可以附加到线程上的组件, 然后可以通过简单消息或Runnable任务对该线程执行某些操作。它与另一个组件Looper协同工作, 后者负责特定线程中的消息处理。
创建处理程序时, 它可以在构造函数中获取Looper对象, 该对象指示处理程序附加到的线程。如果要使用附加到主线程的处理程序, 则需要通过调用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库, 线程之间的通信变得更加简单。
用例5:基于用户操作的线程之间双向通信
假设你正在构建媒体播放器, 并且希望即使关闭应用程序屏幕后它也能够继续播放音乐。在这种情况下, 你将希望UI能够与媒体线程进行通信(例如, 播放, 暂停和其他操作), 并且还将希望媒体线程根据某些事件(例如, 错误, 缓冲状态)来更新UI。等)。
完整的媒体播放器示例不在本文讨论范围之内。但是, 你可以在此处和此处找到良好的教程。
选项1:使用EventBus
你可以在此处使用EventBus。但是, 从UI线程发布事件并在服务中接收事件通常是不安全的。这是因为发送消息后你无法知道服务是否正在运行。
选项2:使用BoundService
BoundService是绑定到活动/片段的服务。这意味着活动/片段始终知道该服务是否正在运行, 并且可以访问该服务的公共方法。
要实现它, 你需要在服务内部创建一个自定义的活页夹, 并创建一个返回服务的方法。
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()运算符将多个可观察对象合并为一个。然后, 你可以订阅”合并的”可观察对象并等待所有结果。
但是, 这种方法无法正常工作。如果一个API调用失败, 则合并的可观察对象将报告整体失败。
选项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数据和管理相应线程的本机组件。这是一个返回游标的加载程序, 我们可以通过调用简单的方法(例如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中没有针对线程化的Silver Bullet解决方案
Android提供了许多处理和管理线程的方法, 但都不是灵丹妙药。
根据你的用例, 选择正确的线程方法可以使整体解决方案易于实现和理解, 从而产生很大的不同。本机组件适合某些情况, 但不适用于所有情况。高端第三方解决方案也是如此。
我希望你在进行下一个Android项目时能对本文有所帮助。在下面的评论中, 与我们分享你在Android中进行线程化的经验, 或者在上述解决方案行之有效的情况下使用案例(或就此而言不行)的任何用例。