Android实践篇:如何优雅的完成后台任务

堵宏毅
2023-12-01


                                        使用IntentService完成后台任务                                             

创建一个后台service

       使用之前我们需要了解,IntentService允许在一个非UI线程完成一些后台任务,但是也有其限制:

  • 它不能直接参与UI交互,需要把结果传递到UI线程,也就是传递给Activity
  • 它在完成后台任务时是串行的
  • 运行中的任务是不能被中断的
  1. 创建一个类继承自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
        ...
    }
}
注意:普通Service需要实现的方法,例如onCreate onStartCommand等在这里都不需要。

2. 在manifest中申明:
<application
        android:icon="@drawable/icon"
        android:label="@string/app_name">
        ...
        <!--
            Because android:exported is set to "false",
            the service is only available to this app.
        -->
        <service
            android:name=".RSSPullService"
            android:exported="false"/>
        ...
    <application/>
注意:可以看到这个service没有设置intent filter,因此Activity需要执行后台任务的时候,需要显式启动service(直接用类名),这也意味着这个service只能由同一个app内的其他组件启动(或者其他拥有相同userID的app内的组件启动)

发送后台任务请求到IntentService

1.new一个显式的Intent,把后台任务需要的数据设置进去:

mServiceIntent = new Intent(getActivity(), RSSPullService.class);
mServiceIntent.setData(Uri.parse(dataUrl));

2.调用 startService()  

getActivity().startService(mServiceIntent);

返回后台任务执行结果

利用LocalBroadcastManager,反馈执行结果到调用它的Activity,LocalBroadcastManager,会限制Intent只发送给同一app内的组件。首先new一个包含执行结果,或者过程状态的Intent,其次通过LocalBroadcastManager.sendBroadcast()方法,把intent发送给app内的任意组件。

实例如下:

在IntentService中发送结果:

public final class Constants {
    public static final String BROADCAST_ACTION =
        "com.example.android.threadsample.BROADCAST";
  public static final String EXTENDED_DATA_STATUS =
        "com.example.android.threadsample.STATUS";
}
public class RSSPullService extends IntentService {
    Intent localIntent =
            new Intent(Constants.BROADCAST_ACTION)
            .putExtra(Constants.EXTENDED_DATA_STATUS, status);
    LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
...
}

创建监听结果的广播接收者:

private class ResponseReceiver extends BroadcastReceiver
{
   private DownloadStateReceiver() {
    }
   public void onReceive(Context context, Intent intent) {
        /*
         * Handle Intents here.
         */
    }
}

在Activity中注册这个广播接收者:

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");
        ...




                                               利用CursorLoader后台加载数据                                            

CursorLoader会在后台执行异步查询query,然后返回结果给调用它的Activity或者FragmentActivity,由于是异步的所以不会影响Activity中User的交互。

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

可以在onCreate或者onCreateView中初始化
    private static final int URL_LOADER_ID = 0;
    public View onCreateView(LayoutInflater inflater,ViewGroup viewGroup,Bundle bundle) {
        getLoaderManager().initLoader(URL_LOADER_ID, null, this);
        ...
    }
注意: getLoaderManager()只有在Fragment中可以调用,在FragmentActivity中需要用 getSupportLoaderManager()来获取 LoaderManager

  • 开始查询

初始化LoaderManager后,系统马上会回调 LoaderManager.LoaderCallbacks<Cursor>接口中的onCreateLoader()方法

在此方法中需要返回CursorLoader,可以在返回CursorLoader的同时就把查询的参数设定好,也可以先返回一个不带参数的CursorLoader

@Override
public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle)
{
    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;
    }
}

  • 处理查询结果

onCreateLoader方法返回非空的CursorLoader后,后台马上开始查询,当查询结束后,系统会回调onLoadFinished()方法,onLoadFinished()中的一个形参Cursor则包含了查询的结果。

除了onCreateLoader() onLoadFinished()之外,还需要实现接口中的另外一个抽象方法onLoaderReset(),当CursorLoader 发现查询结果与上次相比有变化时此方法会被系统回调,同时重新做查询操作,并通过onLoadFinished()返回查询结果。

如果是CursorAdapter,可以用以下方法,把查询结果显示到UI上:

@Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            mThemeAdapter.swapCursor(data);
        }

如果是SimpleCursorAdapter则是:

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
     mAdapter.changeCursor(cursor);
}

同时注意,为了防止内存泄漏需要在CursorLoader reset的时候删除旧Cursor的引用:
@Override
        public void onLoaderReset(Loader<Cursor> loader) {

            mThemeAdapter.swapCursor(null);
        }

或者SimpleCursorAdapter:

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.changeCursor(null);
}


管理设备的Awake(清醒)

Android设备长时间不用的时候,会进入睡眠状态:首先屏幕会变暗,然后会关闭屏幕,最后会关闭CPU,这样可以防止电源很快被耗尽,但是对于有些app,可能需要设备不同的行为:

  • 有些电影播放器app,可能需要始终保持屏幕在常亮状态
  • 还有一些并不需要屏幕保持常亮,但是在完成一些任务之前CPU不能关闭


保持屏幕常亮


某些app需要屏幕常亮,例如一些游戏、播放器app,最常用的方法是在activity中设置一个flag: FLAG_KEEP_SCREEN_ON(而不是在service等其他组件中设置)例如:

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 lock的方式,下面会讨论此方式)

这个方法还可以通过在app的layout文件中设置熟悉达到同样的效果,但是上面通过代码改动的方式更加灵活,可以随时设置开启或着关闭:

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


