Android 进程保活

须要引用请注明出处:juejin.im/post/5ca5f9…android

1、开场白

如今不少公司和开发者但愿本身的app可以长期运行在手机内存中,由于只要该app的进程一直存在,那么咱们就能够干不少事,尽管不少不是很光彩的事,好比偷流量,费电,偷偷安装应用和推送广告信息等等。庆幸的事,道高一尺魔高一丈,android原生系统对如今手机系统作了不少的保护,如今很难保证哪个应用的进程能够一直不被杀死,咱们可以作的就是尽可能保活进程,接下来咱们就对进程保活作个总结。程序员

2、进程相关的基础知识

在正式开始介绍进程保活的知识以前,简单了解一下进程相关的一些东西。首先什么是进程,这些我相信是程序员都会清楚,进程是系统进行分配资源和调度的最小单位,很简单,每一个进程就像一个app运行在手机系统里。shell

1.如何查看进程

咱们能够经过adb shell ps来查看进程的信息:缓存

id 说明
u0_a344 当前用户
9153 pid 进程名
201 ppid 父进程名
1597348 VSIZE进程的虚拟内存大小
com.sunland.staffapp 进程名

2.进程的划分

根据进程当前所处的状态,咱们能够将进程分为5类:前台进程、可见进程、服务进程、后台进程、空进程。每一种进程解释以下:bash

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没与用户进行交互

可见进程也是系统中及其重要的进程,不到万不得已的状况下,系统也是不会杀死可见进程的。app

2.3 服务进程(service process)

某个进程中运行着service,而且该service是经过startService启动的,与用户界面不存在交互的那种service,当内存不足以维持前台进程和可见进程的状况下,会优先杀死服务进程。ide

2.4 后台进程

在程序退到后台,好比用户按了back键或者home,界面看不到了,可是程序仍在运行中,此时的activity处于onpause状态,在任务管理器中能够看到,当系统内存不足的状况下会有限杀死后台进程。布局

2.5 空进程

空进程就是不含有任何active的进程,系统保留的缘由主要是高速缓存,方便下次访问速度很快,若是系统内存不足,首先杀的就是空进程。post

3.如何查看进程的优先级

进程有个参数,oom_adj,通常而言,这个参数的值越小,优先级则越高,处于前台进程的adj为0,固然各个手机厂商的可能会有一点差别,查看adj的方法以下:

能够看到,处于前台进程的adj的值为0。

当按返回键后,程序退到后台,这时候adj变为1

通常而言,进程adj值越大,占用系统内存值越大,优先被杀死。咱们作进程保护就是从这两个方面下手。接下来就是正题了。

3、进程保活的方案

一、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之后,这个方法也不是很靠谱了,咱们能够须要另寻他法了。

二、前台服务

该方法能够说是很靠谱的,主要原理以下:

对于 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的服务漏洞,微信也是采用此法达到保活目的的。

三、相互唤醒

故名思议,当本进程处于后台优先级很低或者被杀死了,有另一个进程能够把你唤醒或者拉活,这里有几种方案。

1.利用系统的广播

监听一些系统的广播,好比重启,开启相机等,监听到广播后就能够拉活进程,可是Android N已经取消部分系统广播了。

二、利用使用频率很高的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, 只有手机收到通知都会监听到,若是应用收到的消息比较多的话能够采用该办法去处理,而且即便进程被杀死也能够监听到,这是何等的牛逼啊。

相关文章
相关标签/搜索