1. 程式人生 > >Android service程序保護

Android service程序保護

應用程序保活基本就是圍繞兩個方面來展開:

1 儘量保證程序不被殺死。

2 程序被殺死後復活。細分如下:

1)Service重啟

2)程序守護

3)Receiver觸發

4) AlarmManager or JobScheduler迴圈觸發

5)與系統Service捆綁—–可以不考慮,瞭解即可

下面將圍繞這幾點展開討論。

一,基本概念

1.什麼才叫應用程序保活

應用程序保活可以理解為應用位於後臺永遠不能被殺死。這裡的可以簡略地分為兩種情況,第一種是當系統資源緊俏的時候或者基於某種系統自身的後臺執行規則選擇殺死你的後臺應用來獲得更多的資源,第二種是使用者手動呼叫某些安全軟體的清理功能幹掉你的後臺應用。對於Android 5.0以前的系統我們可以考慮以上兩種情況下的後臺常駐,而對於Android 5.0以及以後的版本我們只能基於第一種情況考慮後臺常駐,因為從Android 5.0開始對程序的管理更為嚴格,殺得也更為暴力。

2.Android程序的生命週期

Android系統會盡力保持應用的程序,但是有時為了給新的程序和更重要的程序回收一些記憶體空間,它會移除一些舊的程序。
為了決定哪些程序留下,哪些程序被殺死,系統根據在程序中在執行的元件及元件的狀態,為每一個程序分配了一個優先順序等級。
優先順序最低的程序首先被殺死。
這個程序重要性的層次結構有五個等級,下面就列出這五種程序,按照重要性來排列,最重要的放在最前。

1)前臺程序 Foreground process

處於該狀態下的程序表示其當前正在與使用者互動,是必須存在的,無論如何系統都不會去幹掉一個前臺程序除非系統出現錯誤或者說使用者手動殺掉。那麼系統是通過怎樣的一個規則去判斷某個程序是否前臺程序呢?下面是一些具體的情景:

某個程序持有一個正在與使用者互動的Activity並且該Activity正處於resume的狀態。
某個程序持有一個Service,並且該Service與使用者正在互動的Activity繫結。
某個程序持有一個Service,並且該Service呼叫startForeground()方法使之位於前臺執行。
某個程序持有一個Service,並且該Service正在執行它的某個生命週期回撥方法,比如onCreate()、 onStart()或onDestroy()。
某個程序持有一個BroadcastReceiver,並且該BroadcastReceiver正在執行其onReceive()方法。
可以看到使程序位於前臺的方法還是蠻多的,但是你要知道的事Android是一個碎片化非常嚴重的系統,很多定製的ROM都會修改一部分系統邏輯來做所謂的優化,所以說上述的情景以及下述我們將要講到的其它程序狀態其實都只能說可以在原生系統上完美生效而如果在一些定製ROM中則有可能無效甚至出現詭異的現象。

2)可見程序 Visible process

可見程序與前臺程序相比要簡單得多,首先可見程序不包含任何前臺元件,也就是說不會出現上述前臺程序的任何情境,其次,可見程序依然會影響使用者在螢幕上所能看到的內容,一般來說常見的可見程序情景可以分為兩種:

某個程序持有一個Activity且該Activty並非位於前臺但仍能被使用者所看到,從程式碼的邏輯上來講就是呼叫了onPause()後還沒呼叫onStop()的狀態,從視覺效果來講常見的情況就是當一個Activity彈出一個非全屏的Dialog時。
某個程序持有一個Service並且這個Service和一個可見(或前臺)的Activity繫結。

3)服務程序 Service process

服務程序要好理解很多,如果某個程序中執行著一個Service且該Service是通過startService()啟動也就是說沒有與任何Activity繫結且並不屬於上述的兩種程序狀態,那麼該程序就是一個服務程序。

4)後臺程序 Background process

這裡需要注意的是,我們這兒所說的後臺程序只是指的程序的一種狀態,與我們前後文提到的“後臺程序”是兩個概念,切記。當某個程序處於後臺程序時,其一般會持有一個不可見的Activity,也就是說當Activity隱藏到後臺但未退出時,從程式碼的邏輯上來講就是該Activity的onStop被呼叫但onDestory未被執行的狀態,後臺程序會被系統儲存在一個LRU表中以確保最近使用的程序最後被銷燬。

