注册

Android 守护进程的实现方式

 
   “我的APP像微信那样能一直在手机运行吗?”关于 Android 平台的进程保活,一 直是所有Android 开发者瞩目的内容之一,也是环信小伙们比较关心的问题,本篇文章给大家分享关于微信进程保活的原理及Android守护进程的实现教程。
 
为什么微信可以一直在手机后台跑着能收到消息?

    国内手机厂商对 android rom 进行了定制,对后台服务以及运行在后台的程序进行了严格的限制,微信等这些大厂商的 app 都已经通过和设备厂商合作在安装时都已经加入了系统的白名单,因此设备并不会限制对方 app 在后台运行;

我自己的APP该如何实现进程保活?
 
  1. 引导用户把当前 app 加入到设备的白名单中,解除设备对 app 的限制;
  2. 小米和华为设备可以集成对应的推送实现在app 被干掉后依然收推送通知;
  3. 可以自己在 app 端实现守护进程的方式,让 app 在系统级别自动回收的情况下减少被杀死的概率,这种方式对用户主动回收无效。


第一条和第二条就不多说了,环信imgeek社区里已经有了相应的文章(http://www.imgeek.org/article/825308754 )接下来介绍守护进程的实现。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在Android里永生不死的,请忽略本文!请忽略!
 什么是守护进程?

    守护进程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的。
 
守护进程的实现,本文两个核心观点:
 
  1. 提高进程优先级,降低被回收或杀死概率
  2. 在进程被干掉后,进行拉起

 要实现实现上边所说,通过下边几点来实现,首先我们需要了解下进程的优先级划分:

Process Importance记录在ActivityManager.java类中:
**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 这个进程正在运行前台UI,也就是说,它是当前在屏幕顶部的东西,用户正在进行交互的而进程
*/
public static final int IMPORTANCE_FOREGROUND = 100;

/**
* 此进程正在运行前台服务,即使用户不是在应用中时也执行音乐播放,这一般表示该进程正在做用户积极关心的事情
*/
public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
/**
* 这个过程不是用户的直接意识到,但在某种程度上是他们可以察觉的。
*/
public static final int IMPORTANCE_PERCEPTIBLE = 130;

/**
* 此进程正在运行前台UI,但设备处于睡眠状态,因此用户不可见,意思是用户意识不到的进程,因为他们看不到或与它交互,
* 但它是相当重要,因为用户解锁设备时期望的返回到这个进程
*/
public static final int IMPORTANCE_TOP_SLEEPING = 150;

/**
* 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
*/
public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

/**
* 此进程正在运行某些对用户主动可见的内容,但不是直接显示在UI,
* 这可能运行在当前前台之后的窗口(因此暂停并且其状态被保存,不与用户交互,但在某种程度上对他们可见);
* 也可能在系统的控制下运行其他服务,
*/
public static final int IMPORTANCE_VISIBLE = 200;

/**
* 服务进程,此进程包含在后台保持运行的服务,这些后台服务用户察觉不到,是无感知的,所以它们可以由系统相对自由地杀死
*/
public static final int IMPORTANCE_SERVICE = 300;

/**
* 后台进程
*/
public static final int IMPORTANCE_BACKGROUND = 400;

/**
* 空进程,此进程没有任何正在运行的代码
*/
public static final int IMPORTANCE_EMPTY = 500;

// 此过程不存在。
public static final int IMPORTANCE_GONE = 1000;
进程回收机制

了解进程优先级之后,我们还需要知道一个进程回收机制的东西;这里参考AngelDevil在博客园上的一篇文章:
 
详情参考:【Android Low Memory Killer】
 
Android的Low Memory Killer基于Linux的OOM机制,在Linux中,内存是以页面为单位分配的,当申请页面分配时如果内存不足会通过以下流程选择bad进程来杀掉从而释放内存:
alloc_pages -> out_of_memory() -> select_bad_process() -> badness()
在Low Memory Killer中通过进程的oom_adj与占用内存的大小决定要杀死的进程,oom_adj越小越不容易被杀死;
Low Memory Killer Driver在用户空间指定了一组内存临界值及与之一一对应的一组oom_adj值,当系统剩余内存位于内存临界值中的一个范围内时,如果一个进程的oom_adj值大于或等于这个临界值对应的oom_adj值就会被杀掉。

下边是表示Process State(即老版本里的OOM_ADJ)数值对照表,数值越大,重要性越低,在新版SDK中已经在android层去除了小于0的进程状态
// Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java 
// 进程不存在。
public static final int PROCESS_STATE_NONEXISTENT = -1;
// 进程是一个持久的系统进程,一般指当前 UI 进程
public static final int PROCESS_STATE_PERSISTENT = 0;
// 进程是一个持久的系统进程,正在做和 UI 相关的操作,但不直接显示
public static final int PROCESS_STATE_PERSISTENT_UI = 1;
// 进程正在托管当前的顶级活动。请注意,这涵盖了用户可见的所有活动。
public static final int PROCESS_STATE_TOP = 2;
// 进程由于系统绑定而托管前台服务。
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
// 进程正在托管前台服务。
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
// 与{@link #PROCESS_STATE_TOP}相同,但设备处于睡眠状态。
public static final int PROCESS_STATE_TOP_SLEEPING = 5;
// 进程对用户很重要,是他们知道的东西
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
// 进程对用户很重要,但不是他们知道的
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
// 进程在后台运行备份/恢复操作
public static final int PROCESS_STATE_BACKUP = 8;
// 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
public static final int PROCESS_STATE_HEAVY_WEIGHT = 9;
// 进程在后台运行一个服务,与oom_adj不同,此级别用于正常运行在后台状态和执行操作状态。
public static final int PROCESS_STATE_SERVICE = 10;
// 进程在后台运行一个接收器,注意,从oom_adj接收器的角度来看,在较高的前台级运行,但是对于我们的优先级,这不是必需的,并且将它们置于服务之下意味着当它们接收广播时,一些进程状态中的更少的改变。
public static final int PROCESS_STATE_RECEIVER = 11;
// 进程在后台,但主持家庭活动
public static final int PROCESS_STATE_HOME = 12;
// 进程在后台,但托管最后显示的活动
public static final int PROCESS_STATE_LAST_ACTIVITY = 13;
// 进程正在缓存以供以后使用,并包含活动
public static final int PROCESS_STATE_CACHED_ACTIVITY = 14;
// 进程正在缓存供以后使用,并且是包含活动的另一个缓存进程的客户端
public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 15;
// 进程正在缓存以供以后使用,并且为空
public static final int PROCESS_STATE_CACHED_EMPTY = 16;
Process State(即老版本的OOM_ADJ)与Process Importance对应关系,这个方法也是在ActivityManager.java类中,有了这个关系,就知道可以知道我们的应用处于哪个级别,对于我们后边优化有个很好地参考
/** 
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 通过这个方法,将Linux底层的 OOM_ADJ级别码和 android 层面的进程重要程度联系了起来
*/
public static int procStateToImportance(int procState) {
if (procState == PROCESS_STATE_NONEXISTENT) {
return IMPORTANCE_GONE;
} else if (procState >= PROCESS_STATE_HOME) {
return IMPORTANCE_BACKGROUND;
} else if (procState >= PROCESS_STATE_SERVICE) {
return IMPORTANCE_SERVICE;
} else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
return IMPORTANCE_CANT_SAVE_STATE;
} else if (procState >= PROCESS_STATE_IMPORTANT_BACKGROUND) {
return IMPORTANCE_PERCEPTIBLE;
} else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
return IMPORTANCE_VISIBLE;
} else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
return IMPORTANCE_FOREGROUND_SERVICE;
} else {
return IMPORTANCE_FOREGROUND;
}
}
一般情况下,设备端进程被干掉有一下几种情况

