[关闭]
@flyouting 2014-07-06T17:22:06.000000Z 字数 13372 阅读 6541

Android后台任务最佳实践

Best Practices for Background Jobs

Android 线程 服务 后台


官方文档地址

在后台服务中运行

除非你做特别指定,否则在应用中的大部分操作都是执行在前台,在一个特殊的UI线程里面进行的。这有可能会导致一些问题,因为长时间运行的操作会影响到你应用的响应速度。为了避免这个问题,android框架提供了一系列帮助你在后台通过线程推迟加载的功能,用得最多的就是IntentService

这里将向你描述如何实现一个IntentService,发送请求操作并向其它组件报告结果。

创建一个后台服务

IntentService类为一个操作运行在一个线程上提供了一个简单的结构,这允许它可以处理长时间运行的操作而不影响你的用户界面的响应。同时,一个IntentService不被大多数用户界面的生命周期事件影响,因此它可以在异步任务关闭时继续运行。

每个IntentService都是有限制条件的:

  1. 它不可以直接和应用的界面进行交互,为了将操作结果返回给界面,你需要将他们发送到Activity
  2. 工作请求是按顺序进行的,当已经有一个操作在IntentService中运行时,如果这时你发送另外一个请求,需要等到第一个操作执行完毕后才会继续后面的请求
  3. IntentService中运行的操作是不可以被中断的

然而,多数情况下一个IntentService是后台操作最适合的处理方式。

这里将告诉你如何创建你自己的IntentService子类,还会向你展示如何创建一个必要的onHandleIntent()回调。最后,将告诉你如何在manifest文件中定义IntentService

创建一个IntentService

为了在你的应用中创建一个IntentService组件,需要定义一个继承于IntentService的类并复写其onHandleIntent()方法,比如:

  1. public class RSSPullService extends IntentService {
  2. @Override
  3. protected void onHandleIntent(Intent workIntent) {
  4. // Gets data from the incoming Intent
  5. String dataString = workIntent.getDataString();
  6. ...
  7. // Do work here, based on the contents of dataString
  8. ...
  9. }
  10. }

注意,对于任何一个Service都会回调的那些方法,比如onStartCommand(),都会自动地被IntentService引用。在一个IntentService中,你应该避免复写这些回调方法。

在件Manifest中定义IntentService

IntentService同样需要在你应用的清单文件中有一个入口,通过在<application>标签下声明<service>的方式来为IntentService提供入口:

  1. <application
  2. android:icon="@drawable/icon"
  3. android:label="@string/app_name">
  4. ...
  5. <!--
  6. Because android:exported is set to "false",
  7. the service is only available to this app.
  8. -->
  9. <service
  10. android:name=".RSSPullService"
  11. android:exported="false"/>
  12. ...
  13. <application/>

这里的“android:name”属性指定了IntentService的类名。

注意,<service>标签没有包含IntentFilter过滤器。该窗口通过一个明确地Intent向服务发送工作请求,所以不需要任何过滤器。也就是说,只有在同一个应用内,或者是有相同ID的其它应用才可以访问这个服务。

现在你有了基本的IntentService类,你可以通过Intent对象发送工作请求了。

向后台服务发送工作请求

这里将告诉你如何通过发送Intent来触发IntentService执行一个操作。这个Intent可以包含IntentService需要处理的可选数据。你可以在Activity或者Fragment的任何一个地方向IntentService传递Intent。

创建并发送一个工作请求给IntentService

为了创建一个工作请求并将其发送到IntentService,需要创建一个明确地Intent来添加工作请求数据,然后通过调用IntentServiceStartService()方法来发送它。

具体实例如下:

1.为IntentService的子类RSSPullService创建一个新的、明确地Intent。

  1. /*
  2. * Creates a new Intent to start the RSSPullService
  3. * IntentService. Passes a URI in the
  4. * Intent's "data" field.
  5. */
  6. mServiceIntent = new Intent(getActivity(), RSSPullService.class);
  7. mServiceIntent.setData(Uri.parse(dataUrl));

2.调用startService()方法

  1. // Starts the IntentService
  2. getActivity().startService(mServiceIntent);

注意,你可以在Activity或者Fragment的任何地方发送工作请求。比如,如果你需要首先获取用户输入,你可以在按钮点击或者类似于手势操作的回调中来发送请求。

一旦你调用了startService()方法,IntentService会处理定义在onHandleIntent()方法中的工作,然后自己停止。

下一步是向原始的Activity或者Fragment报告工作请求的结果。

报告工作状态