5)空程序 Empty process

如果一個程序不包含任何活躍的應用元件,則認為是空程序。儲存這種程序的唯一理由是為了快取的需要,為了加快下次要啟動這個程序中的元件時的啟動時間。系統為了平衡程序快取和底層核心快取的資源,經常會殺死空程序。

這五種狀態的程序相對於系統來說的重要性從上至下排列,空程序容易被殺死,其次是後臺程序,然後是服務程序甚至是可見程序,而前臺程序一般則不會被輕易幹掉。系統殺程序會遵循一套規則,而這套規則則是建立在系統可用資源的基礎上,打個比方,如果我的裝置有高達3GB的執行記憶體且可用的記憶體還有2GB,那麼即便是空程序系統也不會去幹掉它,相反如果的裝置只有256M的執行記憶體且可用記憶體不足16M,這時即便是可見程序也會被系統考慮幹掉。這套依據系統資源來殺掉程序的規則Android稱之為Low Memory Killer,而且Android在上述五種程序狀態的基礎上衍生出了更多的程序相關定義,比較重要的兩個是程序的Importance等級以及adj值,關於這兩個定義大家可以不必深究,但是要有一定的理解,這兩個玩意是具體決定了系統在資源吃緊的情況下該殺掉哪些程序。其中Importance等級在ActivityManager.RunningAppProcessInfo中宣告:

public static class RunningAppProcessInfo implements Parcelable {
   /**
     * Constant for {@link #importance}: This process is running the
     * foreground UI; that is, it is the thing currently at the top of the screen
     * that the user is interacting with.
     */
    public static final int IMPORTANCE_FOREGROUND = 100;

    /**
     * Constant for {@link #importance}: This process is running a foreground
     * service, for example to perform music playback even while the user is
     * not immediately in the app.  This generally indicates that the process
     * is doing something the user actively cares about.
     */
    public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;

    /**
     * Constant for {@link #importance}: This process is running the foreground
     * UI, but the device is asleep so it is not visible to the user.  This means
     * the user is not really aware of the process, because they can not see or
     * interact with it, but it is quite important because it what they expect to
     * return to once unlocking the device.
     */
    public static final int IMPORTANCE_TOP_SLEEPING = 150;

    /**
     * Constant for {@link #importance}: This process is running something
     * that is actively visible to the user, though not in the immediate
     * foreground.  This may be running a window that is behind the current
     * foreground (so paused and with its state saved, not interacting with
     * the user, but visible to them to some degree); it may also be running
     * other services under the system's control that it inconsiders important.
     */
    public static final int IMPORTANCE_VISIBLE = 200;

    /**
     * Constant for {@link #importance}: This process is not something the user
     * is directly aware of, but is otherwise perceptable to them to some degree.
     */
    public static final int IMPORTANCE_PERCEPTIBLE = 130;

    /**
     * Constant for {@link #importance}: This process is running an
     * application that can not save its state, and thus can't be killed
     * while in the background.
     * @hide
     */
    public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

    /**
     * Constant for {@link #importance}: This process is contains services
     * that should remain running.  These are background services apps have
     * started, not something the user is aware of, so they may be killed by
     * the system relatively freely (though it is generally desired that they
     * stay running as long as they want to).
     */
    public static final int IMPORTANCE_SERVICE = 300;

    /**
     * Constant for {@link #importance}: This process process contains
     * background code that is expendable.
     */
    public static final int IMPORTANCE_BACKGROUND = 400;

    /**
     * Constant for {@link #importance}: This process is empty of any
     * actively running code.
     */
    public static final int IMPORTANCE_EMPTY = 500;

    /**
     * Constant for {@link #importance}: This process does not exist.
     */
    public static final int IMPORTANCE_GONE = 1000;
}

而adj值則在ProcessList中定義:

