需要引用请注明出处:juejin.im/post/5ca5f9…
一、开场白
现在很多公司和开发者希望自己的app能够长期运行在手机内存中,因为只要该app的进程一直存在,那么我们就可以干很多事,尽管很多不是很光彩的事,比如偷流量,费电,偷偷安装应用和推送广告信息等等。庆幸的事,道高一尺魔高一丈,android原生系统对现在手机系统做了很多的保护,现在很难保证哪一个应用的进程可以一直不被杀死,我们能够做的就是尽量保活进程,接下来我们就对进程保活做个总结。
二、进程相关的基础知识
在正式开始介绍进程保活的知识之前,简单了解一下进程相关的一些东西。首先什么是进程,这些我相信是程序员都会清楚,进程是系统进行分配资源和调度的最小单位,很简单,每个进程就像一个app运行在手机系统里。
1.如何查看进程
我们可以通过adb shell ps来查看进程的信息:
id | 说明 |
---|---|
u0_a344 | 当前用户 |
9153 | pid 进程名 |
201 | ppid 父进程名 |
1597348 | VSIZE进程的虚拟内存大小 |
com.sunland.staffapp | 进程名 |
2.进程的划分
根据进程当前所处的状态,我们可以将进程分为5类:前台进程、可见进程、服务进程、后台进程、空进程。每一种进程解释如下:
2.1 前台进程(Foreground process)
- 当该进程有activity处于resume状态,就是可见的activity
- 当该进程有service正在与activity进行交互绑定
- 当拥有service,并且该service正在运行在前台,例如调用了startForeground
- 当持有的service正在进行生命周期方法回调
- 当持有broadcast正在进行onReceive操作
进程只要处在上述任意一种状态,那么该进程就是前台进程,前台进程的优先级最高,系统一般不会直接杀死前台进程,除非手机系统内存完全耗尽。
2.2 可见进程(Visibale process)
- 当activity处于onPause状态下,activity对我们仍然可见,但是,我们无法对该activity进行交互。
- 拥有绑定到可见(或前台)Activity 的 Service,但是该activity没与用户进行交互
可见进程也是系统中及其重要的进程,不到万不得已的情况下,系统也是不会杀死可见进程的。
2.3 服务进程(service process)
某个进程中运行着service,并且该service是通过startService启动的,与用户界面不存在交互的那种service,当内存不足以维持前台进程和可见进程的情况下,会优先杀死服务进程。
2.4 后台进程
在程序退到后台,比如用户按了back键或者home,界面看不到了,但是程序仍在运行中,此时的activity处于onpause状态,在任务管理器中可以看到,当系统内存不足的情况下会有限杀死后台进程。
2.5 空进程
空进程就是不含有任何active的进程,系统保留的原因主要是高速缓存,方便下次访问速度很快,如果系统内存不足,首先杀的就是空进程。
3.如何查看进程的优先级
进程有个参数,oom_adj,一般而言,这个参数的值越小,优先级则越高,处于前台进程的adj为0,当然各个手机厂商的可能会有一点差异,查看adj的方法如下:
可以看到,处于前台进程的adj的值为0。 当按返回键后,程序退到后台,这时候adj变为1一般而言,进程adj值越大,占用系统内存值越大,优先被杀死。我们做进程保护就是从这两个方面下手。接下来就是正题了。
三、进程保活的方案
1、1像素点的Activity
由于前台进程不容易被杀死,所以我们可以试着去开启一个前台进程,并且开启前台进程不为用户所感知,1个像素点的activity就可以满足要求,我们可以在锁屏的时候启动一个activity,这个activity只有一个像素,在开屏的时候finish掉这个activity,考虑到内存的问题,我们可以在service中去启动这个activity,并且这个service在独立的进程中,如下实例:
/**
* foreground service for keeping alive
*/
public class KeepAliveService extends Service {
private static final String TAG = Constants.LOG_TAG;
public static final int NOTICE_ID = 100;
// 动态注册锁屏等广播
private ScreenReceiverUtil mScreenUtil;
// 1像素Activity管理类
private ScreenManager mScreenManager;
private View toucherLayout;
private WindowManager.LayoutParams params;
private WindowManager windowManager;
private ScreenReceiverUtil.ScreenStateListener mScreenStateListenerer = new ScreenReceiverUtil.ScreenStateListener() {
@Override
public void onSreenOn() {
L.d(TAG, "KeepAliveService-->finsh 1 pixel activity");
mScreenManager.finishActivity();
}
@Override
public void onSreenOff() {
L.d(TAG, "KeepAliveService-->start 1 pixel activity");
mScreenManager.startActivity();
}
@Override
public void onUserPresent() {
}
};
//不与Activity进行绑定.
@Override
public IBinder onBind(Intent intent)
{
return null;
}
@Override
public void onCreate()
{
super.onCreate();
//如果API大于18,需要弹出一个可见通知,这个可见通知在大于API 25 版本之前可以通过Cancel隐藏
// 所以在API 18 ~ API 24之间启动前台service,并隐藏Notification
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N){
startForeground(NOTICE_ID, new NotificationUtils(this).getNotification("Sunlands", "打卡提醒进程正在运行"));
// 如果觉得常驻通知栏体验不好
// 可以通过启动CancelNoticeService,将通知移除,oom_adj值不变
Intent intent = new Intent(this,CancelNoticeService.class);
startService(intent);
}else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
startForeground(NOTICE_ID, new Notification());
}
mScreenUtil = new ScreenReceiverUtil(this);
mScreenManager = ScreenManager.getInstance(this);
mScreenUtil.setScreenReceiverListener(mScreenStateListenerer);
createFloatingWindow();
L.d(TAG, "KeepAliveService-->KeepAliveService created");
// If app process is killed system, all task scheduled by AlarmManager is canceled.
// We need reschedule all task when KeepAliveService is revived.
TaskUtil.dispatchAllTask(this);
}
@Override
public void onDestroy() {
super.onDestroy();
// 如果Service被杀死,干掉通知
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
NotificationManager mManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
mManager.cancel(NOTICE_ID);
}
if (toucherLayout != null) {
ensureWindowManager();
try {
L.d(TAG, "KeepAliveService-->remove floating window");
windowManager.removeView(toucherLayout);
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
mScreenUtil.stopScreenReceiverListener();
}
public static void startKeepAliveServiceIfNeed(Context context) {
boolean isKeepAliveServiceEnabled = SystemUtils.isComponentEnabled(context.getApplicationContext(), KeepAliveService.class);
if (isKeepAliveServiceEnabled) {
try {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
if (!LifecycleHandler.getInstance().isForeground()) {
return;
}
}
Intent intentAlive = new Intent(context.getApplicationContext(), KeepAliveService.class);
context.startService(intentAlive);
L.d(TAG, "KeepAliveService-->start KeepAliveService");
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
}
private void createFloatingWindow()
{
// MIUI用TYPE_TOAST无法在后台显示悬浮窗,必须获取draw_over_other_app权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& OSJudgementUtil.isMIUI()
&& !Settings.canDrawOverlays(this)) {
L.d(TAG, "KeepAliveService-->MIUI needs draw overlay permission");
return;
}
//赋值WindowManager&LayoutParam.
params = new WindowManager.LayoutParams();
//设置type.系统提示型窗口,一般都在应用程序窗口之上.
params.type = WindowManager.LayoutParams.TYPE_TOAST;
// 有限使用SYSTEM_ALERT,优先级更高
if (OSJudgementUtil.isMIUI() || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Settings.canDrawOverlays(this))) {
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
//设置效果为背景透明.
params.format = PixelFormat.RGBA_8888;
//设置flags.不可聚焦及不可使用按钮对悬浮窗进行操控.
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//设置窗口初始停靠位置.
params.gravity = Gravity.LEFT | Gravity.TOP;
params.x = 0;
params.y = 0;
//设置悬浮窗口长宽数据.
params.width = 1;
params.height = 1;
//获取浮动窗口视图所在布局.
toucherLayout = new View(this);
//toucherLayout.setBackgroundColor(0x55ffffff);
//添加toucherlayout
ensureWindowManager();
try {
L.d(TAG, "KeepAliveService-->create floating window");
windowManager.addView(toucherLayout,params);
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
private void ensureWindowManager() {
if (windowManager == null) {
windowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
}
}
}
复制代码
public class SinglePixelActivity extends AppCompatActivity {
private static final String TAG = Constants.LOG_TAG;
private ScreenReceiverUtil.ScreenStateListener mScreenStateListenerer = new ScreenReceiverUtil.ScreenStateListener() {
@Override
public void onSreenOn() {
if (!isFinishing()) {
finish();
}
}
@Override
public void onSreenOff() {
}
@Override
public void onUserPresent() {
if (!isFinishing()) {
finish();
}
}
};
private ScreenReceiverUtil mScreenUtil;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
L.d(TAG,"SinglePixelActivity--->onCreate");
Window mWindow = getWindow();
mWindow.setGravity(Gravity.LEFT | Gravity.TOP);
WindowManager.LayoutParams attrParams = mWindow.getAttributes();
attrParams.x = 0;
attrParams.y = 0;
attrParams.height = 1;
attrParams.width = 1;
mWindow.setAttributes(attrParams);
// 绑定SinglePixelActivity到ScreenManager
ScreenManager.getInstance(this).setSingleActivity(this);
mScreenUtil = new ScreenReceiverUtil(this);
mScreenUtil.setScreenReceiverListener(mScreenStateListenerer);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
L.d(TAG, "SinglePixelActivity onTouchEvent-->finsih()");
if (!isFinishing()) {
finish();
}
return false;
}
@Override
protected void onDestroy() {
super.onDestroy();
L.d(TAG,"SinglePixelActivity-->onDestroy()");
if (mScreenUtil != null) {
mScreenUtil.stopScreenReceiverListener();
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
if (!LifecycleHandler.getInstance().isForeground()) {
return;
}
}
try {
Intent intentAlive = new Intent(this, KeepAliveService.class);
startService(intentAlive);
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
}
复制代码
监听系统的锁屏广播,在锁屏的时候开启activity,开屏的关掉即可。
<service
android:name=".plantask.KeepAliveService"
android:enabled="false"
android:exported="true"
android:process=":keepAlive" />
复制代码
service在独立的进程中,由于系统考虑到省电,在锁屏一段时间后会杀掉后台进程,采用这种方式就可以避免了。
局限性: 在Android 5.0系统以后,系统在杀死某个进程的时候同时会杀死该进程群组里面的进程。
Process.killProcessQuiet(app.pid);
Process.killProcessGroup(app.info.uid, app.pid);
复制代码
所以在5.0以后,这个方法也不是很靠谱了,我们可以需要另寻他法了。
2、前台服务
该方法可以说是很靠谱的,主要原理如下:
对于 API level < 18 :调用startForeground(ID, ewNotification()),发送空的Notification ,图标则不会显示。
对于 API level >= 18:在需要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定同样的 ID。Stop掉InnerService ,这样通知栏图标即被移除。 这里我也给出了实际例子:
public void onCreate()
{
super.onCreate();
//如果API大于18,需要弹出一个可见通知,这个可见通知在大于API 25 版本之前可以通过Cancel隐藏
// 所以在API 18 ~ API 24之间启动前台service,并隐藏Notification
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N){
startForeground(NOTICE_ID, new NotificationUtils(this).getNotification("Sunlands", "打卡提醒进程正在运行"));
// 如果觉得常驻通知栏体验不好
// 可以通过启动CancelNoticeService,将通知移除,oom_adj值不变
Intent intent = new Intent(this,CancelNoticeService.class);
startService(intent);
}else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
startForeground(NOTICE_ID, new Notification());
}
mScreenUtil = new ScreenReceiverUtil(this);
mScreenManager = ScreenManager.getInstance(this);
mScreenUtil.setScreenReceiverListener(mScreenStateListenerer);
createFloatingWindow();
L.d(TAG, "KeepAliveService-->KeepAliveService created");
// If app process is killed system, all task scheduled by AlarmManager is canceled.
// We need reschedule all task when KeepAliveService is revived.
TaskUtil.dispatchAllTask(this);
}
复制代码
继续看CancelNoticeService
public class CancelNoticeService extends Service {
private static final String TAG = Constants.LOG_TAG;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
L.d(TAG, "CancelNoticeService-->onStartCommand() begin");
//Notification.Builder builder = new Notification.Builder(this);
//builder.setSmallIcon(R.drawable.earth);
startForeground(KeepAliveService.NOTICE_ID, new NotificationUtils(this).getNotification("Sunlands", "打卡提醒进程正在运行"));
// 开启一条线程,去移除DaemonService弹出的通知
new Thread(new Runnable() {
@Override
public void run() {
// 延迟1s
SystemClock.sleep(50);
// 取消CancelNoticeService的前台
stopForeground(true);
// 移除DaemonService弹出的通知
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
manager.cancel(KeepAliveService.NOTICE_ID);
// 任务完成,终止自己
stopSelf();
L.d(TAG, "CancelNoticeService-->onStartCommand() end");
}
}).start();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
复制代码
这种方式实质上是利用了Android系统service的服务漏洞,微信也是采用此法达到保活目的的。
3、相互唤醒
故名思议,当本进程处于后台优先级很低或者被杀死了,有另外一个进程可以把你唤醒或者拉活,这里有几种方案。
1.利用系统的广播
监听一些系统的广播,比如重启,开启相机等,监听到广播后就可以拉活进程,但是Android N已经取消部分系统广播了。
2、利用使用频率很高的app发出的广播
事实上,QQ,微信这种app在手机中使用频率是非常高的,我们可以去反编译这些app,获取它们可以发出的广播,然后去监听这些广播,再进行进程的拉活。
3.利用第三方推送的机制
像信鸽、极光推送,都有唤醒拉活app的功能。
4.粘性服务和系统服务捆绑
这种方式不怎么靠谱,但是可以算是多一种保险吧,系统自带的service中有onStartCommand这个方法
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_REDELIVER_INTENT;
}
复制代码
我们可以对返回值做特殊处理,处理参数如下:
- START_STICKY
如果系统在onStartCommand返回后被销毁,系统将会重新创建服务并依次调用onCreate和onStartCommand(注意:根据测试Android2.3.3以下版本只会调用onCreate根本不会调用onStartCommand,Android4.0可以办到),这种相当于服务又重新启动恢复到之前的状态了)。
- START_NOT_STICKY
如果系统在onStartCommand返回后被销毁,如果返回该值,则在执行完onStartCommand方法后如果Service被杀掉系统将不会重启该服务。
- START_REDELIVER_INTENT
START_STICKY的兼容版本,不同的是其不保证服务被杀后一定能重启。
系统服务捆绑,使用NotificationListenerService, 只有手机收到通知都会监听到,如果应用收到的消息比较多的话可以采用该办法去处理,并且即使进程被杀死也可以监听到,这是何等的牛逼啊。