QQ截图20170615143439.jpg


由以上分析,我们可以可以总结出,如果想提高我们应用后台运行时间,就需要提高当前应用进程优先级,来减少被杀死的概率

守护进程的实现

分析了那么多,现在对Android自身后台进程管理,以及进程的回收也有了一个大致的了解,后边我们要做的就是想尽一切办法去提高应用进程优先级,降低进程被杀的概率;或者是在被杀死后能够重新启动后台守护进程

1.模拟前台进程

第一种方式就是利用系统漏洞,使用startForeground()将当前进程伪装成前台进程,将进程优先级提高到最高(这里所说的最高是服务所能达到的最高,即1);

这种方式在7.x之前都是很好用的,QQ、微信、IReader、Keep 等好多应用都是用的这种方式实现;因为在7.x 以后的设备上,这种伪装前台进程的方式也会显示出来通知栏提醒,这个是取消不掉的,虽然Google现在还没有对这种方式加以限制,不过这个已经能够被用户感知到了,这种方式估计也用不了多久了

下边看下实现方式,这边这个VMDaemonService就是一个守护进程服务,其中在服务的onStartCommand()方法中调用startForeground()将服务进程设置为前台进程,当运行在 API18 以下的设备是可以直接设置,API18 以上需要实现一个内部的Service,这个内部类实现和外部类同样的操作,然后结束自己;当这个服务启动后就会创建一个定时器去发送广播,当我们的核心服务被干掉后,就由另外的广播接收器去接收我们守护进程发出的广播,然后唤醒我们的核心服务;
/**
* 以实现内部 Service 类的方式实现守护进程,这里是利用 android 漏洞提高当前进程优先级
*
* Created by lzan13 on 2017/3/7.
*/
public class VMDaemonService extends Service {

private final static String TAG = VMDaemonService.class.getSimpleName();

// 定时唤醒的时间间隔,这里为了自己测试方边设置了一分钟
private final static int ALARM_INTERVAL = 1 * 60 * 1000;
// 发送唤醒广播请求码
private final static int WAKE_REQUEST_CODE = 5121;
// 守护进程 Service ID
private final static int DAEMON_SERVICE_ID = -5121;

@Override public void onCreate() {
Log.i(TAG, "VMDaemonService->onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
// 利用 Android 漏洞提高进程优先级,
startForeground(DAEMON_SERVICE_ID, new Notification());
// 当 SDk 版本大于18时,需要通过内部 Service 类启动同样 id 的 Service
if (Build.VERSION.SDK_INT >= 18) {
Intent innerIntent = new Intent(this, DaemonInnerService.class);
startService(innerIntent);
}

// 发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(VMWakeReceiver.DAEMON_WAKE_ACTION);

PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
ALARM_INTERVAL, operation);

/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
*/
return START_STICKY;
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "VMDaemonService->onDestroy");
super.onDestroy();
}