final class ProcessList {
// OOM adjustments for processes in various states:

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 16;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 15;
static final int CACHED_APP_MIN_ADJ = 9;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 8;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app.  This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 7;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 6;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 5;

// This is a process with a heavy-weight application.  It is in the
// background, but we want to try to avoid killing it.  Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 4;

// This is a process currently hosting a backup operation.  Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 3;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 2;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 1;

// This is the process running the current foreground app.  We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -11;

// This is a system persistent process, such as telephony.  Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -12;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -16;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -17;
}

Importance等級與adj值在ActivityManagerService中被關聯起來,相較於Importance等級而言adj值可以賦予我們更多的參考價值,從上述adj值的定義中我們可以看到,值越小優先順序越高,比如native程序的adj值為-17,對於這個adj值的程序來說,系統根本不會動它一分一毫,實質上當程序的adj值去到2時系統就很少會因為其它原因而去殺死它。在平時的開發中,我們可以通過檢視節點目錄proc下的相關程序來獲取其相應的adj值:(程序相關檢視,請檢視前面一篇文章)

adb shell
cat /proc/32366/oom_adj

注意“32366”為程序ID,你可以通過上面我們提到過的ps命令獲取相關程序的ID。
cat檢視程序的adj值後我們會得到其返回結果“0”,說明當前程序正位於前臺,此刻我們再按返回鍵退出應用後再次檢視adj值發現其會變為“8”,也就是說程序優先順序變得很低了。這裡需要注意的是上述操作均在原生的Android系統上執行,如果是其它的定製ROM則輸出及結果可能會有出入,adj值僅僅能作為一個參考而非絕對的常量。

二,後臺程序常駐的策略與選擇

上面說了這麼多,其實我們也差不多能總結出一套規律,要想讓我們的後臺程序長存,我們首先要應付的就是系統的“自殺”機制,而後臺程序被殺的首要原因就是我們的程序優先順序太低同時系統可用資源太少,其次如果真的被系統幹掉,那麼我們得重新拉起程序讓它重複上次的故事,因此我們的程序後臺常駐策略最終可以歸結為兩點:

1.輕量化程序

所謂輕量化程序,其實就是迫使我們的程序佔用儘量少的資源,但是我們知道的是一個執行中的App就算功能再少也會佔用相當一部分資源,因此在這裡我們是不應該去想著讓我們的應用主程序在後臺常駐,讓一個沒有看不見的介面在後臺跑既沒意義也沒必要,因此大多數情況下我們都會使用一個新的程序去常駐在後臺,而這個程序一般會持有一個Service,後臺所有的齷齪事都會交由它去處理,畢竟在Android中幹這種齷齪事的也只有Service了:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test.daemon">

    <application>
        <service
            android:name=".services.DaemonService"
            android:process=":service" />
    </application>
</manifest>

如上所示我們宣告一個services並通過startService的方式啟動它,在這個Service中我們通過一個死迴圈來不斷Toast一段資訊:

package com.test.daemon.services;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.widget.Toast;

/**
 *
 * @author caoyinfei
 * @version [版本號, 2016/5/11]
 * @see [相關類/方法]
 * @since [產品/模組版本]
 */
public class DaemonService extends Service {
    private static boolean sPower = true;
    private Handler handler = new Handler();

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (sPower) {
                    if (System.currentTimeMillis() >= 123456789000000L) {
                        sPower = false;
                    }
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(DaemonService.this, "====" +
                                    System.currentTimeMillis(), Toast.LENGTH_SHORT).show();
                        }
                    });
                    SystemClock.sleep(3000);
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
}

如果你想在程序中的Service裡處理更復雜的邏輯,務必儘量多地使用弱引用或軟引用,或者說盡量多地去置空一些不必要的引用並在需要的時候再賦值,其次Service本身也提供了onTrimMemory方法來告訴我們系統何時需要釋放掉不必要的資源,靈活使用這類方法可以最大程度的讓我們的後臺Service長盛不衰。還是那句話,儘量讓我們的後臺程序做更少的事情,及時釋放資源,才是硬道理。

2.被殺後重啟

