Android 进程保活

·  阅读 3750

需要引用请注明出处:juejin.cn/post/684490…

一、开场白

现在很多公司和开发者希望自己的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, 只有手机收到通知都会监听到,如果应用收到的消息比较多的话可以采用该办法去处理,并且即使进程被杀死也可以监听到,这是何等的牛逼啊。

分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改