/**
* 实现一个内部的 Service,实现让后台服务的优先级提高到前台服务,这里利用了 android 系统的漏洞,
* 不保证所有系统可用,测试在7.1.1 之前大部分系统都是可以的,不排除个别厂商优化限制
*/
public static class DaemonInnerService extends Service {

@Override public void onCreate() {
Log.i(TAG, "DaemonInnerService -> onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "DaemonInnerService -> onStartCommand");
startForeground(DAEMON_SERVICE_ID, new Notification());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "DaemonInnerService -> onDestroy");
super.onDestroy();
}
}
}
当我们启动这个守护进程的时候,就可以使用以下adb命令查看当前程序的进程情况(需要adb shell进去设备),
为了等下区分进程优先级,我启动了一个普通的后台进程,两外两个一个是我们启动的守护进程,一个是当前程序的核心进程,可以看到除了后台进程外,另外两个进程都带有isForeground=true的属性:
# 这个命令的 services 可以换成 service,这样会只显示当前,进程,不显示详细内容
# dumpsys activity services <Your Package Name>
root@vbox86p:/ # dumpsys activity services com.vmloft.develop.daemon
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{170fe1dd u0 com.vmloft.develop.daemon/.services.VMDaemonService}
intent={cmp=com.vmloft.develop.daemon/.services.VMDaemonService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{173fe77f 2370:com.vmloft.develop.daemon:daemon/u0a68}
isForeground=true foregroundId=-5121 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-6s196ms startingBgTimeout=--
lastActivity=-6s157ms restartTime=-6s157ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2fee4f84 u0 com.vmloft.develop.daemon/.services.VMCoreService}
intent={cmp=com.vmloft.develop.daemon/.services.VMCoreService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{18c6a1b4 2343:com.vmloft.develop.daemon/u0a68}
isForeground=true foregroundId=-5120 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-28s136ms startingBgTimeout=--
lastActivity=-28s136ms restartTime=-28s136ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2ef6909e u0 com.vmloft.develop.daemon/.services.VMBackgroundService}
intent={cmp=com.vmloft.develop.daemon/.services.VMBackgroundService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:background
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{29f8734c 2388:com.vmloft.develop.daemon:background/u0a68}
createTime=-3s279ms startingBgTimeout=--
lastActivity=-3s262ms restartTime=-3s262ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1
然后我们可以用下边的命令查看ProcessID
# 这个命令可以查看当前DProcessID(数据结果第二列),我们可以看到当前程序有两个进程
# ps | grep com.vmloft.develop.daemon
root@vbox86p:/ # ps | grep com.vmloft.develop.daemon
u0_a68 2343 274 1012408 42188 ffffffff f74f1b45 S com.vmloft.develop.daemon
u0_a68 2370 274 997012 26152 ffffffff f74f1b45 S com.vmloft.develop.daemon:daemon
u0_a68 2388 274 997012 25668 ffffffff f74f1b45 S com.vmloft.develop.daemon:background
有了ProcessID之后,我们可以根据这个ProcessID获取到当前进程的优先级状态Process State,对应Linux层的oom_adj
可以看到当前核心进程的级别为0,因为这个表示当前程序运行在前台 UI 界面,守护进程级别为1,因为我们利用漏洞设置成了前台进程,虽然不可见,但是他的级别也是比较高的,仅次于前台 UI 进程,然后普通后台进程级别为4;当我们退到后台时,可以看到核心进程的级别变为1了,这就是因为我们利用startForeground()将进程设置成前台进程的原因,这样就降低了进程被系统回收的概率了;
# 这个命令就是通过 ProcessID 输出其对应 oom_adj
# cat /proc/ProcessID/oom_adj
# 程序在前台时,查询进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
0
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
# 当程序退到后台时,再次查看进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
1
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
可以看到这种方式确实能够提高进程优先级,但是在一些国产的设备上还是会被杀死的,比我我测试的时候小米点击清空最近运行的应用进程就别干掉了;当把应用加入到设备白名单里就不会被杀死了,微信就是这样,人家直接装上之后就已经在白名单里了,我们要做的就是在用户使用中引导他们将我们的程序设置进白名单,将守护进程和白名单结合起来,这样才能保证我们的应用持续或者

2.JobScheduler机制唤醒

Android系统在5.x以上版本提供了一个JobSchedule接口,系统会根据自己实现定时去调用改接口传递的进程去实现一些操作,而且这个接口在被强制停止后依然能够正常的启动;不过在一些国产设备上可能无效,比如小米;
下边是 JobServcie 的实现:
/**
* 5.x 以上使用 JobService 实现守护进程,这个守护进程要做的工作很简单,就是启动应用的核心进程
* Created by lzan13 on 2017/3/8.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class VMDaemonJobService extends JobService {

private final static String TAG = VMDaemonJobService.class.getSimpleName();

@Override public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob");
// 这里为了掩饰直接启动核心进程,没有做其他判断操作
startService(new Intent(getApplicationContext(), VMCoreService.class));
return false;
}

@Override public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob");
return false;
}
}
我们要做的就是在需要的时候调用JobSchedule的schedule来启动任务;剩下的就不需要关心了,JobSchedule会帮我们做好,下边就是我这边实现的启动任务的方法:
/**
* 5.x以上系统启用 JobScheduler API 进行实现守护进程的唤醒操作
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startJobScheduler() {
int jobId = 1;
JobInfo.Builder jobInfo = new JobInfo.Builder(jobId, new ComponentName(this, VMDaemonJobService.class));
jobInfo.setPeriodic(10000);
jobInfo.setPersisted(true);
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo.build());
}
3.系统 Service START_STICKY 机制重启

在实现Service类时,将onStartCommand()返回值设置为START_STICKY,利用系统机制在Service挂掉后自动拉活;不过这种方式只适合比较原生一些的系统,像小米,华为等这些定制化比较高的第三方厂商,他们都已经把这些给限制掉了;
@Override 
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
* 3.一些定制化比较高的第三方系统也不适用
*/
return START_STICKY;
}
这种方式在以下两种情况无效:
  • Service第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内Service被杀死达到5次,这个服务就不能再次重启了;
  • 进程被取得Root权限的管理工具或系统工具通过fores-top方式停止掉,无法重启;
  • 一些定制化比较高的第三方系统也不适用
4.其他保活方式

  • 利用 Native 本地进程,这个主要使用到 jni 调用底层实现,而且在 Android 5.x 以后对这个限制也比较高,不适用了,暂时不研究
  • 集成第三方SDK互相唤醒,这个只要正常集成了第三方的SDK,并使用了他们对应的服务,当一个设备安装的多个应用都集成了某一个第三方SDK时,启动任意一个 app 都会唤醒其他的 app,不过这个在一些新版的国内厂商系统也是做了限制,这种方式并没有什么效果
  • 一像素的 Activity 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;


结语

事事没有绝对,万物总有一些漏洞,就算上边的那些方式不可用了,后边肯定还会出现其他的方式;我们不能保证我们的应用不死,但我们可以提高存活率;

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是

0 个评论

要回复文章请先登录注册