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, 只有手機收到通知都會監聽到,若是應用收到的消息比較多的話能夠採用該辦法去處理,而且即便進程被殺死也能夠監聽到,這是何等的牛逼啊。

相關文章
相關標籤/搜索