可以這麼說,沒有任何一個應用程序可以做到永遠不被殺死,除非系統給你開了後門,程序被殺並不可怕,可怕的是殺掉後就永遠GG思密達了,所以如何使我們的程序可以在被殺後重啟呢?這就需要使用到一個叫做守護程序的東西,原理很簡單,多開一個程序,讓這個程序輪詢檢查目標程序是否存活,死了的話將其拉起,同時目標程序也需要做一個輪詢檢查守護程序是否存活,死了的話也將其拉起,相互喚醒一起齷齪。不過即便如此有時候意外也是難免的,在Android中我們還可以通過AlarmManager和系統廣播來在一定條件下喚醒逝去的程序。後面會詳細說明。

三,後臺程序常駐的實現

1 儘量保證程序不被殺死

我們上面曾說到adj值越小的程序越不容易被殺死,相對普通程序來說能讓adj去到0顯然是最完美的,可是我們如何才能讓一個完全沒有可見元素的後臺程序擁有前臺程序的狀態呢?Android給了Service這樣一個功能:startForeground,它的作用就像其名字一樣,將我們的Service置為前臺,不過你需要傳送一個Notification:

public class DaemonService extends Service {
    @Override
    public void onCreate() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(250, builder.build());
        } else {
            startForeground(250, new Notification());
        }
    }
}

值得注意的是在Android 4.3以前我們可以通過構造一個空的Notification,這時通知欄並不會顯示我們傳送的Notification,但是自從4.3以後谷歌似乎意識到了這個問題,太多流氓應用通過此方法強制讓自身悄無聲息置為前臺,於是從4.3開始谷歌不再允許構造空的Notification,如果你想將應用置為前臺那麼請傳送一個可見的Notification以告知使用者你的應用程序依然在後臺執行,這麼就比較噁心了,本來我的程序是想後臺齷齪地執行,這下非要讓老子暴露出來,因此我們得想辦法將這個Notification給幹掉。上面的程式碼中我們在傳送Notification的時候給了其一個唯一ID,那麼問題來了,假設我啟動另一個Service同時也讓其傳送一個Notification使自己置為前臺,並且這個Notification的標誌值也跟上面的一樣,然後再把它取消掉再停止掉這個Service的前臺顯示會怎樣呢:

public class DaemonService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(250, builder.build());
            startService(new Intent(this, CancelService.class));
        } else {
            startForeground(250, new Notification());
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }
}
public class CancelService extends Service {
    @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) {
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(250, builder.build());
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(1000);
                    stopForeground(true);
                    NotificationManager manager =
                            (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                    manager.cancel(250);
                    stopSelf();
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="teach.focus.notification">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".DaemonService"
            android:enabled="true"
            android:exported="true"
            android:process=":service" />
        <service
            android:name=".CancelService"
            android:enabled="true"
            android:process=":service"
            android:exported="true"></service>
    </application>

</manifest>

tips:
android:exported=”true”這個屬性要注意一下:
該屬性用來標示,其它應用的元件是否可以喚醒service或者和這個service進行互動:true可以,false不可以。如果為false,只有同一個應用的元件或者有著同樣user ID的應用可以啟動這個service或者繫結這個service。

預設值根據當前service是否有intent filter來定。如果沒有任何filter意味著當前service只有在被詳細的描述class name後才會被喚醒。這意味這當前service只能在應用內部使用(因為其它應用不知道這個class name).所以在這種情況下它的預設值為 false.從另一方面講,如果至少有一個filter的話那麼就意味著這個service可以被外部應用使用,這種情況下預設值為true。

其實,不只有這個屬性可以指定service是否暴露給其它應用。你也可以使用permission來限制外部實體喚醒當前service

如上程式碼所示,我們先在DaemonService中傳送一個Notification並將其置為前臺,而後如果是4.3及其以上的版本的話我們就start另外一個CancelService,這個CancelService的邏輯很簡單,傳送與DaemonService中ID相同的Notification然後將其取消並取消自己的前臺顯示,然後停止,大家看到這裡可能覺得很奇葩,其實我們就是自導自演裝了一次逼。其實就是個小技巧而已,雖然我們通過CancelService幹掉了前臺顯示需要的Notification,但是,請大家檢視一下當前程序的adj值,你就會發現,我們DaemonService所在的程序竟然還是可見程序!

這裡寫圖片描述

adj值變成了1,預設情況應該是8,是不是很6呢,前段時間就曾有人扒出支付寶曾經以這樣的方式讓自己的後臺程序常駐,但是這個方法有個小小的bug,在一些手機上,傳送前臺通知會喚醒裝置並點亮螢幕,這樣會很耗電而且在電量管理介面系統還會統計到你的程序點亮螢幕的次數,不是很好。

除了使Service置為前臺顯示來提權外,還有很多不是很實用的方式,比如提升優先順序和使用persistent許可權等:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.aigestudio.daemon">

    <application
        android:persistent="true">
    </application>
</manifest>

不過這些方法意義都不會很大。

2 程序被殺死後復活
任何一個普通的應用程序都會有被幹掉的那麼一天,除非你跟系統有關係有契約,說白了就是ROM是定製的且可以給你開特殊許可權,不然的話,系統總會在某個時刻因為某些原因把你殺掉,被殺掉不可怕,可怕的是被殺掉後就再也活不過來了……因此,我們得制定各種策略,好讓程序能在被殺後可以自啟。

1)Service重啟

Android的Service是一個非常特殊的元件,按照官方的說法是用於處理應用一些不可見的後臺操作,對於Service我們經常使用,也知道通過在onStartCommand方法中返回不同的值可以告知系統讓系統在Service因為資源吃緊被幹掉後可以在資源不緊張時重啟:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    return START_REDELIVER_INTENT;
}

