针对 Android 9 或更高版本并使用前台服务的应用必须请求 FOREGROUND_SERVICE 权限。 这是普通权限,因此,系统会自动为请求权限的应用授予此权限。
系统将动态分配各个应用至不同分组。系统或会通过利用机器学习预加载的应用,从而预测各个应用的使用概率,然后将它们编配至相应的群组中。若设备中没有安装此类系统应用,在默认情况下,系统会根据应用的近期使用情况进行等级划分。应用活跃度越高,所处分组的优先级就越高,也就相应地更容易获取设备资源。尤其是,应用所处的的群组决定了其所安排的job,触发AlarmManager以及接受高优先级FCM的频率。这些限制仅在非充电状态下才有效;当设备充电时,应用并不会受到系统限制。
在job、AlarmManager、FCM信息的资源调用上无任何系统限制。
应用的运行频率很高,但目前并未处于“活跃”状态;或被间接使用的应用。
在job、AlarmManager部分限制
经常使用但不是每天使用的应用,如打卡应用。
在job、AlarmManager部分限制,接受的高优先性FCM消息也有数量上限
应用的使用频率很低
在job、AlarmManager、FCM信息的资源调用上受严格限制,网络访问能力也会受到影响
安装但是从未运行过的应用会被归到“从未使用”群组中。 系统会对这些应用施加极强的限制。
当系统监测到应用消耗过多资源时,系统会通知并询问用户是否需要限制该应用的后台活动。
目前有以下两种情况会触发系统发送此通知:
比如:在AOSP构建上存在以下系统限制:
在后台运行的服务在几分钟内会被stop掉,推荐可使用AlarmManager、SyncAdapter、JobScheduler代替后台服务
Android 8.0 有一项复杂功能:系统不允许后台应用创建后台 Service。 因此,Android 8.0 引入了一种全新的方法,即 startForegroundService(),以在前台启动新 Service。 在系统创建 Service 后,应用有五秒的时间来调用该 Service 的 startForeground() 方法以显示新 Service 的用户可见通知。 如果应用在此时间限制内未调用 startForeground(),则系统将停止此 Service 并声明此应用为 ANR。
在很多情况下,您的应用都可以使用 JobScheduler 作业替换后台服务。
JobScheduler 仍然为这些首先的广播提供了功能上的补充。
在6.0的基础上进行了优化,把其中的限制分为了两步进行:
在应用清单静态注册这几项广播将不再有效:
JobScheduler 中提供了网络相关API对CONNECTIVITY_ACTION进行补充。
当设备未连接至电源,且长时间处于闲置状态时,系统会将应用进入Doze。
限制:
如何解除限制: 加入低功耗模式白名单, 可以豁免对网络和wake locks的限制
通过监听一些全局的静态广播,比如开机广播、解锁屏广播、网络状态广播等,来启动应用的后台服务。目前,在高版本的Android系统中已经失效,因为高版本的Android系统规定应用必须在系统开机后运行一次才能监听这些系统广播,一般而言,应用被系统杀死后,基本无法接收系统广播。
以前提高Service优先级方法很多,比如onStartCommand返回START_STICKY使系统内存足够的时候Service能够自动启动、弹出通知、配置service的优先级等,这些方式只能在一定程度上缓解service被立马回收,但只要用户一键清理或者系统回收照样无效。
还有一种方法就是在设置一种全局定时器,定时检测启动后台服务,但这种方法目前也已经无效,因为应用只要被系统杀死,全局定时器最后也只成了摆设。
经过测试,只要当前应用被杀,任何后台service都无法运行,也无法自行启动。
使用NDK在底层fork出一个子进程,来实现与父进程之间的互拉。在Android4.x还是非常有效的,Android 5.0 以上,系统杀进程以 uid 为标识,通过杀死整个进程组来杀进程,因此最后导致双进程无法拉起。
当应用X绑定保活助手A时(浅绿色字体),如果保活助手A被系统杀死,应用X的onServiceDisConnected被回调,我们可以在该方法中执行bindService方法再次尝试绑定唤醒保活助手A;
当保活助手A绑定应用X时(橙色字体),如果应用X被系统杀死,保活助手A的onServiceDisconnected被回调,我们可以在该方法中执行bindService方法再次尝试绑定唤醒应用X。
两次强杀应用X,应用X就无法启动了,这是因为当应用X“体积”较大,启动前需要加载诸如大量的静态变量或者Application类中的变量等,导致启动较慢,当我第一次强杀应用X时,保活助手A是执行绑定启动应用X保活服务的,但我继续第二次强杀应用X时,保活助手A可能还未与应用X绑定,最终导致保活助手A无法检查应用X的绑定状态而失效。
针对单进程守护出现的问题,当应用X“体积”较大时,我们可以采用双进程守护,即实现两个保活助手,它们彼此双向绑定来对应用X进行守护。我们采用“环”的形式来进行互拉,无论谁被杀死,只要系统杀掉剩余的任何一个进程,最后活着的进程都能够将其他被杀进程拉起来。当然,这里还有个小技巧,为了防止两个保活助手进程同时被系统杀死,我这里采取高低优先级的方式来解决。
注: 当上述两个进程长时间运行在后台时还是有可能被系统杀死,以致无法实现保活的目的。当时猜想,系统在回收进程时很可能是按顺序回收的,当这两个进程顺序比较接近,或者说内存中可能就只有这两个进程,那么系统在回收的时候一次性将其干掉了。为了缓解这种情况,我采取了一种高低优先级的方式来尽量保证系统不会同一时间回收两个进程,只要有了这个时间差,两个进程就能够实现互相启动保活的目的。
进程在内存中时活动主要有五种状态: 前台进程、可见进程、服务进程、后台进程、空进程,这几种状态的进程优先级由高到低,oom_adj值由低到高(在ProcessList定义)
优先级 | 进程状态 | oom_adj | 特性与场景 |
---|---|---|---|
1 | 前台进程 | 0 | 正在与用户交互,除深度定制ROM或系统错误情况,系统不会杀死该进程; 1)正在交互(onResume),1像素保活原理; 2)与正在交互的Activity绑定的Service; 3)startForeground启动的前台进程,前台Service保活原理; 4)正在执行生命周期某个方法的Service,播放无声音乐保活原理5)BroadcastReceiver执行onReceive |
2 | 可见进程 | 1 | 进程可见但不能交互 1)进程持有onPause的Activity; 2)进程持有与可见Service绑定的Service |
3 | 服务进程 | 5 | 进程运行着由startService启动且不与任何Activity绑定的Service,且不属于前台进程和可见进程 |
4 | 后台进程 | 6 | 进程持有一个Activity在onStop并未onDestroy |
5 | 空进程 | 9-25 | 该状态下的进程不包含任何活跃的组件,他只是作为缓存的形式存在,以加快进程的启动速度 |
通过使用 startForeground()方法将当前Service置于前台来提高Service的优先级,API大于18时 startForeground()方法需要弹出一个可见通知,可以开启另一个Service将通知栏移除;在onStartCommand方法中返回START_STICKY,作用是当Service进程被kill后,系统会尝试重新创建这个Service;在onDestory方法中重新启动自己
SinglePixelActivity通过getWindow设置属性;onDestroy中重启自己;
android:launchMode="singleInstance"
android:excludeFromRecents="true"//用于控制SinglePixelActivity不在最近任务列表中显示
android:finishOnTaskLaunch="false" //用于标记当用户再起启动应用(TASK)时是否关闭已经存在的Activity的实例,false表示不关闭
android:configChanges="keyboardHidden|orientation|screenSize|navigation|keyboard" //防止Activity重启
android:theme 设置窗体背景颜色为透明、没有边框、不包含标题栏、悬浮、切换无动画、禁用预览动画、不允许背景变暗、自定义TitleBar时去掉多余的阴影等
业务Activity,android:launchMode="singleTask",注册监听锁屏广播,接到锁屏广播,将自身切换到可见模式(启动1像素)
同1),启动Service并播放音频
JobScheduler是谷歌在Android 5.0引入的一个能够执行某项任务的API,它允许APP在将来达到一定条件时执行指定的任务。通常情况下,即使APP被强制停止,预定的任务仍然会被执行。
首先在一个实现了JobService的子类的onStartJob方法中执行这项任务,使用JobInfo的Builder方法来设定条件并和实现了JobService的子类的组件名绑定,然后调用系统服务JobScheduler的schedule方法。这样,即便在执行任务之前应用程序进程被杀,也不会导致任务不会执行,因为系统服务JobScheduler会使用bindServiceAsUser的方法把实现了JobService的子类服务启动起来,并执行它的onStartJob方法。
需要BIND_JOB_SERVICE权限
具体代码在文章末尾
Doze:即休眠、打盹之意。是谷歌在Android M(6.0)提出为了延长电池使用寿命的一种节能方式,它的核心思想是在手机处于屏幕熄灭、不插电或静止不动一段时间后,手机会自动进入Doze模式。处于Doze模式的手机将停止所有非系统应用的WalkLocks、网络访问、闹钟、GPS/WIFI扫描、JobSheduler活动。当进入Doze模式的手机屏幕被点亮、移动或充电时,会立即从Doze模式恢复到正常,系统继续执行被Doze模式"冷冻"的各项活动。Doze模式不会杀死进程,只是停止了进程相关的耗电活动,使其进入"休眠"状态。至Android N(即Android 7.0)后,谷歌进一步对Doze休眠机制进行了优化,休眠机制的应用场景和使用规则进行了扩展。Doze在Android 6.0中需要将手机平行放置一段时间才能开启,在7.0中则可随时开启。
从 Android 6.0 开始,系统为了省电增加了休眠模式,系统待机一段时间后,会杀死后台正在运行的进程。但系统会有一个后台运行白名单,白名单里的应用将不会受到影响,在原生系统下,通过:「设置」 - 「电池」 - 「电池优化」 - 「未优化应用」,可以看到这个白名单。
JobScheduler的具体实现
/**JobService,支持5.0以上forcestop依然有效 */
@TargetApi(21)
public class AliveJobService extends JobService {
private final static String TAG = "KeepAliveService";
// 告知编译器,这个变量不能被优化
private volatile static Service mKeepAliveService = null;
public static boolean isJobServiceAlive(){
return mKeepAliveService != null;
}
private static final int MESSAGE_ID_TASK = 0x01;
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// 具体任务逻辑
if(SystemUtils.isAPPALive(getApplicationContext(), Contants.PACKAGE_NAME)){
Toast.makeText(getApplicationContext(), "APP活着的", Toast.LENGTH_SHORT)
.show();
}else{
Intent intent = new Intent(getApplicationContext(), SportsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Toast.makeText(getApplicationContext(), "APP被杀死,重启...", Toast.LENGTH_SHORT)
.show();
}
// 通知系统任务执行结束
jobFinished( (JobParameters) msg.obj, false );
return true;
}
});
@Override
public boolean onStartJob(JobParameters params) {
if(Contants.DEBUG)
Log.d(TAG,"KeepAliveService----->JobService服务被启动...");
mKeepAliveService = this;
// 返回false,系统假设这个方法返回时任务已经执行完毕;
// 返回true,系统假定这个任务正要被执行
Message msg = Message.obtain(mHandler, MESSAGE_ID_TASK, params);
mHandler.sendMessage(msg);
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
mHandler.removeMessages(MESSAGE_ID_TASK);
if(Contants.DEBUG)
Log.d(TAG,"KeepAliveService----->JobService服务被关闭");
return false;
}
}
/**JobScheduler管理类,单例模式,执行系统任务 */
public class JobSchedulerManager {
private static final int JOB_ID = 1;
private static JobSchedulerManager mJobManager;
private JobScheduler mJobScheduler;
private static Context mContext;
private JobSchedulerManager(Context ctxt){
this.mContext = ctxt;
mJobScheduler = (JobScheduler)ctxt.getSystemService(Context.JOB_SCHEDULER_SERVICE);
}
public final static JobSchedulerManager getJobSchedulerInstance(Context ctxt){
if(mJobManager == null){
mJobManager = new JobSchedulerManager(ctxt);
}
return mJobManager;
}
@TargetApi(21)
public void startJobScheduler(){
// 如果JobService已经启动或API<21,返回
if(AliveJobService.isJobServiceAlive() || isBelowLOLLIPOP()){
return;
}
// 构建JobInfo对象,传递给JobSchedulerService
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,new ComponentName(mContext, AliveJobService.class));
// 设置每3秒执行一下任务
builder.setPeriodic(3000);
// 设置设备重启时,执行该任务
builder.setPersisted(true);
// 当插入充电器,执行该任务
builder.setRequiresCharging(true);
JobInfo info = builder.build();
//开始定时执行该系统任务
mJobScheduler.schedule(info);
}
@TargetApi(21)
public void stopJobScheduler(){
if(isBelowLOLLIPOP())
return;
mJobScheduler.cancelAll();
}
private boolean isBelowLOLLIPOP(){
// API< 21
return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
}
}
<--! AliveJobService需要BIND_JOB_SERVICE权限-->
<service
android:name=".service.AliveJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>