这里将告诉你如何将后台服务的请求工作状态报告给发送请求的组件。这将允许你,比如报告一个窗口对象的UI更新请求状态。一般推荐使用LocalBroadcastManager来发送和接收这些状态,但这仅限于在你自己应用的各组件中广播Intent。

IntentService中报告状态

为了在IntentService中向其他组件发送工作请求状态,首先你需要创建一个包含状态信息数据的Intent,作为了一个选项,你可以在Intent中添加一个操作或者数据URI。

下一步,通过调用LocalBroadcastManager.sendBroadcast()方法来发送Intent,在你应用中发送到其它组件的Intent是注册过的。通过LocalBroadcastManagergetInstance()方法来实例化LocalBroadcastManager

例子:

  1. public final class Constants {
  2. ...
  3. // Defines a custom Intent action
  4. public static final String BROADCAST_ACTION =
  5. "com.example.android.threadsample.BROADCAST";
  6. ...
  7. // Defines the key for the status "extra" in an Intent
  8. public static final String EXTENDED_DATA_STATUS =
  9. "com.example.android.threadsample.STATUS";
  10. ...
  11. }
  12. public class RSSPullService extends IntentService {
  13. ...
  14. /*
  15. * Creates a new Intent containing a Uri object
  16. * BROADCAST_ACTION is a custom Intent action
  17. */
  18. Intent localIntent =
  19. new Intent(Constants.BROADCAST_ACTION)
  20. // Puts the status into the Intent
  21. .putExtra(Constants.EXTENDED_DATA_STATUS, status);
  22. // Broadcasts the Intent to receivers in this app.
  23. LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
  24. ...
  25. }

IntentService中接收广播状态

为了能够接收Intent对象,需要定义一个BroadcastReciver的子类。在该类中,实现BroadcastReceiveronReceive()回调方法,在接收到一个Intent时LocalBroadcastManager会引用它。LocalBroadcastManager将接收到的Intent传递到BroadcastReceiveronRecive()方法中。

一旦你定义了BroadcastReceiver,你就可以通过指定动作、类别和数据等过滤信息来匹配它了。为了达到这种效果,你需要创建一个IntentFilter。下面的代码向你展示了如何定义filter:

  1. // Class that displays photos
  2. public class DisplayActivity extends FragmentActivity {
  3. ...
  4. public void onCreate(Bundle stateBundle) {
  5. ...
  6. super.onCreate(stateBundle);
  7. ...
  8. // The filter's action is BROADCAST_ACTION
  9. IntentFilter mStatusIntentFilter = new IntentFilter(
  10. Constants.BROADCAST_ACTION);
  11. // Adds a data filter for the HTTP scheme
  12. mStatusIntentFilter.addDataScheme("http");
  13. ...

为了在系统中注册BroadcastReceiverIntentFilter,你需要实例化LocalBroadcastManager并调用其registerReceiver()方法。下例展示的是如何注册BroadcastReceiver和其过滤器的过程:

  1. // Instantiates a new DownloadStateReceiver
  2. DownloadStateReceiver mDownloadStateReceiver =
  3. new DownloadStateReceiver();
  4. // Registers the DownloadStateReceiver and its intent filters
  5. LocalBroadcastManager.getInstance(this).registerReceiver(
  6. mDownloadStateReceiver,
  7. mStatusIntentFilter);
  8. ...

一个BroadcastReceiver可以操作多于一种类型的广播Intent对象,每个类型都有自己的操作。这种特征允许你在不同的action中运行代码,不需要为每个action都定义一个BroadcastReceiver。为了为同一个BroadcastReceiver定义其它的IntentFilter,创建IntentFilter并重复调用registerReceiver()。比如:

  1. /*
  2. * Instantiates a new action filter.
  3. * No data filter is needed.
  4. */
  5. statusIntentFilter = new IntentFilter(Constants.ACTION_ZOOM_IMAGE);
  6. ...
  7. // Registers the receiver with the new filter
  8. LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
  9. mDownloadStateReceiver,
  10. mIntentFilter);

发送一个广播Intent不会start或者resume一个窗口。即使你的窗口在后台,窗口中的BroadcastReceiver都是可以接收并处理Intent对象的,但并不会强制让你的应用处于前台。当你的窗口处于后台时如果你想向用户通知这个事件,你可以使用Notification。在接收一个广播Intent时是绝不会启动一个窗口的。

在后台加载数据

对于你需要显示的数据,但需要花时间去通过ContentProvider查询时,如果你直接在Activity层面去执行查询操作,可能会严重影响界面的响应速度,比如ANR。就算不会ANR,用户也会明显地感觉到卡顿的现象。为了避免这种问题,你应该在非UI线程里面来初始化查询操作,直到等待它结束后再窗口显示结果。

你可以通过一个对象在后台执行查询同步,待查询结束后更新UI。这个对象就是CursorLoader。除了初始化后台查询外,当查询有变动时CursorLoader会自动地重新查询数据。

这里将向你描述如何通过CursorLoader执行后台查询操作。在代码中用到了V4-SupportLibrary版本的类,它支持V1.6及以上的版本执行此操作。

通过CursorLoader执行查询操作

通过CursorLoader在后台执行同步查询有别于ContentProvider,它会返回结果到调用它的Activity或者FragmentActivity。这样就允许Activity或者FragmentActivity在后台查询数据时可以和用户交互。

定义一个使用CursorLoader的Activity

为了能够在Activity或者FragmentActivity中使用CursorLoader,需要使用LoaderCallbacks<Cursor>接口,CursorLoader引用接口定义的回调和类进行交互;本课和下一课将详细描述每一个回调。

比如,下面的实例向你展示如何使用依赖库中的CursorLoader定义FragmentActivity。通过扩展FragmentActivity,可以达到通过Fragment使用CursorLoader一样的效果。

  1. public class PhotoThumbnailFragment extends FragmentActivity implements
  2. LoaderManager.LoaderCallbacks<Cursor> {
  3. ...
  4. }

初始化查询操作

为了初始化查询操作,你需要调用LoadManagerinitLoader()方法,它初始化后台框架,你可以在用户进入查询的数据后执行该操作,或者,如果你不需要任何数据,你可以在onCreate()或者onCreateView()中执行该操作,比如:

  1. // Identifies a particular Loader being used in this component
  2. private static final int URL_LOADER = 0;
  3. ...
  4. /* When the system is ready for the Fragment to appear, this displays
  5. * the Fragment's View
  6. */
  7. public View onCreateView(
  8. LayoutInflater inflater,
  9. ViewGroup viewGroup,
  10. Bundle bundle) {
  11. ...
  12. /*
  13. * Initializes the CursorLoader. The URL_LOADER value is eventually passed
  14. * to onCreateLoader().
  15. */
  16. getLoaderManager().initLoader(URL_LOADER, null, this);
  17. ...
  18. }

注意:getLoaderManager()方法仅仅适用于Fragment类,为了能够在FragmentActivity中获取LoaderManager,需通过调用getSupportLoaderManager()

开始查询

为了能够尽快地初始化后台框架,系统会调用你类中的onCreateLoader()方法,为了能够开始查询,需要从该方法中反馈一个CursorLoader对象。你可以初始化一个空的CursorLoader对象然后通过它来定义查询操作,或者你可以在初始化对象的同时定义查询操作。

  1. /*
  2. * Callback that's invoked when the system has initialized the Loader and
  3. * is ready to start the query. This usually happens when initLoader() is
  4. * called. The loaderID argument contains the ID value passed to the
  5. * initLoader() call.
  6. */
  7. @Override
  8. public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle)
  9. {
  10. /*
  11. * Takes action based on the ID of the Loader that's being created
  12. */
  13. switch (loaderID) {
  14. case URL_LOADER:
  15. // Returns a new CursorLoader
  16. return new CursorLoader(
  17. getActivity(), // Parent activity context
  18. mDataUrl, // Table to query
  19. mProjection, // Projection to return
  20. null, // No selection clause
  21. null, // No selection arguments
  22. null // Default sort order
  23. );
  24. default:
  25. // An invalid id was passed in
  26. return null;
  27. }
  28. }

一旦生成了后台框架的对象,系统就会开始在后台执行查询操作,当查询操作执行完成后,后台框架会调用onLoadFinished()方法。

处理结果

正如前面所述,你应该在你所实现类的onCreateLoader()方法中通过CursorLoader加载你的数据,加载器会在你Acitivity或者FragmentActivity的LoaderCallbacks.onLoadFinished()方法中返回查询结果。该方法的其中一个入参为包含查询结果的游标。你可以使用这个对象来更新你的数据或者做其它操作。

除了onCreateLoader()onLoadFinished()方法,你还需要实现onLoaderReset()方法,这个方法会在数据更新更新时被调用,当数据变化时,框架会重新执行当前的查询操作。

处理查询结果

为了显示从CursorLoader返回的游标数据,你需要自定义一个继承于AdapterView的视图,并为这个视图定义一个继承于CursorAdapter的适配器。然后系统会自动地将数据从游标移到视图。

在你显示任何数据之前你可以为视图和适配器建立连接,然后再onLoadFinished()方法中将游标移到适配器。当你将游标移到适配器后,系统会自动地更新视图。在游标的数据有改动时同样会更新视图。

  1. public String[] mFromColumns = {
  2. DataProviderContract.IMAGE_PICTURENAME_COLUMN
  3. };
  4. public int[] mToFields = {
  5. R.id.PictureName
  6. };
  7. // Gets a handle to a List View
  8. ListView mListView = (ListView) findViewById(R.id.dataList);
  9. /*
  10. * Defines a SimpleCursorAdapter for the ListView
  11. *
  12. */
  13. SimpleCursorAdapter mAdapter =
  14. new SimpleCursorAdapter(
  15. this, // Current context
  16. R.layout.list_item, // Layout for a single row
  17. null, // No Cursor yet
  18. mFromColumns, // Cursor columns to use
  19. mToFields, // Layout fields to use
  20. 0 // No flags
  21. );
  22. // Sets the adapter for the view
  23. mListView.setAdapter(mAdapter);
  24. ...
  25. /*
  26. * Defines the callback that CursorLoader calls
  27. * when it's finished its query
  28. */
  29. @Override
  30. public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
  31. ...
  32. /*
  33. * Moves the query results into the adapter, causing the
  34. * ListView fronting this adapter to re-display
  35. */
  36. mAdapter.changeCursor(cursor);
  37. }

删除旧的游标信息

当游标非法时CursorLoader会被重置,这多数情况发生在游标的数据有改动时,在重新执行查询操作前,框架会调用你所实现的onLoaderReset()方法。在这个回调中,你应该删除当前游标的所有信息来避免内存泄露。一旦结束回调onLoaderReset()方法后,CursorLoader会重新执行查询操作。比如

  1. /*
  2. * Invoked when the CursorLoader is being reset. For example, this is
  3. * called if the data in the provider changes and the Cursor becomes stale.
  4. */
  5. @Override
  6. public void onLoaderReset(Loader<Cursor> loader) {
  7. /*
  8. * Clears out the adapter's reference to the Cursor.
  9. * This prevents memory leaks.
  10. */
  11. mAdapter.changeCursor(null);
  12. }

管理设备的激活状态

当一个android设备处于空闲状态时,它首先会变暗,然后会关屏,最终会让CPU停止工作。这样处理是为了避免设备的电池被快速地耗尽,然而有些时候你的应用需要一些不同的表现:

(1)游戏或者电影应用可能需要保持屏幕常亮;

(2)有些应用虽然不需要屏幕常亮,但在CPU执行完核心操作之前同样需要保持程序运行。

本节的目的是告诉你在避免电池被快速耗尽的情况下如何保持设备处于激活状态。

保持设备处于激活状态

为了避免电池被耗尽,android设备会在处于空闲状态时立即切换到休眠状态。然而,有些时候一个应用需要保持屏幕常亮或者CPU直到某些事情被处理完成。
你该采取什么操作取决于你应用的需求。然而,通用的规则是你应该使用最轻量级的操作来处理你的应用程序,使你的应用减少对系统资源的占用。下面将向你描述通过怎样地操作来使得你对应用的处理和系统默认的休眠行为相容。

保持屏幕常亮

某些应用需要保持屏幕常亮,比如游戏或者电影应用。最好的方式是在你的窗口中使用FLAG_KEEP_SCREEN_ON属性(只在一个窗口,绝不是在一个服务或者其它应用组件中),比如:

  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  7. }

这种先进的处理方式有别于唤醒锁,它不需要特殊的权限,平台会正确地管理应用之间的切换,你不需要担心自己的应用没有释放没有使用的资源。

实现该功能的另一种思路是在你应用xml文件中使用android:keepScreenOn属性:

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent"
  4. android:keepScreenOn="true">
  5. ...
  6. </RelativeLayout>

使用andorid:keepScreenOn=”true”和使用FLAG_KEEP_SCREEN_ON是一样的效果。你可以使用适合于你应用的任何一种方式。通过在程序中设置你窗口常亮状态的优势是:它可以清楚这个标志,从而可以关闭屏幕。

保持CPU持续工作

如果你希望在设备休眠之前CPU能够完成需要处理的工作,你可以使用一个叫唤醒锁的PowerManager系统服务。唤醒锁允许你的应用可以控制主机设备电源的状态。

创建和保持唤醒锁会在一定程度上对电池的寿命有所影响,因此你应该只在非常有必要的情况下使用它,并尽量控制使用时间。比如,你绝不应该在一个窗口中使用唤醒锁,正如上面所描述的那样,如果你想保持当前窗口的屏幕常亮,你可以使用FLAG_KEEP_SCREEN_ON