注意:只有在前台的app不想保持屏幕常亮时,需要清除 FLAG_KEEP_SCREEN_ON这个flag,至于app跳转到后台,或者又从后台返回前台,flag的状态可以放心交给Window manager处理,手动清除此flag的方法: getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) .

保持cup不关闭:


如果app需要让cpu不关闭,开发者可以利用 PowerManager中的系统服务,调用wake lock,wake lock可以允许app控制设备的进入睡眠行为
wake lock不当使用会严重影响电池寿命,所以应该尽量少的持有wake lock,而且不要再activity中使用wake lock,activity中使用 FLAG_KEEP_SCREEN_ON就可以了。
常见的场景是后台service 持有wake lock,使得cpu不关闭后台任务一直可以执行,但是又不需要屏幕常亮。
使用wakelock第一步是需要在AndroidManifest文件中添加 WAKE_LOCK  权限:
<uses-permission android:name="android.permission.WAKE_LOCK" />

直接申请wakelock的方式:
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
Wakelock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
        "MyWakelockTag");
wakeLock.acquire();
一定要记得,尽量早的用这个方法 wakelock.release()释放wake lock,防止消耗系统电量。


使用WakefulBroadcastReceiver

broadcast receiver 和Service配合使用,管理后台任务生命周期:
WakefulBroadcastReceiver是一个特殊的broadcast receiver,可以创建和管理app的PARTIAL_WAKE_LOCK
WakefulBroadcastReceiver把后台任务传递给Service(一般是一个IntentService),可以确保设备在执行后台任务的时候不进入睡眠状态,
使用WakefulBroadcastReceiver的第一步是把它加到AndroidManifest文件中:

<receiver android:name=".MyWakefulReceiver"></receiver>
下面的代码,用   startWakefulService()方法启动MyIntentService ,这个方法会让WakefulBroadcastReceiver在Service启动时申请 wake lock:

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);
    }
}

当Service完成后台工作时,调用MyWakefulReceiver.completeWakefulIntent()来释放wake lock,行参intent和传入的intent是同一个。

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);
    }
}

使用Alarm

Alarm机制基于AlarmManager类:

  • 可以定时发送intent
  • 可以结合广播接收者,来启动service,完成后台任务
  • 即使app不在运行,甚至设备处于睡眠状态,任然可以激活后台任务
  • 不需要后台service始终运行,也可以规划后台任务的执行
注意:此方法允许app生命周期结束之外,任然唤醒后台任务,如果在app的生命周期之内(如进程还没有被kill)那么可以用Handler,Timer,Thread的方式做定时任务。

最常用使用Alarm的场景是:在app的生命周期之外,需要同server同步数据(例如周期性同步天气数据),如果app有自己的服务器,那么可以考虑使用google的Google Cloud Messaging ,因为它能提供更好的灵活性。

原则:

如果需要每天重复请求网络数据,不要设置固定时间,例如每天11点,这样会导致服务器负载过重:
  • 如果周期性的请求网络记得添加一个随机数
  • alarm周期不要设置的过短
  • 不要毫无根据的唤醒系统(有些alarm类型可以把系统从睡眠状态唤醒)
  • alarm的触发时间尽量不要设置太细,例如尽量用setInexactRepeating() 而不是 setRepeating().因为使用setInexactRepeating(),系统会尽量把多个时间相近的alarm集中到一起触发系统,这样可以减少唤醒系统的次数,从而延长电池使用时间。
  • 尽量用ELAPSED_REALTIME类型alarm,而不要用real time clock,因为前者更加可靠

如何设置一个周期触发的Alarm
  1. 首先选择一个合适的alarm类型
  2. 设置好触发时间,如果时间是一个过去的时间点,那么alarm会被马上触发
  3. 设置alarm的间隔周期,例如,一天,一个小时,甚至5秒等
  4. 设置一个alarm到时后需要发送的pending intent,如果后来的alarm和前面的alarm设置的pending intent一样,那么前面的alarm会被后面的取代
如何选择合适的alarm类型:

根据触发的时间类型可以分为"elapsed real time"(消逝时间) 和 "real time clock" (RTC)(正式时间).根据字面意思也应该知道区别啦

两种类型的alarm都有一个wakeup版本,wakeup版本可以在系统睡眠的时候唤醒cpu,如果不用wakeup版本,那么alarm会在系统后续被其他因素唤醒时触发。

四个版本的alarm列举如下:

  • ELAPSED_REALTIME—基于系统boot后的时间轴,不会唤醒系统,包含系统睡眠时间
  • ELAPSED_REALTIME_WAKEUP基于系统boot后的时间轴,会唤醒系统,然后发出pending intent
  • RTC—基于系统实际时间,不会唤醒系统
  • RTC_WAKEUP基于系统实际时间,会先唤醒系统然后再发出pending intent

ELAPSED_REALTIME_WAKEUP 例子:

30分钟之后,如果系统睡眠就先唤醒系统,然后发出intent,每隔30分钟重复执行
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        AlarmManager.INTERVAL_HALF_HOUR,
        AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
一分钟之后如果 系统睡眠就先唤醒系统,然后发出intent,不重复
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 实例

每天两点, 如果 系统睡眠就先唤醒系统,然后发出intent,每天重复一次
// 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:30pm时刻发出intent,之后每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);

确定定alarm是否需要精细的时间执行



如何取消alarm

用cancel,参数用同一个intent即可:
if (alarmMgr!= null) {
    alarmMgr.cancel(alarmIntent);
}










 类似资料: