导读
增强的Doze模式
后台优化
Data Saver
一.增强的Doze模式
Android N对Android M引进的Doze模式进行了进一步的增强,变化体现在两个方面.一方面是降低了进入Doze模式的条件,Android M中的条件是不插电,屏幕熄灭且静置一段时间,在Android N中去掉了静置的条件,这个改变大大增加了设备进入Doze模式的机会,因而使得Doze对应用程序的影响大大增加.另一方面,Doze模式被分成了两个阶段,当设备切断电源,熄灭屏幕一段时间,会进入到第一阶段,切断网络连接,推迟任务和同步.如果设备在第一阶段的基础上再静置一段时间,就会进入第二阶段,在第一阶段的基础上增加对维持唤醒(PowerManager.WakeLock),定时任务(AlarmManager alarms),GPS和Wi-Fi扫描的限制,如下图所示:
First level
Second level
应对方案:
方案一:在Android 6.0中AlarmManager中增加了两个方法setAndAllowWhileIdle() and setExactAndAllowWhileIdle(),通过使用这两个方法可以让alarm在Doze模式下运行.
需要注意的是官方文档指出,使用这两个方法时,每个应用每9分钟只能唤醒一次alarm.
Note: Neither setAndAllowWhileIdle() nor setExactAndAllowWhileIdle() can fire alarms more than once per 9 minutes, per app.
以一个定时任务为例进行测试,核心代码如下:
Service:
在Service的onStartCommand()方法中,获取一个AlarmManager的实例,设置任务执行的时间是10s后,处理定时任务的广播接收器是AlarmReceiver.
public class LongRunningService extends Service {
public static final String TAG = "LongRunningService";
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
Log.i(TAG,"executed at " + new Date().toString());
}
}).start();
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int offset= 10 * 1000;//间隔时间10s
long triggerAtTime = SystemClock.elapsedRealtime() + offset;
Intent i = new Intent(this, AlarmReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, i, 0);
manager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);
// manager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);
return super.onStartCommand(intent, flags, startId);
}
}
BroadcastReceiver:
重写onReceive()方法,创建一个Intent,启动LongRunningService.这样一来,就形成了一个每隔10s执行一次的定时任务.
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(context, LongRunningService.class);
context.startService(i);
}
}
测试结果:
I/LongRunningService: executed at Thu Apr 14 22:32:58 GMT+08:00 2016
I/LongRunningService: executed at Thu Apr 14 22:33:08 GMT+08:00 2016
I/LongRunningService: executed at Thu Apr 14 22:33:18 GMT+08:00 2016
I/LongRunningService: executed at Thu Apr 14 22:42:18 GMT+08:00 2016
I/LongRunningService: executed at Thu Apr 14 22:51:18 GMT+08:00 2016
从测试结果可以看出,设备在正常使用的情况下(前三行),每隔10s运行一次,进入到Doze模式后(后三行),每隔9分钟执行一次.
为方便操作,这里介绍一下测试步骤:
Step1.运行应用程序
Step2.关闭设备的屏幕
Step3.使用如下命令强制系统进入Doze模式
$ adb shell dumpsys battery unplug
$ adb shell dumpsys deviceidle step
需要多次运行第二条命令,直到设备进入到空闲状态.
方案二:在应用程序运行时,引导用户将该应用添加到白名单中.
实现方式1(不推荐):使用ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS使用户跳转到电池优化设置页,手动将该应用添加到白名单中.
核心代码如下:
Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
startActivity(intent);
该方法可行,但存在一个缺点:跳转到电池优化页后,用户需要在”所有应用”列表里(应用安装后会默认设置为”优化”)找到该应用进行设置,而且系统提示会引导用户选择优化,如图所示:
实现方式2(推荐):给应用添加权限REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,并使用ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,系统会弹出设置窗口,用户可以直接将该应用添加到白名单中,如下图所示:
核心代码:
在清单文件中添加权限:
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
使用ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS作为参数创建一个Intent,并以"package:com.example.xxx.xxx"的Uri形式将包名传入.
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:com.example.janiszhang.dozedemo3"));
startActivity(intent);
方案三:使用GCM(Google Cloud Messaging ),该方法不可行,不再赘述.
二.后台优化
优化点1.针对预览版,应用不再接收静态注册的CONNECTIVITY_ACTION广播.但是应用在前台时仍然能够监听到动态注册的CONNECTIVITY_CHANGE广播.
优化点2.应用程序不能发送或者接收ACTION_NEW_PICTURE 和ACTION_NEW_VIDEO广播,这个优化会影响到所有应用,不只是针对预览版.
应对方案:
为了应对CONNECTIVITY_ACTION的变化所带来的影响,官方给出了两种缓解方案.
方案一:使用JobScheduler在无计量网络下调度网络任务.
核心代码:
Activity:
在使用JobInfo.Builder()创建JobInfo对象时,调用setRequiredNetworkType()方法,并将
JobInfo.NETWORK_TYPE_UNMETERED作为参数传递进去,这段代码的作用是当设备接入无计量网络时将调起MyJobService.
public static final int MY_BACKGROUND_JOB = 0;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void scheduleJob(Context context) {
JobScheduler js =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo job = new JobInfo.Builder(
MY_BACKGROUND_JOB,
new ComponentName(context, MyJobService.class))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.build();
js.schedule(job);
}
Service:
当条件满足时(在该例中为接入无计量网络),MyJobService中的回调方法onStartJob()将被执行,在实际业务中,可以在这里执行网络任务.
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class MyJobService extends JobService{
public static final String TAG = "MyJobService";
@Override
public boolean onStartJob(JobParameters jobParameters) {
Log.i(TAG, "on start job: " + jobParameters.getJobId());
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
Log.i(TAG, "on stop job: " + jobParameters.getJobId());
return false;
}
}
注意:
需要在清单文件中为该Service设置权限:android.permission.BIND_JOB_SERVICE”
<service android:name=".MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
方案二:在应用运行时监控网络连接
方式一:动态注册BroadcastReceiver,监听
“android.net.conn.CONNECTIVITY_CHANGE”
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG,"onReceive");
}
};
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
this.registerReceiver(broadcastReceiver,intentFilter);
方式二:使用ConnectivityManager
首先,使用NetworkRequest.Builder创建一个NetworkRequest对象,然后使用registerNetworkCallback()把这个NetworkRequest对象传递给系统.当网络条件被满足时,应用将收到一个回调去执行定义在ConnectivityManager.MetworkCallback类中的onAvailable()方法.
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder builder = new NetworkRequest.Builder();
builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
NetworkRequest networkRequest = builder.build();
ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
super.onAvailable(network);
Log.i(TAG, "onAvailable");
}
};
connectivityManager.registerNetworkCallback(networkRequest, networkCallback);
三.Data Saver
当用户在计量网络下启用数据节约功能时,系统会*后台数据的使用,运行在前台的应用也会尽量少的使用数据流量.用户可以使用白名单允许指定的应用在数据节约模式下使用后台数据.Android N开发者预览版中扩展了ConnectivityManager API的能力,向用户提供了查看和监控数据节约设置的接口.
检测Data Saver首选项的变化
动态注册监听ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED ("android.net.conn.RESTRICT_BACKGROUND_CHANGED")的广播接收者.
核心代码:
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Data Saver Changed");
}
};
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.RESTRICT_BACKGROUND_CHANGED");
this.registerReceiver(broadcastReceiver,intentFilter);
注意:必须使用动态注册的方式才能够监听到该广播,不能在清单文件中静态注册.
检查数据节约设置
ConnectivityManager的getResrictBackgroundStatus()方法的返回值如下:
RESTRICT_BACKGROUND_STATUS_DISABLED
禁用数据节约
RESTRICT_BACKGROUND_STATUS_ENABLED
启用数据节约
RESTRICT_BACKGROUND_STATUS_WHITELISTED
用户启用了数据节约,但是该应用在白名单中,故不受限制.
下面的示例代码来自官方文档,给出了使用ConnectivityManager.isActiveNetworkMetered()
和ConnectivityManager.getRestrictBackgroundStatus()来判断当前Data Saver设置状态的方法.
注意:这段代码目前还不能使用,RESTRICT_BACKGROUND_STATUS_ENABLED等三个状态值尚不可用.
使用adb命令进行测试
$ adb shell dumpsys netpolicy
生成一个报告,包括当前的全部后台网络限制的设置,白名单中的包的UID,其他已知包的网络限制.
$ adb shell cmd netpolicy
显示一个完整的网络策略管理命令列表.
$ adb shell cmd netpolicy set restrict-background <boolean>
启用或者禁用数据节约命令
$ adb shell cmd netpolicy add restrict-background-whitelist <UID>
将指定的包的UID加入白名单中
$ adb shell cmd netpolicy remove restrict-background-whitelist <UID>
从白名单中将指定包的UID移除