应该使用唤醒锁的情况可能就是后台服务在屏幕关闭时需要通过唤醒锁保持CPU持续工作。再次声明,尽量限制它的使用时间,因为它会影响到电池的寿命。

为了使用唤醒锁,首先需要在清单文件中添加WAKE_LOCK权限:

  1. <uses-permission android:name="android.permission.WAKE_LOCK" />

如果你的应用包含一个使用服务处理某些事情的广播接收器,你可以通过WakefulBroadcastReceiver来管理你的唤醒锁,正如使用WakefulBroadcastReceiver一课中所描述的那样,这是一个比较好的处理方式。如果你的应用没有遵循这种方式,通过下面的代码你可以直接设置唤醒锁:

  1. PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
  2. Wakelock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
  3. "MyWakelockTag");
  4. wakeLock.acquire();

为了释放唤醒锁,你需要调用wakelock的release()方法。它将释放你对CPU的声明,在你的应用结束工作后尽快关闭唤醒锁避免电池被耗尽。

使用WakefulBroadcastReceiver

使用广播接收器和服务可以让你很好地管理后台任务的生命周期。

一个WakefulBroadcastReceiver是广播接收器的一个特殊类型,它可以创建和管理你应用的PARITAL_WAKE_LOCK。一个WakeBroadcastReceiver接收到广播后将工作传递给Service(一个典型的IntentService),直到确保设备没有休眠。如果你在交接工作给服务的时候没有保持唤醒锁,在工作还没完成之前就允许设备休眠的话,将会出现一些你不愿意看到的情况。

要使用WakefulBroadcastReceiver的第一步是在manifest文件中添加它,和其它广播接收器是一样的:

  1. <receiver android:name=".MyWakefulReceiver"></receiver>

接下来是在代码中通过startWakefulService()来启动MyIntentService。和starService()方法相比,除了在服务启动时可以保持唤醒锁外,通过startWakefulService()方法传递的Intent可以保持一个额外的唤醒锁:

  1. public class MyWakefulReceiver extends WakefulBroadcastReceiver {
  2. @Override
  3. public void onReceive(Context context, Intent intent) {
  4. // Start the service, keeping the device awake while the service is
  5. // launching. This is the Intent to deliver to the service.
  6. Intent service = new Intent(context, MyIntentService.class);
  7. startWakefulService(context, service);
  8. }
  9. }

当服务执行完成后,系统会调用MyWakefulReceivercompleteWakefulIntent()方法来释放唤醒锁,completeWakefulIntent()方法携带的参数是从WakefulBroadcastReceiver传递过来的intent:

  1. public class MyIntentService extends IntentService {
  2. public static final int NOTIFICATION_ID = 1;
  3. private NotificationManager mNotificationManager;
  4. NotificationCompat.Builder builder;
  5. public MyIntentService() {
  6. super("MyIntentService");
  7. }
  8. @Override
  9. protected void onHandleIntent(Intent intent) {
  10. Bundle extras = intent.getExtras();
  11. // Do the work that requires your app to keep the CPU running.
  12. // ...
  13. // Release the wake lock provided by the WakefulBroadcastReceiver.
  14. MyWakefulReceiver.completeWakefulIntent(intent);
  15. }
  16. }

调度重复性警报

Alarms(基于AlarmManager类)给你一种方式来执行应用生命周期之外的基于时间的操作。例如,你可以使用一个alarm去初始化一个长时间运行的操作。比如开启一个服务一天一次去下载一个天气预报。

Alarms有这些特点:

理解Trade-offs

一个重复报警是一个具有有限灵活性的相对简单的机制。对我们的应用,它可能不是最好的选择,特别是如果您需要触发网络操作。一个设计糟糕的alarm会导致电池消耗,给服务器增加了负担。

在应用生命周期外触发一个操作的常见场景是跟服务器段同步数据。在这种情况下,你可能想使用一个重复性alarm,但是你的服务器使用谷歌云存储消息传递(GCM)结合同步适配器要比 AlarmManager好很多,一个同步适配器提供给你像AlarmManager一样的调度选项,但是它提供了更多的灵活性。例如,一个同步可以是基于“新数据”从服务端或者设备,用户的不同activity,一天中的不同时间,或者其他。

最佳实践

当你在设计你的一个重复性的alarm时,你的任何选择都可能影响你的app使用(或者说是滥用)系统资源。例如,假设一个受欢迎的应用程序与服务器同步,如果同步操作是基于时钟时间,每个app的实例在晚上11点同步。服务器上的负载可能会高负载甚至导致“拒绝服务”,使用alarm时遵循这些最佳实践:

设置一个重复性alarm

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注