1. 程式人生 > >Android Serivce 調優與保活

Android Serivce 調優與保活

說到Android 效能優化, 不可避免地會說到Service調優。 本篇文章會從調優與保活兩個部分寫。

調優

提到Service 難免會想到這傢伙是一個後臺服務,專門用來處理常駐後臺的工作的元件。

就像即時通訊:service來做常駐後臺的心跳傳輸。
核心服務儘可能地輕, 可以通過核心服務放在簡單的一段話裡, 把其他主要執行任務的分支,放在其他Service中。
很多人喜歡把所有的後臺操作都集中在一個service裡面。
為核心服務專門做一個程序,跟其他的所有後臺操作隔離。
樹大招風,核心服務千萬要輕。因為即使系統不殺死,使用者發現這個應用比較耗記憶體,耗電也會kill掉。

當然,如果僅僅是一個執行耗時任務,而不希望Service長時間在後臺執行 可以考慮使用IntentService , 對於IntentService的使用與原理可以參考我的另一篇文章 , Handler訊息機制文中 IntentService 一部分,https://blog.csdn.net/shinnexi/article/details/80385357

還有的像筆記一類的應用: 需要短時間或者不定時,亦或使用者的操作觸發的service 同步的操作。

這樣的Service 架構 就符合核心Service負責任務的排程, 其他子Service 負責任務離開應用時的同步,或者執行時同步。

其實無論怎麼調優,Service中執行緒的執行都需要時間, 如果被使用者kill了怎麼辦, 核心架構設計的再好也沒有任何意義了。
那麼調優的話題就轉變成了Service保活。

保活

目前市面上的應用, 其實市面上除了廠商對微信和QQ進行了特別關照外,出廠商自己關照的APP外都無法做到保活。 接下來會對 Android 程序拉活進行一個總結。
其實活期保活,就是Android 開發界無休無止的一個話題,其複雜度可見一斑。

Android 程序拉活包括兩個層面:

A. 提供程序優先順序,降低程序被殺死的概率

B. 在程序被殺死後,進行拉活

本文下面就從這兩個方面做一下總結。

1. 程序的優先順序

Android 系統將盡量長時間地保持應用程序,但為了新建程序或執行更重要的程序,最終需要清除舊程序來回收記憶體。 為了確定保留或終止哪些程序,系統會根據程序中正在執行的元件以及這些元件的狀態,將每個程序放入“重要性層次結構”中。 必要時,系統會首先消除重要性最低的程序,然後是清除重要性稍低一級的程序,依此類推,以回收系統資源。

程序的重要性,劃分5級:

前臺程序(Foreground process)

可見程序(Visible process)

服務程序(Service process)

後臺程序(Background process)

空程序(Empty process)

前臺程序的重要性最高,依次遞減,空程序的重要性最低,下面分別來闡述每種級別的程序

1.1. 前臺程序 —— Foreground process

使用者當前操作所必需的程序。通常在任意給定時間前臺程序都為數不多。只有在記憶體不足以支援它們同時繼續執行這一萬不得已的情況下,系統才會終止它們。

A. 擁有使用者正在互動的 Activity(已呼叫 onResume())

B. 擁有某個 Service,後者繫結到使用者正在互動的 Activity

C. 擁有正在“前臺”執行的 Service(服務已呼叫 startForeground())

D. 擁有正執行一個生命週期回撥的 Service(onCreate()、onStart() 或 onDestroy())

E. 擁有正執行其 onReceive() 方法的 BroadcastReceiver

1.2. 可見程序 —— Visible process

沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程序。可見程序被視為是極其重要的程序,除非為了維持所有前臺程序同時執行而必須終止,否則系統不會終止這些程序。

A. 擁有不在前臺、但仍對使用者可見的 Activity(已呼叫 onPause())。

B. 擁有繫結到可見(或前臺)Activity 的 Service

1.3. 服務程序 —— Service process

儘管服務程序與使用者所見內容沒有直接關聯,但是它們通常在執行一些使用者關心的操作(例如,在後臺播放音樂或從網路下載資料)。因此,除非記憶體不足以維持所有前臺程序和可見程序同時執行,否則系統會讓服務程序保持執行狀態。

