Android最佳实践之后台任务

后台运行的服务IntentService

参考地址:http://developer.android.com/training/run-background-service/index.html
Android的四大组件都是运行在UI(主)线程的。Service组件没有界面,也是运行在主线程中的,如果在Service中运行耗时操作,我们一般采用新建子线程的方式。其实Android Framework提供了IntentService给我们使用。
使用Intentservice有几个限制:

  • 不要和UI直接交互。因为它是在子线程中运行,所以不要在其中操作UI。
  • 任务一个一个完成。比如Intentservice正在进行一个任务,这时又添加一个任务近来。则Intentservice在第一个任务完成时再进行第二个任务。
  • 在Intentservice中运行的任务不能被中断。

创建IntentService

创建一个IntentService只需继承它,并覆盖onHandleIntent()方法:

public class RSSPullService extends IntentService {
    @Override
    protected void onHandleIntent(Intent workIntent) {
        // Gets data from the incoming Intent
        String dataString = workIntent.getDataString();
        ...
        // Do work here, based on the contents of dataString
        ...
    }
}

注意:IntentService是一个Service,它也有正常Service的生命周期,但我们要避免时覆盖其它这些方法比如onStartCommand()等等。

在Manifest中定义

"@drawable/icon"
        android:label="@string/app_name">
        ...
        
        ".RSSPullService"
            android:exported="false"/>
        ...
    

注意:这里没有使用intent filter,因为在Android中,我们使用Service,需要指定明确的Intent。

创建和发送一个任务给IntentService

你可以在 Activity 或Fragment中任意位置发送任务给IntentService。当你startService(),IntentService在onHandleIntent()方法中工作,并Stop掉自己。

/*
 * Creates a new Intent to start the RSSPullService
 * IntentService. Passes a URI in the
 * Intent's "data" field.
 */
mServiceIntent = new Intent(getActivity(), RSSPullService.class);
mServiceIntent.setData(Uri.parse(dataUrl));
// Starts the IntentService
getActivity().startService(mServiceIntent);

从IntentService发出一个回馈

Android提供了推荐的方式来从IntentService中发出反馈,让调用它的Activity或Fragment知道运行的怎么样了。方法是使用LocalBroadcastManager类。LocalBroadcastManager是一个只在自己的App发送广播的类,相对于BradcastReceiver,不用接收全局的广播,性能上更好。LocalBroadcastManager时单例的使用方法如下:

public final class Constants {
    ...
    // Defines a custom Intent action
    public static final String BROADCAST_ACTION =
        "com.example.android.threadsample.BROADCAST";
    ...
    // Defines the key for the status "extra" in an Intent
    public static final String EXTENDED_DATA_STATUS =
        "com.example.android.threadsample.STATUS";
    ...
}
public class RSSPullService extends IntentService {
...
    /*
     * Creates a new Intent containing a Uri object
     * BROADCAST_ACTION is a custom Intent action
     */
    Intent localIntent =
            new Intent(Constants.BROADCAST_ACTION)
            // Puts the status into the Intent
            .putExtra(Constants.EXTENDED_DATA_STATUS, status);
    // Broadcasts the Intent to receivers in this app.
    LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
...
}

接收IntentService发出的广播

我们使用BroadcastReceiver的子类接收IntentService发出的广播。

// Broadcast receiver for receiving status updates from the IntentService
private class ResponseReceiver extends BroadcastReceiver
{
    // Prevents instantiation
    private DownloadStateReceiver() {
    }
    // Called when the BroadcastReceiver gets an Intent it's registered to receive
    @
    public void onReceive(Context context, Intent intent) {
...
        /*
         * Handle Intents here.
         */
...
    }
}

然后是注册广播:

// Class that displays photos
public class DisplayActivity extends FragmentActivity {
    ...
    public void onCreate(Bundle stateBundle) {
        ...
        super.onCreate(stateBundle);
        ...
        // The filter's action is BROADCAST_ACTION
        IntentFilter mStatusIntentFilter = new IntentFilter(
                Constants.BROADCAST_ACTION);

        // Adds a data filter for the HTTP scheme
        mStatusIntentFilter.addDataScheme("http");
        ...
       /*
         * Instantiates a new action filter.
         * No data filter is needed.
         */
        statusIntentFilter = new IntentFilter(Constants.ACTION_ZOOM_IMAGE);
        ...
        // Registers the receiver with the new filter
        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
                mDownloadStateReceiver,
                mIntentFilter);

即使你的app在后台运行,它也会监听到广播,但它不会让你的app进入到前台。永远不要通过启动Activity来响应接收到的广播。我们可以使用Notification来响应。

例子代码下载:ThreadSample.zip

后台加载数据CursorLoader

参考地址:http://developer.android.com/training/load-data-background/index.html
如果我们在UI线程加载数据(通过ContentProvider),有可能会出现ANR(Application Not Responding),这是非常不友好的。这时我们需要在子线程中处理数据的加载,直到取到数据才更新界面。
我们使用CursorLoader类来实现这个。它不仅可以做初始的查询,而且可以在数据变化时自动重新查询。

在Activity中使用CursorLoader

在FragmentActivity中使用CursorLoader,需要实现LoaderCallbacks接口。

public class PhotoThumbnailFragment extends FragmentActivity implements
        LoaderManager.LoaderCallbacks<Cursor> {
...
}

初始化一个查询,需要调用LoaderManager.initLoader(),这个在onCreate()或onCreateView()中调用:

 // Identifies a particular Loader being used in this component
    private static final int URL_LOADER = 0;
    ...
    /* When the system is ready for the Fragment to appear, this displays
     * the Fragment's View
     */
    public View onCreateView(
            LayoutInflater inflater,
            ViewGroup viewGroup,
            Bundle bundle) {
        ...
        /*
         * Initializes the CursorLoader. The URL_LOADER value is eventually passed
         * to onCreateLoader().
         */
        getLoaderManager().initLoader(URL_LOADER, null, this);
        ...
    }

注意:**getLoaderManager()方法只在Fragment中可用,要在FragmentActivity中使用,需要调用**getSupportLoaderManager()方法。

执行查询

当初始化之后,会回调onCreateLoader()方法,要有查询功能,这个方法会返回一个CursorLoader对象。你可以初始化一个空的CursorLoader对象,然后使用它的方法定义你的查询;或者你可以同时实例化对象和定义查询。

/*
* Callback that's invoked when the system has initialized the Loader and
* is ready to start the query. This usually happens when initLoader() is
* called. The loaderID argument contains the ID value passed to the
* initLoader() call.
*/
@Override
public Loader onCreateLoader(int loaderID, Bundle bundle)
{
    /*
     * Takes action based on the ID of the Loader that's being created
     */
    switch (loaderID) {
        case URL_LOADER:
            // Returns a new CursorLoader
            return new CursorLoader(
                        getActivity(),   // Parent activity context
                        mDataUrl,        // Table to query
                        mProjection,     // Projection to return
                        null,            // No selection clause
                        null,            // No selection arguments
                        null             // Default sort order
        );
        default:
            // An invalid id was passed in
            return null;
    }
}

当查询完成,会回调onLoadFinished()方法。

除了重写onCreateLoader()onLoadFinished()方法,还需要重写onLoaderReset()方法(这个方法在CursorLoader检测到与之相关的Cursor数据有变化时调用,它重新运行查询)。

处理查询结果

在onLoadFinished()方法中将查询到的结果显示到View中:

public String[] mFromColumns = {
    DataProviderContract.IMAGE_PICTURENAME_COLUMN
};
public int[] mToFields = {
    R.id.PictureName
};
// Gets a handle to a List View
ListView mListView = (ListView) findViewById(R.id.dataList);
/*
 * Defines a SimpleCursorAdapter for the ListView
 *
 */
SimpleCursorAdapter mAdapter =
    new SimpleCursorAdapter(
            this,                // Current context
            R.layout.list_item,  // Layout for a single row
            null,                // No Cursor yet
            mFromColumns,        // Cursor columns to use
            mToFields,           // Layout fields to use
            0                    // No flags
    );
// Sets the adapter for the view
mListView.setAdapter(mAdapter);
...
/*
 * Defines the callback that CursorLoader calls
 * when it's finished its query
 */
@Override
public void onLoadFinished(Loader loader, Cursor cursor) {
    ...
    /*
     * Moves the query results into the adapter, causing the
     * ListView fronting this adapter to re-display
     */
    mAdapter.changeCursor(cursor);
}

删除旧的Cursor引用

当Cursor无效之后CursorLoader被重置,这个一般发生在绑定在Cursor上的数据发送变化。在重新查询之前,framework 会调用 onLoaderReset()方法。在这个回调方法里,需要删除当前Cursor的所有相关引用以防止内存泄露。当onLoaderReset()执行结束,CursorLoader将重新查询:

/*
 * Invoked when the CursorLoader is being reset. For example, this is
 * called if the data in the provider changes and the Cursor becomes stale.
 */
@Override
public void onLoaderReset(Loader loader) {

    /*
     * Clears out the adapter's reference to the Cursor.
     * This prevents memory leaks.
     */
    mAdapter.changeCursor(null);
}

保持设备唤醒状态

参考地址:http://developer.android.com/training/scheduling/index.html
当Android设备空闲时,屏幕会变暗,然后关闭屏幕,最后会停止CPU的运行,这样可以防止电池电量掉的快。但有些时候我们需要改变Android系统默认的这种状态:比如玩游戏时我们需要保持屏幕常亮,比如一些下载操作不需要屏幕常亮但需要CPU一直运行直到任务完成。

保持屏幕常亮

最好的方式是在Activity中使用FLAG_KEEP_SCREEN_ON 的Flag。

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

这个方法的好处是不像唤醒锁(wake locks),需要一些特定的权限(permission)。并且能正确管理不同app之间的切换,不用担心无用资源的释放问题。
另一个方式是在布局文件中使用android:keepScreenOn属性:

"http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true">
    ...

android:keepScreenOn = ” true “的作用和FLAG_KEEP_SCREEN_ON一样。使用代码的好处是你允许你在需要的地方关闭屏幕。

注意:一般不需要人为的去掉FLAG_KEEP_SCREEN_ON的flag,windowManager会管理好程序进入后台回到前台的的操作。如果确实需要手动清掉常亮的flag,使用getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

保持CPU运行

需要使用PowerManager这个系统服务的唤醒锁(wake locks)特征来保持CPU处于唤醒状态。唤醒锁允许程序控制宿主设备的电量状态。创建和持有唤醒锁对电池的续航有较大的影响,所以,除非是真的需要唤醒锁完成尽可能短的时间在后台完成的任务时才使用它。比如,你永远都不需要在Activity中使用唤醒锁。如果需要关闭屏幕,使用上述的FLAG_KEEP_SCREEN_ON。
只有一个合法的使用场景,是在使用后台服务在屏幕关闭情况下hold住CPU完成一些工作。
要使用唤醒锁,第一步就是添加唤醒锁权限:

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

直接使用唤醒锁:

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
        "MyWakelockTag");
wakeLock.acquire();

但推荐的方式是使用WakefulBroadcastReceiver:使用广播和Service(典型的IntentService)结合的方式来管理后台服务的生命周期。如果不使用唤醒锁来执行后台服务,不能保证未来的某个时刻任务会停止,这不是我们想要的。
使用WakefulBroadcastReceiver第一步就是在Manifest中注册:

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

使用startWakefulService()方法来启动服务,与startService()相比,在启动服务的同时,并启用了唤醒锁。