關於onStartCommand方法的返回值,系統一共提供了四個:

START_STICKY

如果Service程序因為系統資源吃緊而被殺掉,則保留Service的狀態為起始狀態,但不保留傳遞過來的Intent物件,隨後當系統資源不緊張時系統會嘗試重新建立Service,由於服務狀態為開始狀態,所以建立服務後一定會呼叫onStartCommand方法,如果在此期間沒有任何啟動命令被傳遞到Service,那麼引數Intent將為null。

START_STICKY_COMPATIBILITY

START_STICKY的相容版本,不同的是其不保證服務被殺後一定能重啟。

START_NOT_STICKY

與START_STICKY恰恰相反,如果返回該值,則在執行完onStartCommand方法後如果Service被殺掉系統將不會重啟該服務。

START_REDELIVER_INTENT

同樣地該值與START_STICKY不同的是START_STICKY重啟後不會再傳遞之前的Intent,但如果返回該值的話系統會將上次的Intent重新傳入。

一般情況下,作為一個後臺常駐的Service,個人建議是儘量不要傳遞Intent進來,避免有時候邏輯不好處理。同時需要注意的是,預設情況下Service的返回值就是START_STICKY或START_STICKY_COMPATIBILITY:

因此如果沒有什麼特殊原因,我們也沒必要更改。
雖然Service預設情況下是可以被系統重啟的,但是在某些情況or某些定製ROM上會因為各種原因而失效,因此我們不能單靠這個返回值來達到程序重啟的目的。

2)程序守護
關於程序守護其實也不是什麼高深的技術,其邏輯也很簡單,AB兩個程序,A程序裡面輪詢檢查B程序是否存活,沒存活的話將其拉起,同樣B程序裡面輪詢檢查A程序是否存活,沒存活的話也將其拉起,而我們的後臺邏輯則隨便放在某個程序裡執行即可,一個簡單的例子是使用兩個Service:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="teach.focus.notification">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".DaemonService"
            android:enabled="true"
            android:exported="false"
            android:process=":service" />
        <service
            android:name=".ProtectService"
            android:enabled="true"
            android:exported="false"
            android:process=":remote"/>
    </application>

</manifest>

使用兩個程序分別裝載兩個Service,在兩個Service中開輪詢,互相喚醒:

public class DaemonService extends Service {
    private static boolean isRunning;
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
         if (!isRunning) {
            isRunning = true;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(3000);
                    isRunning = false;
                    startService(new Intent(DaemonService.this, ProtectService.class));
                    System.out.println("DaemonService");
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }
}
public class ProtectService extends Service {
     private static boolean isRunning;

    @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 (!isRunning) {
            isRunning = true;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(1500);
                    System.out.println("ProtectService");
                    isRunning = false;
                    startService(new Intent(ProtectService.this, DaemonService.class));
                }
            }).start();
        }
        return START_STICKY;
    }
}