A. 正在執行 startService() 方法啟動的服務,且不屬於上述兩個更高類別程序的程序。

1.4. 後臺程序 —— Background process

後臺程序對使用者體驗沒有直接影響,系統可能隨時終止它們,以回收記憶體供前臺程序、可見程序或服務程序使用。 通常會有很多後臺程序在執行,因此它們會儲存在 LRU 列表中,以確保包含使用者最近檢視的 Activity 的程序最後一個被終止。如果某個 Activity 正確實現了生命週期方法,並儲存了其當前狀態,則終止其程序不會對使用者體驗產生明顯影響,因為當用戶導航回該 Activity 時,Activity 會恢復其所有可見狀態。

A. 對使用者不可見的 Activity 的程序(已呼叫 Activity的onStop() 方法)

1.5. 空程序 —— Empty process

保留這種程序的的唯一目的是用作快取,以縮短下次在其中執行元件所需的啟動時間。 為使總體系統資源在程序快取和底層核心快取之間保持平衡,系統往往會終止這些程序。

A. 不含任何活動應用元件的程序

2. Android 程序回收策略

Android 中對於記憶體的回收,主要依靠 Lowmemorykiller 來完成,是一種根據 OOM_ADJ 閾值級別觸發相應力度的記憶體回收的機制。

關於 OOM_ADJ 紅色部分代表比較容易被殺死的 Android 程序(OOM_ADJ>=4),綠色部分表示不容易被殺死的 Android 程序,其他表示非 Android 程序(純 Linux 程序)。在 Lowmemorykiller 回收記憶體時會根據程序的級別優先殺死 OOM_ADJ 比較大的程序,對於優先順序相同的程序則進一步受到程序所佔記憶體和程序存活時間的影響。

Android 手機中程序被殺死可能有 系統記憶體清除,安全軟體的記憶體清除,使用者的執行時清除,設定中清除。

綜上,可以得出減少程序被殺死概率無非就是想辦法提高程序優先順序,減少程序在記憶體不足等情況下被殺死的概率。

3. 提升程序優先順序的方案

3.1. 利用 Activity 提升許可權

3.1.1. 方案設計思想

監控手機鎖屏解鎖事件,在螢幕鎖屏時啟動1個畫素的 Activity,在使用者解鎖時將 Activity 銷燬掉。注意該 Activity 需設計成使用者無感知。

通過該方案,可以使程序的優先順序在螢幕鎖屏時間由4提升為最高優先順序1。

3.1.2. 方案適用範圍

適用場景: 本方案主要解決第三方應用及系統管理工具在檢測到鎖屏事件後一段時間(一般為5分鐘以內)內會殺死後臺程序,已達到省電的目的問題。

適用版本: 適用於所有的 Android 版本。

3.1.3. 方案具體實現

首先定義 Activity,並設定 Activity 的大小為1畫素:

其次,從 AndroidManifest 中通過如下屬性,排除 Activity 在 RecentTask 中的顯示:

最後,控制 Activity 為透明:

 <style name="KeepLiveStyle">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:backgroundDimEnabled">false</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowAnimationStyle">@null</item>
        <item name="android:windowDisablePreview">true</item>
        <item name="android:windowNoDisplay">false</item>
 </style>

方法就是監聽鎖屏與否,來控制1*1px的Activity是否顯示,於此同事來啟動CoreService

Activity 啟動與銷燬時機的控制:

3.2. 利用 Notification 提升許可權

3.2.1. 方案設計思想

Android 中 Service 的優先順序為4,通過 setForeground 介面可以將後臺 Service 設定為前臺 Service,使程序的優先順序由4提升為2,從而使程序的優先順序僅僅低於使用者當前正在互動的程序,與可見程序優先順序一致,使程序被殺死的概率大大降低。

3.2.2. 方案實現挑戰

從 Android2.3 開始呼叫 setForeground 將後臺 Service 設定為前臺 Service 時,必須在系統的通知欄傳送一條通知,也就是前臺 Service 與一條可見的通知時繫結在一起的。

對於不需要常駐通知欄的應用來說,該方案雖好,但卻是使用者感知的,無法直接使用。

這樣的例子常駐通知欄,比如說360 安全衛士, 墨跡天氣, 萬年曆等應用的保活方案。