public class MyWakefulReceiver extends WakefulBroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        // Start the service, keeping the device awake while the service is
        // launching. This is the Intent to deliver to the service.
        Intent service = new Intent(context, MyIntentService.class);
        startWakefulService(context, service);
    }
}

当后台服务的任务完成,要调用MyWakefulReceiver.completeWakefulIntent()来释放唤醒锁。

public class MyIntentService extends IntentService {
    public static final int NOTIFICATION_ID = 1;
    private NotificationManager mNotificationManager;
    NotificationCompat.Builder builder;
    public MyIntentService() {
        super("MyIntentService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        Bundle extras = intent.getExtras();
        // Do the work that requires your app to keep the CPU running.
        // ...
        // Release the wake lock provided by the WakefulBroadcastReceiver.
        MyWakefulReceiver.completeWakefulIntent(intent);
    }
}

定时闹钟(Repeating Alarms)

参考地址:http://developer.android.com/training/scheduling/alarms.html
你可以使用闹钟进行需要长时间运行的操作,比如一天一次下载天气预报。
闹钟有下列特征:

  • 它让你设置次数和时间间隔来进行某操作
  • 你可以使用它和 broadcast receivers联合起来启动Service和其它一些操作
  • 它独立于你的app存在,所以你可以使用它来触发事件或者action,即使你的app没有运行,即使你的设备正在睡眠状态
  • 它可以帮助你最小化资源占用。它可以不依赖Timer或持续运行的后台服务来执行一些操作

对于需要保证发生在应用程序生命周期中的定时操作,不要考虑使用Handler和Timer以及Thread。闹钟给了Android更好的控制系统资源的方法

理解取舍

定时闹钟是一个简单的相对缺乏灵活性的机制。对于你的app它可能不是最好的选择,特别是当你需要触发网络操作时。一个不好的闹钟设计可能导致耗电严重或过重的服务器负载。
一个常见的在应用程序生命周期外触发操作的场景是和服务器同步数据,这种场景下你可能忍不住使用定时闹钟。但是,如果你拥有app的服务器,那么使用Google Cloud Messaging (GCM) 和同步适配器(sync adapter)是比AlarmManger更好的方案。sync adapter能够提供AlarmManger所有的相同操作,但它明显的更灵活。例如,需要从服务器同步新消息下来到设备这样的操作。

最佳实践

每次选择使用闹钟的时候,都会对系统资源的使用有很大的影响。比如和服务器同步。如果同步操作在上午11点进行,那么服务器负载很可能高压甚至“拒绝服务”。下面是使用闹钟的最佳实践:

  • 对于会导致定时请求服务器的闹钟增加时间随机性
  • 将闹钟频率调到最小
  • 不要不必要的唤醒设备
  • 将闹钟时间调的不精确。自Android4.4(API 19)以来,定时闹钟的时间就是不精确的。我们使用setInexactRepeating(),而不是setRepeating(),它减少了设备唤醒的次数,也就减少了电量的消耗。尽管setInexactRepeating()相对于setRepeating()已经改善了,它依然在服务器请求的场景上会对服务器造成大负载,这时请使用时间随机性。
  • 尽可能避免基于对时钟时间的闹钟。

设置一个定时闹钟

定时闹钟有下面几个特征:

  • 闹钟类型
  • 触发时间
  • 时间间隔
  • 触发事件。如果你设置第二个闹钟触发同一个事件,那么第二个闹钟会替换第一个闹钟。

选择一个闹钟类型

一般有两种时钟类型:一个是消逝时间( ELAPSED_REALTIME),一种时真实时间(Real Time Clock,RTC)。消失时间指系统启动开始到现在的时间,真实时间指挂钟时间。所以,消逝时间的闹钟适合做定时(重复)任务,而RTC更适合某时刻执行的闹钟。
这两种时钟都有一个“唤醒”的版本,就是当屏幕关闭时唤醒设备的CPU,这样会保证闹钟在某一个既定的时间启动。如果你的app有时间依赖性的话,就非常有用了。比如,你要在一个受限的窗口执行一个特殊的操作,如果你不使用闹钟的唤醒版本,那么当设备下一次唤醒时所有的定时闹钟都会启动。
如果你仅仅简单的需要每隔一段时间执行一个操作,使用一种消逝时间的闹钟即可,一般这是更好的选择。
如果你需要在一天中的某个时刻执行操作,则选择基于真实时间的闹钟类型。注意:这种方式可能有些弊端:比如app不能很好的转化区域时间或者用户修改了设备的时间,都会导致app不可预计的行为。如果可以的话建议你使用消逝时间类型。
下面是闹钟类型:

  • ELAPSED_REALTIME
  • ELAPSED_REALTIME_WAKEUP
  • RTC
  • RTC_WAKEUP

ELAPSED_REALTIME_WAKEUP例子

唤醒设备30分钟后启动闹钟,每隔30分钟一次:

// Hopefully your alarm will have a lower frequency than this!
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        AlarmManager.INTERVAL_HALF_HOUR,
        AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);

唤醒设备后1分钟启动一个一次性闹钟:

private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        SystemClock.elapsedRealtime() +
        60 * 1000, alarmIntent);

RTC例子

大约下午2点启动闹钟,然后一天启动一次:

// Set the alarm to start at approximately 2:00 p.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 14);

// With setInexactRepeating(), you have to use one of the AlarmManager interval
// constants--in this case, AlarmManager.INTERVAL_DAY.
alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
        AlarmManager.INTERVAL_DAY, alarmIntent);

上午8点30分准确的执行,然后每隔20分钟执行:

private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

// Set the alarm to start at 8:30 a.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 30);

// setRepeating() lets you specify a precise custom interval--in this case,
// 20 minutes.
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
        1000 * 60 * 20, alarmIntent);

决定使用什么精度的时间

对于大部分的app来说,setInexactRepeating()是一个正确的选择。当你使用这个方法,Android咋同一时间同步多个不准确时间的闹钟并启动它们,有效的减少了电量的消耗。
对于极少的app,需要有严格的时间要求,例如,闹钟上午8点30分准时响起,并接着每小时响铃,使用setRepeating()。但可能的话你就要避免使用这样精确的时间。
使用setInexactRepeating()方法,你就不能指定时间间隔了,只能使用时间常量,如INTERVAL_FIFTEEN_MINUTES, INTERVAL_DAY等;setRepeating()可以指定时间间隔。

取消闹钟

传递一个PendingIntent参数:

// If the alarm has been set, cancel it.
if (alarmMgr!= null) {
    alarmMgr.cancel(alarmIntent);
}

当系统启动时启动一个闹钟

默认情况下,系统关机了所有的闹钟就取消了。为防止这事发生,你可以让你的应用程序在系统启动后自动重启闹钟。这样可以保证AlarmManager可以持续的工作,而不用人为的去重启闹钟。下面时步骤:

  1. 申请自启权限。这样允许app接收系统启动的广播:
  2. 接收广播。
public class SampleBootReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
            // Set the alarm here.
        }
    }
}

3、在Manifest中注册广播,设置自启的IntentFitler:

<receiver android:name=".SampleBootReceiver"
        android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED">action>
    intent-filter>
receiver>

注意在Manifest中,广播接受者设置了android:enabled=”false”。这意味着,如果应用程序不明确启用(Enable)它的话,这个广播就不会调用。这样可以防止广播接收者被不必要的调用。你可以Enable一个接收者(例如用户设置一个闹钟)如下:

ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
        PackageManager.DONT_KILL_APP);

一旦你用这个方式enable了接收者,它将一直可用(enabled),甚至重启设备。换句话说,代码层enable的接收者覆盖了manifest中设置的,即使是系统重启。接收者直到你禁用它它才会不可用。你可以禁用一个接收者(例如用户取消闹钟):

ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
        PackageManager.DONT_KILL_APP);

例子代码:Scheduler.zip

你可能感兴趣的:(Android,Andorid最佳实践)