目录
- 保活功能相关基础内容
1.1 进程优先级介绍
1.2 系统回收进程内存机制LMS简介
1.3 查看oom_adj的方法 - 进程保活的关键保活和复活
2.1 保活分析
2.2 在什么情况下进程会被杀死
2.3 保活常用手段
2.4 复活常用方法 - 具体保活方案的实现过程
3.1 单Service的提高进程的优先级
3.2通过监听锁屏和开屏广播,使用“1”像素Activity提升优先级
3.3通过JobScheduler的方式复活Service
3.4通过在后台播放无声的音乐
3.5 双进程守护方案
3.6 双App相互拉活方案 - 保活方案实现效果统计
4.1 双进程守护方案
4.2 监听锁屏广播打开1像素Activity
4.3 后台播放无声的音乐
4.4 使用JobScheduler唤醒Service
4.5 混合使用的效果,并且在通知栏弹出通知 - 总结
如何跳转: Ctrl + F 输入章节标号即可 (例: 4.1)
前言
保活是什么,简单的说就是让你的App不会被轻易杀死,一直留存在用户的后台去自动执行一些关于应用程序业务需求的相关逻辑(如实时传送位置、实时消息的接收)。
但是Android 系统为了保持系统运行流畅,在内存不足时,会将一些进程 kill ,以释放一部分内存。但是有些产品是有即时性的,在收到消息、推送等都是要立刻通知到用户。由此就出现了android的种种黑科技和奇葩操作来保障App的存活。
本文总结了当前保活圈里最常用的方法,其中也含有大厂用到过的方法。并且在本文探索的过程中梳理了关于保活内容的相关知识点(进程种类,AIDL,如何查看oom_adj等),还有不同的手机品牌在不同系统版本、不同的环境下都有什么样的表现,都有说明。无论是刚刚开始探索这个功能的小白,还是已经在保活圈里摸爬滚打的大佬都适合收藏。 文中若有不足之处,还请多多指教修改。
1.保活功能相关基础内容:
1.1进程优先级
Android一般的进程优先级划分:
1.前台进程 (Foreground process)
2.可见进程 (Visible process)
3.服务进程 (Service process)
4.后台进程 (Background process)
5.空进程 (Empty process)
1.1.1 前台进程
用户当前操作的进程。一个进程满足以下任一条件 ,即视为前台进程:
- 托管用户正在交互的 Activity(已调用 onResume() 方法)。
- 托管某个 Service ,且 Service 绑定到用户正在交互的 Activity。
- 托管正在“前台”运行的 Service(服务已调用startForeground())。
- 托管正在执行生命周期回调的 Service( onCreate() 、 onStart() 或 onDestory() )。
- 托管正在执行 onReceive() 方法的 BroadcastReceiver。
通常,任意时间的前台进程数据都不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会 kill 它们。
1.1.2可见进程
没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:
- 托管不在前台、但仍对用户可见的 Activity(已调用 onPause() 方法)。如:前台 Activity 启动了一个对话框,允许在其后面显示上一个 Activity。
- 托管绑定到可见(或前台)的 Activity 的 Service。
可见进程被视为及其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会kill这些进程。
1.1.3 服务进程
正在运行已使用 startService() 方法启动的 Service 且不属于上述两个更高类别进程的进程。
尽管服务进程与用户可见内容没有直接关联,但是它们通常在执行一些用户比较关心的操作(如:在后台播放音乐或从网络下载数据等),因此,除非内部不足以维持所有前台进程和可见进程同时运行,否则系统不会 kill 这些进程。
1.1.4 后台进程
托管目前对用户不可见的 Activity 的进程
- 已调用 Activity 的 onStop() 方法
后台进程对用户体验没有直接影响,系统可能随时会 kill 它们,以回收内存提供给前台进程、可见进程、服务进程使用。通常会有很多后台进程同时运行,系统将它们保存在 LRU(最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。
1.1.5 空进程
不包含任何活动组件的进程。
保留这种进程的唯一目的是缓存,以缩短下次在其中运行的组件的启动时间。为使系统总体资源在进程缓存和底层内核缓存之间保持平衡,系统往往会kill这些进程。
1.2 Android 系统回收进程内存的机制 LMS
LMS( Low Memory Killer )机制,是一种根据 oom_adj 阈值级别触发相应力度的内存回收的机制。oom_adj 代表进程的优先级,数值越高,优先级越低,越容易被杀死。
- 红色部分是容易被回收的进程,属于android进程
- 绿色部分是较难被回收的进程,属于android进程
- 其他部分则不是android进程,也不会被系统回收,一般是ROM自带的app和服务才能拥有
1.3 查看oom_adj的方法
查看oom_adj的前提是手机必须是root后的,如果没有root的小伙伴先将手机root后再进行这项操作。
第一步
首先要获取到想要查看的pid,简单一点就是直接查看看图,通过android studio就可以查看当前pid。括号中的14850和14897
如果想查看当前手机中所有的所有pid,则在cmd或者Terminal 输入
adb shell ps
即可查看当前手机运行的所有pid,包括系统的。
第二步:
首先输入
adb shell
然后输入
su
(用su可以提权,直接执行su就会看到用户命令提示符由”$”变成了”#”,如果手机没有root,会提示su: Permission Denied。注意:手机须root)
su这个文件不是每个手机都有的,可以别处找来放在adb同一目录下,执行:
adb push su /system/bin/
adb shell chmod 4755 /system/bin/su
第三步:
`cat /proc/想要查看的pid号码/oom_adj
这时就会显示当前的oom_adj值
其他命令可忽略 :dumpsys meminfo
使用dumpsys meminfo命令时,会列出当前系统的所有进程,不同进程放入不同的分类,对应的分类名基本与lmk的分类一致。有一点不同的就是,退到后台启动了服务且显示过UI的进程,在dumpsys meminfo命令中会归为b service一类,但从lmk角度分配的oom_adj值为9~16的范围,属于cached一类
2 进程保活的关键保活和复活
如果想要将app一直存活下来,要从2个方面进行考虑,一个是我们的app不会被轻易杀死,另一个是我们的app在被杀死以后怎么还会被调起来怎么唤醒进程,也就是进程复活。下面简要的分析上述2个问题
2.1 保活分析
通过LMS的机制可以看出,如果想要应用保活,那么首先就要将进程的优先级提高,也就是将oom_adj提高到最小值0,不过随着android 版本的提升,还有国内不同厂家的定制系统,像华为如果开启了锁屏清除进程的话就便是oom_adj为0该杀的也会杀死,不过也有解决办法,这个先放后面再说。
2.2 在什么情况下进程会被杀死
2.2.1 主动杀死常见场景
主动杀死的意思就是用户故意的将应用从后台清理掉,比如说通过手机内置的清理功能如小米 华为 等国产手机的一键清理功能,还有就是用户通过第三方下载的清理软件如360 猎豹清理大师。对于主动杀死的情况,不同手机不同android版本对不同复活方式都有不同的表现。
具体的复活方式请移步到 第三章 节
具体的效果统计移步到 第四章 节。
2.2.2 被动杀死常见场景
被动杀死的意思是不根据用户的意愿,通过手机内部的机制导致的进程被杀死,最常见的有2种情况。
- 当点击home键将当前的app放在后台,在使用了其他程序时导致内存不足,则会将需要保活的进程回收。
处理这种情况最简单的方式是你的app至少运行了一个service,然后通过Service.startForeground() 设置为前台服务,可以将oom_adj的数值由4降低到1,大大提高存活率。除了如此,还有更多的方式。具体代码和如何操作请移步 3.1、3.2、3.4 章节
- 当不退出app也不将app切换到后台的前提,直接将屏幕进入锁屏状态。
android系统自带的优化以及国产手机rom自带的优化(小米的省电策略,华为的锁屏后清理),当锁屏一段时间之后,即使手机内存够用为了省电,也会释放掉一部分内存。
被动杀死进程相对主动杀死进程使应用能够继续存活率会大大增加,因为在高版本以及定制的rom下,主动杀死即使做了很多复活的操作他也复活不了,而被动杀死则能。
2.3 保活常用的手段
- 跳转到系统白名单界面让用户自己添加app进入白名单
- 监听锁屏广播:使Activity始终保持前台
- 降低oom_adj的值:常驻通知栏(可通过启动另外一个服务关闭Notification,不对oom_adj值有影响)、使用”1像素“的Activity覆盖在getWindow()的view上、循环播放无声音频(黑科技,7.0下杀不掉)
2.4 复活常用方法
如果我们的app在oom_adj已经最优的时候还是会被杀死,这里就需要通过一些方法将程序复活才行。不同的复活方案都存在限制条件或者版本兼容性问题。
- 利用 JobScheduler 机制拉活
JobScheduler 允许在特定状态与特定时间间隔周期执行任务。可以利用它的这个特点完成保活的功能,效果类似开启一个定时器,与普通定时器不同的是其调度由系统完成。JobService在5.0,5.1,6.0作用很大,7.0时候有一定影响(可以在电源管理中给APP授权) - 利用 Native 进程拉活
利用 Linux 中的 fork 机制创建 Native 进程,在 Native 进程中监控主进程的存活,当主进程挂掉后,在 Native 进程中立即对主进程进行拉活。 - 通过监听系统广播拉活
简单讲就是监听一些特定的系统广播,当系统发出这些广播时,即可相应事件拉活。比如说开屏和关屏的广播等。在高版本的系统是需要app开机后运行过才能监听到这些广播,所以一般情况都是做的app启动以后的保活。 - 利用第三方应用广播拉活
这个稍微有点黑科技,是在知道第三方广播的前提下,根据他的广播做适配,对本身的app进行拉活。 - 利用系统Service机制拉活
将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后拉活。 - 双进程守护
通过双进程的 Service 相互绑定,在一个进程被 kill 时,另一个进程将其拉活。 - 双进程(NDK方式Fork子进程)、双Service守护:高版本已失效,5.0起系统回收策略改成进程组。双Service方案也改成了应用被杀,任何后台Service无法正常状态运行
- 推送互相唤醒复活:极光、友盟、以及各大厂商的推送
- 同派系APP广播互相唤醒:比如今日头条系、阿里系
3 具体保活和复活方案的实现过程
以下内容的源代码已上传GitHub,具体内容请结合Demo进行实践。
主进程在没有做任何保活的情况下,切换前后台的时候进程的oom_adj则保持0(前台进程)和6(Home进程)的切换。
3.1单Service的提高进程的优先级
抛砖引玉,这个方法是最简单的。只要在 新的进程 里开启一个被提高优先级的Service即可。
提高Service优先级需要增加startForeground(SERVICE_ID, new Notification());
用于开启前台进程。
未提高优先级前:
后台进程刚启动时 oom_adj为4 (后台重量级进程),如果打开了其他App,则当前这个Service的oom_adj会变成7。
提高优先级后:
在进程没Kill时,当前Service始终保持oom_adj=1;
核心代码:
public class ForegroundService extends Service {
/**
* 前台进程的NotificationId 不可为0
*/
private final static int SERVICE_ID = 1001;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(SERVICE_ID, new Notification());
return START_STICKY;
}
}
再此,就已经完成了Service的优先级提升。不过在通知栏会出现“XXX”正在运行。的字样,取消他也很简单。再开启一个Service用来和当前的通知栏上XXX共用,然后关闭当前的Service,可以通知栏“XXX”正在运行消失。关闭这个Service不会影响ForegroundService的优先级以及存活状态。
START_STICKY字段:系统尝试重启service 不同手机版本不一定能够自启
//用来关闭通知栏的Service
public static class SubsidiaryService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(KEEP_SERVICE_ID, new Notification());
stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}
注:这个SubsidiaryService 需要和ForegroundService 在同一个进程中,不然会出现通知栏先显示一下,然后在关闭的情况。
3.2 通过监听锁屏和开屏广播,使用“1”像素Activity提升优先级(微信也这么干过)
注册广播监听锁屏和解锁事件, 锁屏后启动一个1像素的透明Activity,这样直接把进程的oom_adj数值降低到0,0是android进程的最高优先级。 解锁后销毁这个透明Activity。
想要保活那个进程,就把这个1像素的Activity放在那个进程中。
流程一共四个步骤,具体内容参考Github源码。
- 首先要建立一个1像素的Activity.以及他的相关配置
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window window = getWindow();
window.setGravity(Gravity.RIGHT | Gravity.BOTTOM);
WindowManager.LayoutParams params = window.getAttributes();
params.x = 0;
params.y = 0;
params.height = 1;
params.width =1;
window.setAttributes(params);
KeepServiceManager.getInstance().setKeepLiveActivity(this);
}
Mainifest配置:
style:
- 为了方便封装了一个保活工具类
其中包含了隐藏通知栏的SubsidiaryService,广播也是在这里注册的,并且优化了防止1像素的Activity内存泄漏
public class KeepServiceManager {
/**
* Service ID
*/
private final static int KEEP_SERVICE_ID = 998;
private static KeepServiceManager instance = new KeepServiceManager();
public static KeepServiceManager getInstance(){
return instance;
}
/**
* 设置服务为前台服务
* @param service
*/
public void setServiceForeground(Service service){
if (Build.VERSION.SDK_INT < 18) {
//Android4.3以下 ,此方法能有效隐藏Notification上的图标
service.startForeground(KEEP_SERVICE_ID, new Notification());
} else if(Build.VERSION.SDK_INT>18 && Build.VERSION.SDK_INT<25){
//Android4.3 - Android7.0,此方法能有效隐藏Notification上的图标
Intent innerIntent = new Intent(service, SubsidiaryService.class);
service.startService(innerIntent);
service.startForeground(KEEP_SERVICE_ID, new Notification());
}else{
//Android7.1 google修复了此漏洞,暂无解决方法(现状:
// Android7.1以上app启动后通知栏会出现一条"正在运行"的通知消息)
service.startForeground(KEEP_SERVICE_ID, new Notification());
}
}
/**
* 辅助Service
*/
public static class SubsidiaryService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(KEEP_SERVICE_ID, new Notification());
stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
/**
* 传入1像素的Activity,并且防止内存泄漏
*/
private WeakReference mActivity;
/**
* 监听锁屏/解锁的广播(必须动态注册)
*/
private LockReceiver lockReceiver;
/**
* 传入1像素的透明Activity实例
* @param activity
*/
public void setKeepLiveActivity(Activity activity){
this.mActivity = new WeakReference<>(activity);
Log.e("setKeepLiveActivity","传入1像素的透明Activity实例");
}
/**
* 注册锁屏/解锁广播
* @param context
*/
public void registerReceiver(Context context){
Log.e("KeepServiceManager","registerReceiver");
lockReceiver = new LockReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_USER_PRESENT);
filter.addAction(Intent.ACTION_USER_PRESENT);
context.registerReceiver(lockReceiver,filter);
}
/**
* 注销锁屏/解锁广播
* @param context
*/
public void unRegisterReceiver(Context context){
Log.e("KeepServiceManager","unRegisterReceiver");
if(lockReceiver!=null){
context.unregisterReceiver(lockReceiver);
}
}
class LockReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()){
case Intent.ACTION_SCREEN_OFF:
startLiveActivity(context);//关闭屏幕则开启1像素的Activity
break;
case Intent.ACTION_USER_PRESENT://开启屏幕则关闭1像素的Activity
destroyLiveActivity();
break;
}
}
}
private void startLiveActivity(Context context){
Log.e("KeepServiceManager","接到关闭广播");
Intent intent = new Intent(context, PixelActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
private void destroyLiveActivity(){
Log.e("KeepServiceManager","接到开启屏幕广播");
if(mActivity!=null){
Activity activity=mActivity.get();
if (activity!=null){
activity.finish();
}
}
}
}
- 开启一个新的Service.
根据Service的生命周期进行相关操作。
public class ReceiverService extends Service {
@Override
public void onCreate() {
super.onCreate();
KeepServiceManager.getInstance().registerReceiver(this);
}
@Override
public void onDestroy() {
super.onDestroy();
KeepServiceManager.getInstance().unRegisterReceiver(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
KeepServiceManager.getInstance().setServiceForeground(this);
return START_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
- 开启通过广播启动1像素Activity的Service
startService(new Intent(MainActivity.this, ReceiverService.class));
3.3通过JobScheduler的方式复活Service
简介:
JobScheduler是用于计划基于应用进程的多种类型任务的api接口。
对象获取方法:Context.getSystemService(Context.JOB_SCHEDULER_SERVICE)
使用JobInfo.Builder.JobInfo.Builder(int,android.content.ComponentName)
构造JobInfo对象,并作为参数传给JobSechduler的schedule(JobInfo)方法。
当JobInfo中声明的执行条件满足时,系统会在应用的JobService中启动执行这个任务。
当任务执行时,系统会为你的应用持有WakeLock,所以应用不需要做多余的操作确保设备唤醒的工作。
public abstract class JobService extends Service
JobService继承自Service,是用于处理JobScheduler中规划的异步请求的特殊Service
如何使用:
- 使用JobService必须先在AndroidManifest.xml中声明service和权限
- 应用需要实现
onStartJob(JobParameters)
接口,在其中执行任务逻辑。
这个Service会在一个运行在主线程的Handler中执行规划的任务,所以应用需要在另外的thread/handler/AsyncTask中执行业务逻辑,如果不这么做的话可能会引起主线程的阻塞。
onStopJob(android.app.job.JobParameters)接口是当计划的执行条件“不再”满足时被执行的(例如网络中断)。
3.4 通过在后台播放无声的音乐
在GitHub中上传了2个MP3文件,一个是无声的文件,一个是用于测试用的文件(卡农)。
播放MP3使用无限循环模式setLooping(true);
public class MusicService extends Service {
private final static String TAG = MusicService.class.getSimpleName();
private MediaPlayer mMediaPlayer;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "MusicService启动服务");
mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.canon);
mMediaPlayer.setLooping(true);//无线循环
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
if (mMediaPlayer != null) {
Log.e(TAG, "启动播放无声音乐");
mMediaPlayer.start();
}
}
}).start();
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (mMediaPlayer != null) {
Log.e(TAG, "关闭播放无声音乐");
mMediaPlayer.stop();
}
Log.e(TAG, "MusicService停止服务");
// 重启自己
Intent intent = new Intent(getApplicationContext(), MusicService.class);
startService(intent);
}
}
记得在Manifest中注册:
3.5 双进程守护方案
基础:
在双进程守护方案中,需要用到跨进程访问的Service,首先先了解AIDL相关内容。
AIDL:Android Interface Definition Language,即Android接口定义语言使用AIDL定义的接口会被开发工具生成为可实现远程访问的接口。
Android系统中的进程之间不能共享内存,因此,需要提供一些机制在不同进程之间进行数据通信。
为了使其他的应用程序也可以访问本应用程序提供的服务,Android系统采用了远程过程调用(Remote Procedure Call,RPC)方式来实现。与很多其他的基于RPC的解决方案一样,Android使用一种接口定义语言(Interface Definition Language,IDL)来公开服务的接口。由于存在两个应用程序进行通信,一般提供服务的应用程序被称为“服务端”,而调用它接口方法的应用程序称为“客户端”。在双进程守护方案中,Service既是客户端也是服务端。
总体流程
使用AIDL绑定方式新建2个Service,不一样的进程互相拉起对方,并在每一个守护进程的ServiceConnection
的绑定回调里判断保活Service是否需要重新拉起和对守护线程进行重新绑定。
1.新建一个AIDL
interface IKeepAliveConnection {
}
2.新建2个Service
onBind()方法返回new KeepAliveConnection.Stub()对象,并在ServiceConnection的绑定回调中对另外的进程服务类启动和绑定。
public class GuardService extends Service {
private final static String TAG = GuardService.class.getSimpleName();
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Log.e(TAG, "GuardService建立链接");
boolean isServiceRunning = ServiceAliveUtils.isServiceAlice("ReceiverService");
if (!isServiceRunning) {
Intent i = new Intent(GuardService.this, ReceiverService.class);
startService(i);
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
// 断开链接
startService(new Intent(GuardService.this, OtherGuardService.class));
// 重新绑定
bindService(new Intent(GuardService.this, OtherGuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new IKeepAliveConnection.Stub() {
};
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(1111, new Notification());
// 绑定建立链接
bindService(new Intent(this, OtherGuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
return START_STICKY;
}
}
public class OtherGuardService extends Service {
private final static String TAG = OtherGuardService.class.getSimpleName();
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Log.e(TAG, "OtherGuardService:建立链接");
boolean isServiceRunning = ServiceAliveUtils.isServiceAlice("ReceiverService");
if (!isServiceRunning) {
Intent i = new Intent(OtherGuardService.this, ReceiverService.class);
startService(i);
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
// 断开链接
startService(new Intent(OtherGuardService.this, GuardService.class));
// 重新绑定
bindService(new Intent(OtherGuardService.this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new IKeepAliveConnection.Stub() {
};
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(1111, new Notification());
// 绑定建立链接
bindService(new Intent(this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
return START_STICKY;
}
}
3.在Activity中启动的被保活的双Service的启动Service(KeepDoubleStartService)
public class KeepDoubleStartService extends Service {
public static final int NOTICE_ID = 100;
private static final String TAG = UseJobService.class.getSimpleName();
private DownloadBinder mDownloadBinder;
private NotificationCompat.Builder mBuilderProgress;
private NotificationManager mNotificationManager;
private Timer mRunTimer;
private int mTimeSec;
private int mTimeMin;
private int mTimeHour;
private OnTimeChangeListener mOnTimeChangeListener;
public interface OnTimeChangeListener {
void showTime(String time);
}
@Override
public void onCreate() {
super.onCreate();
KeepServiceManager.getInstance().registerReceiver(this);
mDownloadBinder = new DownloadBinder();
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand");
KeepServiceManager.getInstance().setServiceForeground(this);
startRunTimer();
return START_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mDownloadBinder;
}
@Override
public boolean onUnbind(Intent intent) {
Log.e(TAG, "onUnbind");
return super.onUnbind(intent);
}
private void startRunTimer() {
TimerTask mTask = new TimerTask() {
@Override
public void run() {
mTimeSec++;
if (mTimeSec == 60||mTimeHour%60==0) {
mTimeSec = 0;
mTimeMin++;
}
if (mTimeMin == 60|mTimeMin%60==0) {
mTimeMin = 0;
mTimeHour++;
}
String time = "运行时间:" + mTimeHour + " : " + mTimeMin + " : " + mTimeSec;
if (mOnTimeChangeListener != null) {
mOnTimeChangeListener.showTime(time);
}
Log.e(TAG, time);
}
};
mRunTimer = new Timer();
// 每隔1s更新一下时间
mRunTimer.schedule(mTask, 1000, 1000);
}
private void stopRunTimer() {
if (mRunTimer != null) {
mRunTimer.cancel();
mRunTimer = null;
}
mTimeSec = 0;
mTimeMin = 0;
mTimeHour = 0;
}
public class DownloadBinder extends Binder {
public void setOnTimeChangeListener(OnTimeChangeListener onTimeChangeListener) {
mOnTimeChangeListener = onTimeChangeListener;
}
}
@Override
public void onDestroy() {
super.onDestroy();
KeepServiceManager.getInstance().unRegisterReceiver(this);
NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (mManager == null) {
return;
}
mManager.cancel(NOTICE_ID);
stopRunTimer();
}
}
3.6 双App相互拉活方案
此方案和双进程守护方案原理差不多,但是更加可靠一点,在Android高版本上8.0+ 和一些国内的定制手机在手动滑出一个应用时,都会杀死所有当前应用开启的Service,此方法可以有效防止,不过一般只有大厂才有被用户安装多个App的可能,这里就不讨论了。如有需要可以查看如下链接:
Android 8.0 双App应用保活实践
4.保活方案实现效果统计
以下方案都是基于Service的onStartCommand() return START_STICKY为基础进行统计。
4.1 双进程守护方案
品牌 | 系统版本 | 效果 |
---|---|---|
-- | 原生5.0、5.1 | 原生任务栏滑动清理app,Service会被杀掉,然后被拉起,接着一直存活 |
金立F100 | 5.1 | 一键清理直接杀掉整个app,包括双守护进程。不手动清理情况下,经测试能锁屏存活至少40分钟 |
Vivo V3MA | 5.0 | 滑动退出App直接杀死. |
华为畅享5x | 6.0 | 一键清理直接杀掉整个app,包括双守护进程。不手动清理下,锁屏只存活10s。结论:双进程守护方案失效。 |
摩托罗拉(root) | 原生6.0 | 原生任务栏滑动清理app,Service不被杀 |
美图m8s | 7.1.1 | 一键清理直接杀掉整个app,包括双守护进程。不清理情况下,锁屏会有被杀过程(9分钟左右被杀),之后重新复活,之后不断被干掉然后又重新复活。结论:双守护进程可在后台不断拉起Service。 |
华为AGS-W09 | 7.0 | 一键清理直接杀掉整个app,包括双守护进程。锁屏后可以存活 |
-- | 原生7.0 | 务栏清除APP后,Service存活。使用此方案后Service照样存活。 |
LG V30+ | 7.1.2 | 不加双进程守护的时候,一键清理无法杀掉服务。加了此方案之后也不能杀掉服务,锁屏存活(测试观察大于50分钟) |
小米8 | 8.1 | 一键清理直接干掉app并且包括双守护进程。不清理情况下,不加守护进程方案与加守护进程方案Service会一直存活,12分钟左右closed。结论:此方案没有起作用 |
小米8 | 9 | 同上 |
结论:除了华为、小米以及未更改底层的厂商不起作用外(START_STICKY字段就可以保持Service不被杀)。此方案可以与其他方案混合使用
4.2 监听锁屏广播打开1像素Activity
品牌 | 系统版本 | 效果 |
---|---|---|
-- | 原生5.0、5.1 | 锁屏后3s服务被干掉然后重启(START_STICKY字段起作用) |
Vivo V3MA | 5.0 | 锁屏后不会关闭,任务管理器退出直接杀死 |
华为畅享5x | 6.0 | 锁屏只存活4s。结论:方案失效。 |
摩托罗拉(root) | 原生6.0 | 正常任务栏清理主线程关闭。锁屏不被清理,仅仅是提升优先级,此方法可以正常任务栏清理后保持主线程存活 |
美图m8s | 7.1.1 | 锁屏后3s服务被干掉然后重启(START_STICKY字段起作用) |
华为AGS-W09 | 7.0 | 锁屏后可以存活 |
-- | 原生7.0 | 锁屏后3s服务被干掉然后重启(START_STICKY字段起作用) |
LG V30+ | 7.1.2 | 锁屏后情况跟不加情况一致,服务一致保持运行,结论:此方案不起作用 |
小米8 | 8.1 | 关屏过2s之后app全部被干掉。结论:此方案没有起作用 |
结论:提升优先级,并无太大的效果。
4.3 后台播放无声的音乐
品牌 | 系统版本 | 效果 |
---|---|---|
-- | 原生5.0、5.1 | 锁屏后3s服务被干掉然后重启(START_STICKY字段起作用) |
Vivo V3MA | 5.0 | 关闭屏幕服务依旧开启音乐自动播放 |
华为畅享5x | 6.0 | 一键清理后服务依然存活,需要单独清理,才可杀掉服务,锁屏8分钟后依然存活。结论:此方案适用 |
摩托罗拉(root) | 原生6.0 | 正常任务栏清理主线程关闭。锁屏不被清理。音乐自启动 |
美图m8s | 7.1.1 | 锁屏后3s服务被干掉然后重启(START_STICKY字段起作用) |
华为AGS-W09 | 7.0 | 一键清理不会单独杀死这个服务,但是手动滑动退出会杀死。播放存活时间超过2小时 |
-- | 原生7.0 | 任务管理器中关闭APP后服务被干掉,大概过3s会重新复活(同仅START_STICKY字段模式)。结论:看不出此方案有没有其作用 |
LG V30+ | 7.1.2 | 使用此方案前后效果一致。结论:此方案不起作用 |
小米8 | 8.1 | 一键清理可以杀掉服务。锁屏后保活超过20分钟 |
结论:成功对华为手机保活。小米8下也成功突破20分钟
4.4 使用JobScheduler唤醒Service
品牌 | 系统版本 | 效果 |
---|---|---|
-- | 原生5.0、5.1 | 任务管理器中干掉APP,服务会在周期时间后重新启动。结论:此方案起作用 |
Vivo V3MA | 5.0 | 一键清理直接杀掉APP,无法自动重启 |
华为畅享5x | 6.0 | 一键清理直接杀掉APP,过12s左右会自动重启服务,JobScheduler起作用 |
摩托罗拉(root) | 原生6.0 | 任务管理器中干掉APP,服务会在周期时间后重新启动,就算手机重启,JobSchedulerService中onStartJob方法启动的线程都能重新启动!结论:此方案起作用 |
美图m8s | 7.1.1 | 一键清理直接杀掉APP,无法自动重启 |
华为AGS-W09 | 7.0 | 一键清理直接杀掉APP,无法自动重启 |
-- | 原生7.0 | 一键清理直接杀掉APP,无法自动重启 |
小米8 | 8.1 | 一键清理直接杀掉APP,无法自动重启 |
小米8 | 9 | 一键清理直接杀掉APP,无法自动重启 |
结论:只对5.0,5.1、6.0起作用
4.5 混合使用的效果,并且在通知栏弹出通知
品牌 | 系统版本 | 效果 |
---|---|---|
-- | 原生5.0、5.1 | 任务管理器中干掉APP,服务会在周期时间后重新启动。锁屏超过11分钟存活 |
Vivo V3MA | 5.0 | 一键清理可以杀掉服务,锁屏下后台保活时间超过12小时 |
华为畅享5x | 6.0 | 一键清理后服务依然存活,需要单独清理才可杀掉服务。结论:方案适用。 |
摩托罗拉(root) | 原生6.0 | 任务管理器中关闭APP后服务正常,锁屏查过过12小时存活 |
美图m8s | 7.1.1 | 一键清理APP会被杀掉。正常情况下锁屏后服务依然存活。 |
华为AGS-W09 | 7.0 | 一键清理可以杀掉服务,锁屏下后台保活时间超过12小时。结论:方案适用。 |
-- | 原生7.0 | 任务管理器中关闭APP后服务被干掉,过2s会重新复活 |
LG V30+ | 7.1.2 | 使用此方案前后效果一致。结论:此方案不起作用 |
小米8 | 8.1 | 一键清理可以杀掉服务,锁屏下后台保活时间超过38分钟 |
荣耀10 | 8.0 | 一键清理杀掉服务,锁屏下后台保活时间超过23分钟 |
小米8 | 9 | 一键清理杀掉服务,锁屏下后台保活时间超过10分钟 |
结论:高版本情况下可以使用弹出通知栏、双进程、无声音乐提高后台服务的保活概率
5 保活总结
对于5.0以上的系统7.0以下(不含7.0),除了个别的手机型号品牌(Vivo V3MA)都可以通过JobScheduler的方式进行复活,而且复活率很高。过高版本情况下可以使用弹出通知栏、双进程、无声音乐提高后台服务的保活概率,而且大部分都会在锁屏的情况下保持12小时以上的存活。
对于高版本和定制的ROM,如果一键清理大多是无法进行保活,都是直接将进程杀死了,但是7.0的华为平板也会出现一键清理不会清理有MusicService服务的情况。如果单独在任务管理器中滑出App,会将所有的进程杀死。
国产的ROM这功能很好,这样就可以有效防止流氓APP占用手机的内存。
使用各种保活的手段只能是提高应用的存活率,但是现在保活基本上属于伪命题,只有用户设置白名单方可活。
不同的厂家都有对白名单(自启管理)相关的设置,当然这只能是要用户去主动设置才行。
- 小米自家有省电策略和自启管理对单独应用进行设置可以提高应用后台的存活率
- HUAWEI有自启管理,在电池管理中有锁屏清理应用,两者都会提高应用的存活率。2种方式
打开应用管理-->相关App-->电池-->关闭锁屏后清理
或者
设置--》电池--》锁屏清理应用--》关闭锁屏清理 - Vivo叫做加速白名单,加入一键加速白名单的软件,软件后台界面的右上角锁状图标即会锁住,还有一个后台高耗电功能,在这里可以将需要保活的App加进来就行。
如果非必要都是不建议做这种保活的功能,从开发角度既伤神又费力,用户更是不能接受这种费电的应用。自此保活相关的基础和实践探索总结完毕~!
最后要感谢作者@minminaya授权提供的部分方案实现效果的数据。以及作者@cspecialy提供的部分相关知识点的说明,还有小智同学(不怕我用了他心爱的米8测试成砖哈哈哈)
文内详细说明代码 移步至GitHub