3.2.3. 方案挑戰應對措施

通過實現一個內部 Service,在 LiveService 和其內部 Service 中同時傳送具有相同 ID 的 Notification,然後將內部 Service 結束掉。隨著內部 Service 的結束,Notification 將會消失,但系統優先順序依然保持為2。

3.2.4. 方案適用範圍

適用於目前已知所有版本。

4. 程序死後拉活的方案

4.1. 利用系統廣播拉活

4.1.1. 方案設計思想

在發生特定系統事件時,系統會發出響應的廣播,通過在 AndroidManifest 中“靜態”註冊對應的廣播監聽器,即可在發生響應事件時拉活。

常用的用於拉活的廣播事件包括:

4.1.2. 方案適用範圍

適用於全部 Android 平臺。但存在如下幾個缺點:

1) 廣播接收器被管理軟體、系統軟體通過“自啟管理”等功能禁用的場景無法接收到廣播,從而無法自啟。

2) 系統廣播事件不可控,只能保證發生事件時拉活程序,但無法保證程序掛掉後立即拉活。

因此,該方案主要作為備用手段。

4.2. 利用第三方應用廣播拉活

4.2.1. 方案設計思想

該方案總的設計思想與接收系統廣播類似,不同的是該方案為接收第三方 Top 應用廣播。

通過反編譯第三方 Top 應用,如:手機QQ、微信、支付寶、UC瀏覽器等,以及友盟、信鴿、個推等 SDK,找出它們外發的廣播,在應用中進行監聽,這樣當這些應用發出廣播時,就會將我們的應用拉活。

4.2.2. 方案適用範圍

該方案的有效程度除與系統廣播一樣的因素外,主要受如下因素限制:

1) 反編譯分析過的第三方應用的多少

2) 第三方應用的廣播屬於應用私有,當前版本中有效的廣播,在後續版本隨時就可能被移除或被改為不外發。

這些因素都影響了拉活的效果。

4.3. 利用系統Service機制拉活

4.3.1. 方案設計思想

將 Service 設定為 START_STICKY,利用系統機制在 Service 掛掉後自動拉活:

@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }
常用的返回值有:START_NOT_STICKY、START_SICKY和START_REDELIVER_INTENT,這三個都是靜態常理值。

START_NOT_STICKY:表示當Service執行的程序被Android系統強制殺掉之後,不會重新建立該Service,如果想重新例項化該Service,就必須重新呼叫startService來啟動。

使用場景:表示當Service在執行工作中被中斷幾次無關緊要或者對Android記憶體緊張的情況下需要被殺掉且不會立即重新建立這種行為也可接受的話,這是可以在onStartCommand返回值中設定該值。如在Service中定時從伺服器中獲取最新資料

START_STICKY:表示Service執行的程序被Android系統強制殺掉之後,Android系統會將該Service依然設定為started狀態(即執行狀態),但是不再儲存onStartCommand方法傳入的intent物件,然後Android系統會嘗試再次重新建立該Service,並執行onStartCommand回撥方法,這時onStartCommand回撥方法的Intent引數為null,也就是onStartCommand方法雖然會執行但是獲取不到intent資訊。

使用場景:如果你的Service可以在任意時刻執行或結束都沒什麼問題,而且不需要intent資訊,那麼就可以在onStartCommand方法中返回START_STICKY,比如一個用來播放背景音樂功能的Service就適合返回該值。

START_REDELIVER_INTENT:表示Service執行的程序被Android系統強制殺掉之後,與返回START_STICKY的情況類似,Android系統會將再次重新建立該Service,並執行onStartCommand回撥方法,但是不同的是,Android系統會再次將Service在被殺掉之前最後一次傳入onStartCommand方法中的Intent再次保留下來並再次傳入到重新建立後的Service的onStartCommand方法中,這樣我們就能讀取到intent引數。
4.3.2. 方案適用範圍

如下兩種情況無法拉活:

Service 第一次被異常殺死後會在5秒內重啟,第二次被殺死會在10秒內重啟,第三次會在20秒內重啟,一旦在短時間內 Service 被殺死達到5次,則系統不再拉起。

程序被取得 Root 許可權的管理工具或系統工具通過 forestop 停止掉,無法重啟。

4.4. 利用Native程序拉活

4.4.1. 方案設計思想

主要思想:利用 Linux 中的 fork 機制建立 Native 程序,在 Native 程序中監控主程序的存活,當主程序掛掉後,在 Native 程序中立即對主程序進行拉活。

主要原理:在 Android 中所有程序和系統元件的生命週期受 ActivityManagerService 的統一管理。而且,通過 Linux 的 fork 機制建立的程序為純 Linux 程序,其生命週期不受 Android 的管理。

4.4.2. 方案實現挑戰

挑戰一:在 Native 程序中如何感知主程序死亡。

要在 Native 程序中感知主程序是否存活有兩種實現方式:

在 Native 程序中通過死迴圈或定時器,輪訓判斷主程序是否存活,檔主程序不存活時進行拉活。該方案的很大缺點是不停的輪詢執行判斷邏輯,非常耗電。

在主程序中建立一個監控檔案,並且在主程序中持有檔案鎖。在拉活程序啟動後申請檔案鎖將會被堵塞,一旦可以成功獲取到鎖,說明主程序掛掉,即可進行拉活。由於 Android 中的應用都運行於虛擬機器之上,Java 層的檔案鎖與 Linux 層的檔案鎖是不同的,要實現該功能需要封裝 Linux 層的檔案鎖供上層呼叫。

封裝 Linux 檔案鎖的程式碼或Native 層中堵塞申請檔案鎖的部分程式碼:
一般情況就是fork native 程序 對檔案進行讀寫,加鎖,保證程序的存活。

挑戰二:在 Native 程序中如何拉活主程序。

通過 Native 程序拉活主程序的部分程式碼如下,即通過 am 命令進行拉活。通過指定“—include-stopped-packages”引數來拉活主程序處於 forestop 狀態的情況。

挑戰三:如何保證 Native 程序的唯一。

從可擴充套件性和程序唯一等多方面考慮,將 Native 程序設計層 C/S 結構模式,主程序與 Native 程序通過 Localsocket 進行通訊。在Native程序中利用 Localsocket 保證 Native 程序的唯一性,不至於出現建立多個 Native 程序以及 Native 程序變成殭屍程序等問題。

4.4.3. 方案適用範圍

該方案主要適用於 Android5.0 以下版本手機。

該方案不受 forcestop 影響,被強制停止的應用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。

對於 Android5.0 以上手機,系統雖然會將native程序內的所有程序都殺死,這裡其實就是系統“依次”殺死程序時間與拉活邏輯執行時間賽跑的問題,如果可以跑的比系統邏輯快,依然可以有效拉起。記得網上有人做過實驗,該結論是成立的,在某些 Android 5.0 以上機型有效。

4.5. 利用 JobScheduler 機制拉活

4.5.1. 方案設計思想

Android5.0 以後系統對 Native 程序等加強了管理,Native 拉活方式失效。系統在 Android5.0 以上版本提供了 JobScheduler 介面,系統會定時呼叫該程序以使應用進行一些邏輯操作。

在本專案中,我對 JobScheduler 進行了進一步封裝,相容 Android5.0 以下版本。封裝後 JobScheduler 介面的使用如下:


@SuppressLint("NewApi")
public class JobHandleService extends JobService{
    private int kJobId = 0;
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("INFO", "jobService create");

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("INFO", "jobService start");
        scheduleJob(getJobInfo());
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
    }

    @Override
    public boolean onStartJob(JobParameters params) {
        // TODO Auto-generated method stub
        Log.i("INFO", "job start");
//      scheduleJob(getJobInfo());
        boolean isLocalServiceWork = isServiceWork(this, "com.dn.keepliveprocess.LocalService");
        boolean isRemoteServiceWork = isServiceWork(this, "com.dn.keepliveprocess.RemoteService");
//      Log.i("INFO", "localSericeWork:"+isLocalServiceWork);
//      Log.i("INFO", "remoteSericeWork:"+isRemoteServiceWork);
        if(!isLocalServiceWork||
           !isRemoteServiceWork){
           // 這裡使用的遠近雙程序保活
            this.startService(new Intent(this,LocalService.class));
            this.startService(new Intent(this,RemoteService.class));
            Toast.makeText(this, "process start", Toast.LENGTH_SHORT).show();
        }
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.i("INFO", "job stop");
//      Toast.makeText(this, "process stop", Toast.LENGTH_SHORT).show();
        scheduleJob(getJobInfo());
        return true;
    }

    /** Send job to the JobScheduler. */
    public void scheduleJob(JobInfo t) {
        Log.i("INFO", "Scheduling job");
        JobScheduler tm = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
        tm.schedule(t);
    }

    public JobInfo getJobInfo(){
        JobInfo.Builder builder = new JobInfo.Builder(kJobId++, new ComponentName(this, JobHandleService.class));
        builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
        builder.setPersisted(true);
        builder.setRequiresCharging(false);
        builder.setRequiresDeviceIdle(false);
        builder.setPeriodic(10);//間隔時間--週期
        return builder.build();
    }


    /** 
     * 判斷某個服務是否正在執行的方法 
     *  
     * @param mContext 
     * @param serviceName 
     *            是包名+服務的類名(例如:net.loonggg.testbackstage.TestService) 
     * @return true代表正在執行,false代表服務沒有正在執行 
     */  
    public boolean isServiceWork(Context mContext, String serviceName) {  
        boolean isWork = false;  
        ActivityManager myAM = (ActivityManager) mContext  
                .getSystemService(Context.ACTIVITY_SERVICE);  
        List<RunningServiceInfo> myList = myAM.getRunningServices(100);  
        if (myList.size() <= 0) {  
            return false;  
        }  
        for (int i = 0; i < myList.size(); i++) {  
            String mName = myList.get(i).service.getClassName().toString();  
            if (mName.equals(serviceName)) {  
                isWork = true;  
                break;  
            }  
        }  
        return isWork;  
    }  
}

4.5.2. 方案適用範圍

該方案主要適用於 Android5.0 以上版本手機。

該方案在 Android5.0 以上版本中不受 forcestop 影響,被強制停止的應用依然可以被拉活,在 Android5.0 以上版本拉活效果非常好。

僅在小米手機可能會出現有時無法拉活的問題。

4.6. 利用賬號同步機制拉活

4.6.1. 方案設計思想

Android 系統的賬號同步機制會定期同步賬號進行,該方案目的在於利用同步機制進行程序的拉活。新增賬號和設定同步週期 AccountManager 實現方法就是SyncAdapter ,利用系統的Account進行回撥,啟動Service 執行Sync操作。

該方案需要在 AndroidManifest 中定義賬號授權與同步服務。

4.6.2. 方案適用範圍

該方案適用於所有的 Android 版本,包括被 forestop 掉的程序也可以進行拉活。

最新 Android 版本(Android N)中系統好像對賬戶同步這裡做了變動,該方法不再有效。

4.7 利用監聽其它應用的廣播,進行保活

該方案可以靜態註冊在清單檔案中,啟動拉取自己應用的service .

監聽QQ,微信,系統應用,友盟,阿里推送,小米推送等等的廣播,然後把自己啟動了。

5. 其他有效拉活方案

經研究發現還有其他一些系統拉活措施可以使用,但在使用時需要使用者授權,使用者感知比較強烈。

這些方案包括:

利用系統通知管理許可權進行拉活

利用輔助功能拉活,將應用加入廠商或管理軟體白名單。

這些方案需要結合具體產品特性來搞。

上面所有解釋這些方案都是考慮的無 Root 的情況。

其他還有一些技術之外的措施,比如說應用內 Push 通道的選擇:

國外版應用:接入 Google 的 GCM 。

國內版應用:使用信鴿 或者極光推送 或者阿里推送 整合第三方廠商的推送SDK, 比如 信鴿平臺中配置 小米, 華為, 魅族推送,進行廠商平臺相容。 其他部分使用具體推送平臺自己的sdk service 保活機制。

總結

Service的調優與保活最重要的就是適合自己的應用需求, 以上方案總有一款適合自己的。當然要適可而止,不要把自己的應用搞得烏煙瘴氣被使用者反感,卸掉就不好了。

下邊是我使用SyncAdapter 與Native 程序保活方案建立的demo
https://github.com/samuelhehe/KeepAlive 這不是最終方案,可以根據自己的需求進行修改,適配。

參考連結