在原生系統及相當一部分的ROM下上述方法就已經很有用了,即便應用主程序被使用者在Recent Task中被清理也無妨上述程序的進行,該方法直至Android 6.0也相當有效,但是對於一些深度定製的ROM就顯得很雞肋,比如魅族、小米。

有些時候,我們會使用一個更為純淨的程序來作為守護程序而非藉助Service,你可以使用C層fork,也可以直接Java建立一個新的程序,在5.0以前的版本中,這兩種方式建立的程序有一定的區別,不過5.0以後已經變得不再那麼重要了,不依賴Android環境的程序唯一的好處是可以做到更輕量,除此之外並無卵用,這裡以Java為例,使用Java的方式建立程序有兩種途徑,一是通過Runtime;二是通過ProcessBuilder,後者提供了更多的選擇,使用ProcessBuilder建立程序的過程也很簡單,三部曲:構建環境變數、指定使用者目錄、執行命令:(這邊不詳細介紹了)

3)Receiver觸發

使用Receiver來檢測目標程序是否存活不失為一個好方法,靜態註冊一系列廣播,什麼開機啟動、網路狀態變化、時區地區變化、充電狀態變化等等,這聽起來好像很6,而且在大部分手機中都是可行的方案,但是對於深度定製的ROM,是的,又是深度定製,你沒有看錯,而且代表性人物還是魅族、小米,這兩個業界出了名的喜歡“深度定製”系統。

大部分手機可行的方法如下:

一. 在眾多的Intent的action動作中,Intent.ACTION_TIME_TICK是比較特殊的一個,根據SDK描述:

Broadcast Action: The current time has changed. Sent every minute. You can not receive this through components declared in manifests, only by exlicitly registering for it withContext.registerReceiver()

意思是說這個廣播動作是以每分鐘一次的形式傳送。但你不能通過在manifest.xml裡註冊的方式接收到這個廣播,只能在程式碼裡通過registerReceiver()方法註冊。

1.服務裡面註冊廣播

 /**
 * 呼叫startForeground,提高程序adj值
 * 該程序為需要保護的程序
 */
public class DaemonService extends Service {
    private static boolean isRunning;

    AlarmManager mAlarmManager = null;
    PendingIntent mPendingIntent = null;

    @Override
    public void onCreate() {
        super.onCreate();
        //服務啟動廣播接收器,使得廣播接收器可以在程式退出後在後天繼續執行,接收系統時間變更廣播事件
        TimeChangeReceiver receiver=new TimeChangeReceiver();
        registerReceiver(receiver,new IntentFilter(Intent.ACTION_TIME_TICK));
    }

2.廣播裡面判斷服務是否存在,不存在則開啟服務。

public class TimeChangeReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        //啟動service
        boolean isServiceRunning = false;
        System.out.println("TimeChangeReceiver");
        if (intent.getAction().equals(Intent.ACTION_TIME_TICK)) {
            //檢查Service狀態
            ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
                if ("so.xxxx.xxxxService".equals(service.service.getClassName())) {
                    isServiceRunning = true;
                }
            }
            if (!isServiceRunning) {
                Intent i = new Intent(context, DaemonService.class);
                context.startService(i);
            }
        }
    }
}

自從Android 3.1開始系統對我們的應用增加了一種叫做STOPPED的狀態,什麼叫STOPPED?就是安裝了之後從未啟動過的,大家可能經常在網上看到對開機廣播的解釋,說要想應用正確接收到開機廣播那麼就得先啟動一下應用,這個說法的技術支援就來源於此,因為自Android 3.1後所有的系統廣播都會在Intent新增一個叫做FLAG_EXCLUDE_STOPPED_PACKAGES的標識,說白了就是所有處於STOPPED狀態的應用都不可以接收到系統廣播,是不是感到很蛋疼菊緊?沒事、更蛋疼的還在後面。在原生的系統中,當應用初次啟動後就會被標識為非STOPPED狀態,而且再也沒有機會被打回原形除非重新安裝應用,但是,但是,但是,一些深(fang)度(ni)定(gou)制(pi)的ROM按耐不住了,這樣的話,如果每個應用都這麼搞豈不是後臺一大堆程序在跑?所以這些深度定製的ROM會在它們的清理邏輯中,比如小米的長按Home,魅族的Recent Task加入了將應用重置為STOPPED的邏輯,也就是直接或間接地呼叫ActivityManagerService中的forceStopPackageLocked:

private void forceStopPackageLocked(final String packageName, int uid, String reason) {
    // 省略一行程式碼……

    Intent intent = new Intent(Intent.ACTION_PACKAGE_RESTARTED,
            Uri.fromParts("package", packageName, null));

    // 省略多行程式碼……

    broadcastIntentLocked(null, null, intent,
            null, null, 0, null, null, null, AppOpsManager.OP_NONE,
            null, false, false, MY_PID, Process.SYSTEM_UID, UserHandle.getUserId(uid));
}

可以看到上面的程式碼裡傳送了一個ACTION_PACKAGE_RESTARTED廣播,這個廣播會呼叫broadcastIntentLocked等方法來將相應的應用重置為STOPPED狀態,因此一旦我們的應用被重置為STOPPED則再也無法接受到相應的系統廣播除非再次啟動一下應用清除掉STOPPED標識。

4) AlarmManager or JobScheduler迴圈觸發

使用AlarmManage間隔一定的時間來檢測並喚醒程序不失為一個好方法,雖然說從Android 4.4和小米的某些版本開始AlarmManage已經變得不再準確但是對我們拉活程序來說並不需要太精確的時間,對於4.4以前的版本,我們只需通過AlarmManage的setRepeating方法即可達到目的:

 Intent sendIntent = new Intent(getApplicationContext(), DaemonService.class);
        mAlarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
        mPendingIntent = PendingIntent.getService(this, 0, sendIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        long now = System.currentTimeMillis();
        mAlarmManager.setInexactRepeating(AlarmManager.RTC, now, 60000, mPendingIntent);

而對於4.4及其以上的版本來說如果我們想精確的方式重複啟動的話,就得使用一點小手段,在4.4及其以上的版本中Android提供給我們一個新的API:setExact,顧名思義就是精確啟動,但是與之前版本不同的是,4.4開始並不能精確地重複啟動,也就是不能像setRepeating那樣,setExact只能被喚醒一次,那麼該如何做到重複精確呢?其實很簡單,我們每次通過AlarmManager喚醒時都發送一個廣播,在這個廣播裡我們處理一些必要的邏輯,爾後又設定一次AlarmManager,如此往復迴圈,實質就是對廣播做一個遞迴以達到目的:

public class DReceiver extends BroadcastReceiver {
    private PendingIntent mPendingIntent;
    private AlarmManager am;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (null == intent) return;
        if (null == mPendingIntent) {
            Intent i = new Intent(context, DReceiver.class);
            i.putExtra("time", System.currentTimeMillis() + 3000);
            mPendingIntent = PendingIntent.getService(context, 0x123, i,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }
        if (null == am)
            am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        long time = intent.getLongExtra("time", System.currentTimeMillis());
        am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, mPendingIntent);
    }
}

上述程式碼中我們使用Intent來傳遞資料,事實上我們應該使用持久化的資料來儲存這個time值,儘量少用甚至不用Intent。
看到這裡很多朋友會問是不是OK了啊?很遺憾地告訴你NO!為什麼呢?不知道大家是否在開發的過程中遇到這樣的問題,你設定的Alarm在應用退出後發現過不了多久居然就沒了,特別是在某些深度定製的系統上,上面我們曾提到Receiver如果應用被置為STOPPED狀態就再也無法接收到廣播,很不幸地告訴你AlarmManager也一樣,在AlarmManagerService中有一個BroadcastReceiver,這個BroadcastReceiver會接收上面我們曾說的ACTION_PACKAGE_RESTARTED廣播:

class UninstallReceiver extends BroadcastReceiver {
    public UninstallReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
        filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
        filter.addDataScheme("package");
        getContext().registerReceiver(this, filter);

        // 省去幾行程式碼……
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        synchronized (mLock) {
            String action = intent.getAction();
            String pkgList[] = null;
            if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
                // 省去幾行程式碼……

            } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
                pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
            } else if (Intent.ACTION_USER_STOPPED.equals(action)) {
                // 省去幾行程式碼……

            }