后台服务以及电量监控的最佳实践

作者: CyrusChan | 来源:发表于2017-07-10 19:42 被阅读244次

    原文地址

    创建一个后台服务:

    IntentService类提供一个直接的结构对于一个单独后台线程执行操作。这就允许它处理长时间的操作而不影响用户交互接口。并且,一个IntentService不被多数用户接口循环事件影响,所以持续运行的环境中应该关掉AsyncTask。

    一个IntentService有如下限制:
    • 不能直接和用户接口交互。为了把它的结果交给UI,你应该把它们发送给一个Activity。
    • 工作请求顺序的执行。如果一个操作在IntentService中执行,并且你发送另一个请求,请求一直等待上一个操作执行完。
    • 在IntentService中执行的操作能够被打断。

    然而,在大多数情况下,IntentService是首选的方式对于简单的后台操作。

    这节课像你展示如何创建你自己的IntentService的子类。课程也像你展示了如何创建一个需要的回调方法onHandleIntent().最后,课程描述了如何在manifestfile定义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
            ...
        }
    }
    

    注意其他的常规Service组件的回调方法,例如onStartCommand()自动被IntentService调用,你应该避免重写这些回调。

    在Manifest中定义IntentService

    <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使用确切的Intent发送请求到service,所有不需要过滤器。这也意味着在同一应用或者有相同用户Id的应用可以访问这个服务。

    发送工作请求到后台服务

    创建和发送一个工作请求到一个IntentService

    为了创建一个工作请求并且发送它到一个IntentService,创建一个明确的Intent,并且为它添加请求数据,并且通过调用startService()发送给IntentService。

    1. 创建一个新的、确切的Intent为IntentService调用RSSPullService
    /*
     * 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));
    
    1. 调用startService()
    // Starts the IntentService
    getActivity().startService(mServiceIntent);
    

    注意你可以发送工作请求从任何地方,Activity、Fragment。例如,如果你需要先获取用户输入,你可以发送请求从一个涉及到按键点击或者简单手势的回调当中。
    一旦你调用了startService(),IntentService执行onHandleIntent()方法定义的工作,接着停止它自己。

    汇报工作状态:

    从IntentService中汇报工作状态

    为了从IntentService中发送工作请求的状态到其他组件,首先创建一个Intent。作为可选项,你可以增加action和data Uri到这个Intent。
    下一步,发送Intent通过调用 LocalBroadcastManager.sendBroadcast().这发送Intent到任何你应用中注册接收它的组件。调用getInstance()可以拿到本地广播的实例。
    例如:

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

    下一步是在组件中处理到来的广播Intent对象。
    接收状态从一个IntentService
    例如:

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

    一旦你已经定义广播接收器,你能够定义为它定义过滤器去匹配特定的actions,categories和data。如下展示如何定义一个过滤器:

    // 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 statusIntentFilter = new IntentFilter(
                    Constants.BROADCAST_ACTION);
    
            // Adds a data filter for the HTTP scheme
            statusIntentFilter.addDataScheme("http");
            ...
    

    为了注册广播接收器和过滤器,拿到本地广播的实例并且调用注册接收器的方法。如下所示:

      // Instantiates a new DownloadStateReceiver
            DownloadStateReceiver mDownloadStateReceiver =
                    new DownloadStateReceiver();
            // Registers the DownloadStateReceiver and its intent filters
            LocalBroadcastManager.getInstance(this).registerReceiver(
                    mDownloadStateReceiver,
                    statusIntentFilter);
            ...
    

    一个单独的广播接收器能够处理超过一种类型的广播对象。如下:

      /*
             * 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,
                    statusIntentFilter);
    

    发送一个广播Intent不会start或者resume actvitiy。Activity的BroadcastReciver接收和处理Intent对象即使当你的应用在后台,但是不强制你的应用到前台。如果你想通知用户关于发生在后台的事件当你的应用不可见的时候,使用Notification。永远不要start一个Activity去回应到来的广播Intent

    管理设备的唤醒状态

    当你的设备空闲的时候,它先变昏暗,接着关闭屏幕,最终关闭CPU。这防止电量被过早榨干。然而有时你的应用可能需要一个不同的表现。
    游戏应用或者电影应用可能需要保持屏幕亮着。
    其他应用可能不需要保持屏幕亮着,但是他们需要CPU持续运行直到关键的操作完成。
    这节描述如何保持一个设备唤醒当需要的时候而不榨干它的电量

    保持设备唤醒

    某些应用需要保持屏幕开启,例如游戏或者电影应用。最好的方式是使用 FLAG_KEEP_SCREEN_ON 在你的应用当中(只在activity当中,不要在一个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 locks(详见Keep the CPU On),它不需要请求特殊的权限,平台妥善的管理用户在不同应用之间的切换,而你的应用不需要担忧释放不需要的资源。
    另一种方式是在你应用的layout XML文件中,通过使用 android:keepScreenOn属性:

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

    注意:你不需要清掉FlAG_KEEP_SCREEN_ON标签除非你不再希望屏幕保持亮在你运行的应用当中(例如,如果你希望屏幕一段确切的时间不活动熄灭)。Window manager确保正确的事情发生当应用到后台或者返回前台。但是你想明确的清楚标签并且允许屏幕再次关闭,使用clearFlags():
    getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

    保持CPU工作:

    如果你需要保持CPU工作为了完成某些任务在设备睡眠之前,你可以使用PowerManager系统服务调用wake locaks,Wake locks允许你的应用控制设备的电池状态。
    创建并且持有wake locks会有剧烈的影响在设备电池的寿命上。因此你应该使用wake locks仅当严格需要并且保持尽可能短的时间。例如,你不应该使用wake lock 在一个activity中。
    一个合法的情况是使用wake lock 可能是后台服务需要获得wake lock让cpu 持续工作当屏幕关闭的时候,而且,这种实践应该最小化由于电池的寿命。
    使用Wake lock,第一步添加wake lock的权限到你应用的manifest文件中:

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

    如果你的应用包含一个使用service做一些工作广播接收器,你可以管理你的wake lock通过一个WakefulBroadcastReceiver.如 Using a WakefulBroadcastReceiver 描述的,这是首选的方式。如果你的应用不跟从这个规则,你可以这样直接使用:

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

    调用wakelock.release()释放wake lock。尽快释放wake lock 是非常重要的,避免它榨干你的电量。

    使用WakefulBroadcastReciver

    使用broadcast reciver和service管理后台任务的生命周期。
    WakefulBroadcastReceiver是一种特殊类型的广播接收器,为你的app关注创建和管理PARTIAL_WAKE_LOCK。WakefulBroadcastReceiver把工作传到Service(典型的IntentService),当确保设备不会在事务中入睡。如果你不持有一个wake lock当过渡工作到一个服务中,你实际上是允许设备休眠在工作完成之前。应用的网络结果可能没有完成工作直到未来一些任意的点,这并不是你想要的。
    使用WakefulBroadcastReceiver是添加它到你的manifest中,像其他广播接收器那样:

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

    如下代码start MyIntentService通过方法startWakefulService()。这个方法与startService()相等,WakefulBroadcastReceiver持有一个wake lock当serivce开始的时候。startWakefulService()传入的Intent中持有一个额外的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。completeWakefulIntent()方法有像它参数一样的从 WakefulBroadcastReceiver被传入的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);
        }
    

    监控电池电量和充电状态

    如果您要通过改变后台更新的频率来减少这些更新对电池寿命的影响,最好先从检查当前电池电量和充电状态入手。
    执行中的应用更新对电池寿命的影响取决于设备的电池电量和充电状态。 设备通过交流电源充电时执行更新的影响可以忽略不计,因此在大多数情况下,只要设备连接了壁式充电器,您就可以将更新频率提高到最高水平。 相反,如果设备正在放电,降低更新频率有助于延长电池寿命。
    同理,您也可以检查电池充电电量,并在电池电量近乎耗尽时降低更新频率,甚至停止更新
    确定当前充电状态
    首先,确定当前充电状态。BatteryManager 在一个包含充电状态的粘性 Intent 中广播所有电池和充电详情。
    由于它是一种粘性 Intent,因此您并不需要如下一代码段中所示的那样通过简单地调用 registerReceiver传入 null 作为接收器来注册BroadcastReceiver,便可返回当前电池状态 Intent。您可以在此处传入实际 BroadcastReceiver 对象,但由于稍后我们将会处理更新,因此并不需要这样做。

    IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    Intent batteryStatus = context.registerReceiver(null, ifilter);
    

    您可以提取当前充电状态,并且如果设备正在充电,则还可以提取设备是通过 USB 还是交流充电器进行充电。

    // Are we charging / charged?
    int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
    boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                         status == BatteryManager.BATTERY_STATUS_FULL;
    
    // How are we charging?
    int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
    boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
    boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
    

    通常,如果设备连接了交流充电器,您应该最大限度提高后台更新的频率;而如果设备是通过 USB 充电,则应降低更新频率,如果电池正在放电,则应进一步降低更新频率。
    监控充电状态变化
    就像设备可以轻松地插入电源,充电状态也很容易发生变化,因此必须监控充电状态的变化并相应地改变更新频率。
    每当设备连接或断开电源时,BatteryManager 都会广播一个操作。 必须接收这些事件,即便您的应用并未运行 — 尤其要考虑到这些事件可能会影响您启用应用以便发起后台更新的频率 — 因此您应该在清单文件中注册一个 BroadcastReceiver,通过在一个 Intent 过滤器内定义ACTION_POWER_CONNECTED 和 ACTION_POWER_DISCONNECTED 来同时侦听这两种事件。

    <receiver android:name=".PowerConnectionReceiver">
      <intent-filter>
        <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
        <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
      </intent-filter>
    </receiver>
    

    在关联的 BroadcastReceiver 实现内,您可以按上一步所述提取当前充电状态和方法。

    public class PowerConnectionReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
            boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                                status == BatteryManager.BATTERY_STATUS_FULL;
    
            int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
            boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
            boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
        }
    }
    

    确定当前电池电量

    在某些情况下,确定当前电池电量也很有用处。您可以选择在电池电量低于某一水平时降低后台更新的频率。
    您可以如以下所示,通过从电池状态 Intent 提取当前电池电量和刻度来了解当前电池电量:

    int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
    int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    
    float batteryPct = level / (float)scale;
    

    监控显著的电池电量变化

    您无法轻松地持续监控电池状态,您也不必如此。
    一般而言,持续监控电池电量对电池的影响大于对应用正常行为的影响,因此最好只监控显著的电池电量变化—特别是在设备进入或退出电量不足状态时。
    以下清单文件代码段摘自某个广播接收器内的 Intent 过滤器元素。 通过侦听 ACTION_BATTERY_LOW 和 ACTION_BATTERY_OKAY,每当设备电池电量不足或退出不足状态时,便会触发该接收器。

    <receiver android:name=".BatteryLevelReceiver">
    <intent-filter>
      <action android:name="android.intent.action.ACTION_BATTERY_LOW"/>
      <action android:name="android.intent.action.ACTION_BATTERY_OKAY"/>
      </intent-filter>
    </receiver>
    

    一般而言,最好在电池电量极低时停用所有后台更新。 如果手机在您利用数据前就已自行关机,数据的新鲜度则无关紧要。
    在许多情况下,为设备充电与将设备插入基座是同一操作。下一节课为您介绍如何确定当前基座状态以及如何监控设备插接状态的变化。
    确定和监控插接状态和基座类型
    Android 设备可插接到几个不同种类的基座,其中包括汽车或家用基座以及数字和模拟基座。 插接状态通常与充电状态联系密切,因为许多基座都为插接的设备供电。
    手机插接状态对更新频率的影响取决于您的应用。您可以选择在手机插入桌面基座时提高体育中心应用的更新频率,或者在设备插入车载手机座时完全停用更新。 相反,如果您的后台服务正在更新交通状况,则您可以选择在已插接车载基座的情况下最大限度提高更新频率。
    插接状态也以粘性 Intent 形式广播,以便您查询设备是否已插接以及已插接情况下的插接类型。

    确定当前插接状态

    插接状态详情以 extra 形式包含在 ACTION_DOCK_EVENT 操作的粘性广播中。由于它是粘性广播,因此您无需注册 BroadcastReceiver。如下一段代码中所示,您只需调用 registerReceiver(),将 null 作为广播接收器传入。

    IntentFilter ifilter = new IntentFilter(Intent.ACTION_DOCK_EVENT);
    Intent dockStatus = context.registerReceiver(null, ifilter);
    您可以从 EXTRA_DOCK_STATE extra 中提取当前插接状态:
    int dockState = battery.getIntExtra(EXTRA_DOCK_STATE, -1);
    boolean isDocked = dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
    

    确定当前基座类型


    如果设备已插接,其插接的基座可能为以下四种不同类型之一:
    • 车载基座
    • 桌面基座
    • 低端(模拟)桌面基座
    • 高端(数字)桌面基座
    请注意,后两种类型从 Android 的 API 级别 11 才开始引入,因此如果您只关注基座类型而不关心其具体为数字还是模拟形式,则最好检查所有三种类型:

    boolean isCar = dockState == EXTRA_DOCK_STATE_CAR;
    boolean isDesk = dockState == EXTRA_DOCK_STATE_DESK ||
                     dockState == EXTRA_DOCK_STATE_LE_DESK ||
                     dockState == EXTRA_DOCK_STATE_HE_DESK;
    

    监控插接状态或基座类型变化


    每当设备插入或拔出基座时,都会广播 ACTION_DOCK_EVENT 操作。如需监控设备的插接状态变化,只需如下面的代码段所示,在您的应用清单文件中注册一个广播接收器:
    <action android:name="android.intent.action.ACTION_DOCK_EVENT"/>
    您可以使用上一步中介绍的相同技巧提取接收器实现内的基座类型和插接状态。
    确定和监控连接状态
    重复闹铃和后台服务的一些最常见用途是安排定期从互联网资源、缓存数据更新应用数据,或者执行长时间下载。但如果您未连入互联网,或者因连接速度过慢而无法完成下载,何苦要唤醒设备来安排更新呢?
    您可以利用 ConnectivityManager 来检查是否已实际连入互联网以及已连入情况下的连接类型。
    确定您是否连入了互联网


    如果您未连入互联网,则无需安排基于互联网资源的更新。 下面这段代码展示了如何利用 ConnectivityManager 查询活动网络并确定其是否连入了互联网。

    ConnectivityManager cm =
            (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
    
    NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
    boolean isConnected = activeNetwork != null &&
                          activeNetwork.isConnectedOrConnecting();
    

    确定您的互联网连接类型


    还可以确定当前可用的互联网连接类型。
    设备连接可由移动数据、WiMAX、Wi-Fi 和以太网连接提供。如下所示,您可以通过查询活动网络的类型,根据可用带宽改变更新频率。

    boolean isWiFi = activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;
    

    移动数据成本往往远高于 Wi-Fi,因此在大多数情况下,使用移动连接时应降低您的应用的更新频率。 同理,您应在接入 Wi-Fi 后再进行大数据量下载。
    停用更新后,您必须侦听连接变化,以便在建立互联网连接后立即恢复更新。
    监控连接变化


    每当连接详情发生变化时,ConnectivityManager 便会广播 CONNECTIVITY_ACTION ("android.net.conn.CONNECTIVITY_CHANGE") 操作。您可以在清单文件中注册一个广播接收器,以便侦听这些变化和相应地恢复(或暂停)后台更新。

    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
    

    设备的连接变化可能非常频繁—您每次在移动数据与 Wi-Fi 之间切换时都会触发该广播。 因此,最好只在您之前暂停过更新或下载时监控该广播,以便恢复这些更新或下载。通常,只要在开始更新前检查互联网连接即已足够,如果没有任何连接,则再暂停其他更新直至连接恢复。

    相关文章

      网友评论

        本文标题:后台服务以及电量监控的最佳实践

        本文链接:https://www.haomeiwen.com/subject/fjyuhxtx.html