Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 論Android應用進程長存的可行性

論Android應用進程長存的可行性

編輯:關於Android編程

如何能讓我們的應用能夠在系統後台持續地運行是一個自Android從娘(ma)胎(bi)裡出來時就議論不停的話題,而且這似乎成了一個牛(liu)逼(mang)應用標配的功能。每當有人問起愛哥這個沉重的問題時我都會選擇避而不答,原因有二,一是我並不曾深入地研究過相關功能,二是本人作為一個有情懷的開發者是不提倡讓應用去占用沒必要且吃緊的系統資源。不過最近一個偶然的機會讓我不得不去正視這個問題,那就是在Android中一個應用是否真的能常駐後台呢?這裡愛哥可以先給出答案:可以但又不可以。很多朋友看到這裡一定會暗罵愛哥“你這特麼不是廢話麼”?先表急,等你看完這篇文章後就知道這句話的真(dou)谛(bi)。

什麼才叫後台常駐

大家對應用後台常駐比較普遍的理解是當應用位於後台時不被干掉,退一步說應用位於後台時被干掉後依然能頑強地重新啟動起來,這裡的被干掉也可以簡略地分為兩種情況,第一種是當系統資源緊俏的時候or基於某種系統自身的後台運行規則選擇干掉你的後台應用來獲得更多的資源,第二種是用戶手動調用某些安全軟件的清理功能干掉你的後台應用。對於Android 5.0以前的系統我們可以考慮以上兩種情況下的後台常駐,而對於Android 5.0以及以後的版本我們只能基於第一種情況考慮後台常駐,因為從Android 5.0開始對進程的管理更為嚴格,啥得也更為暴力。

Android是如何干掉你應用的

要想讓應用後台常駐我們還必須先了解一點,那就是Android是如何干掉你的應用的,所謂知己知彼才能百戰百勝。上面我們曾提到Android殺應用實質上是殺進程,正常情況下,每一個Android應用啟動後都會對應一個進程,我們可以在adb shell中通過ps命令查看:

adb shell
ps|grep aigestudio

上述方法意思是列出條目裡含有aigestudio字符的進程:

u0_a68    1202  340   712072 42936    ep_poll f73940c5 S com.aigestudio.daemon

可以看到當我們的應用運行後Android會為我們創建一個用戶ID為u0_a68進程ID為1202父進程ID為340的進程,當系統資源吃緊或者說用戶手動調用某些清理應用時,就會殺掉相應的進程,當然,雖然殺進程這種活對系統來說是一瞬間的事,但是其實質還是按部就班地進行並遵循一定的規則,這裡就不得不提到

Android進程的生命周期

與大家比較熟悉的Activity生命周期相比,Android進程的生命周期實質更為簡單,越核心的東西越簡單嘛,Android將一個進程分為五種不同的狀態:

前台進程 Foreground process

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

某個進程持有一個正在與用戶交互的Activity並且該Activity正處於resume的狀態。 某個進程持有一個Service,並且該Service與用戶正在交互的Activity綁定。 某個進程持有一個Service,並且該Service調用startForeground()方法使之位於前台運行。 某個進程持有一個Service,並且該Service正在執行它的某個生命周期回調方法,比如onCreate()、 onStart()或onDestroy()。 某個進程持有一個BroadcastReceiver,並且該BroadcastReceiver正在執行其onReceive()方法。

可以看到使進程位於前台的方法還是蠻多的,但是你要知道的事Android是一個碎片化非常嚴重的系統,很多定制的ROM都會修改一部分系統邏輯來做所謂的優化,所以說上述的情景以及下述我們將要講到的其它進程狀態其實都只能說可以在原生系統上完美生效而如果在一些定制ROM中則有可能無效甚至出現詭異的現象。

可見進程 Visible process

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

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

服務進程 Service process

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

服務進程 Service process

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

後台進程 Background process

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

空進程 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/1728/oom_adj

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

後台進程常駐的策略與選擇

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

輕量化進程

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



    
        
    

如上所示我們聲明一個services並通過startService的方式啟動它,在這個Service中我們通過一個死循環來不斷Toast一段信息:

package com.aigestudio.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 AigeStudio
 * @since 2016-05-05
 */
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, "AigeStudio" +
                                    System.currentTimeMillis(), Toast.LENGTH_SHORT).show();
                        }
                    });
                    SystemClock.sleep(3000);
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
}

即便我們不斷地彈出Toast,但是因為間隔時間相對來說還算較大,不會使用太多的內存:
這裡寫圖片描述
這個時候我們可以考慮來瞅瞅目前兩個進程的adj值<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> AigeStudio:Android AigeStudio$ adb shell root@vbox86p:/ # ps|grep aigestudio u0_a61 1631 339 1007512 33976 ffffffff f74aa3b5 S com.aigestudio.daemon u0_a61 1658 339 1012640 33884 ffffffff f74aa3b5 S com.aigestudio.daemon:service root@vbox86p:/ # cat /proc/1658/oom_adj 1 root@vbox86p:/ # cat /proc/1631/oom_adj 8

這裡至於為什麼一個是1一個是8大家動動腦子想想也許就明白了。隨著時間的推移進程中的一些對象可能會做緩存導致內存的使用增大,不過只要能被回收就沒有什麼大礙:
這裡寫圖片描述
因此,如果你想在進程中的Service裡處理更復雜的邏輯,務必盡量多地使用弱引用或軟引用,或者說盡量多地去置空一些不必要的引用並在需要的時候再賦值,其次Service本身也提供了onTrimMemory方法來告訴我們系統何時需要釋放掉不必要的資源,靈活使用這類方法可以最大程度的讓我們的後台Service長盛不衰。還是那句話,盡量讓我們的後台進程做更少的事情,及時釋放資源,才是硬道理。

被殺後重啟

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

後台進程常駐的實現

進程提權

我們上面曾說到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的前台顯示會怎樣呢:

/**
 * @author AigeStudio
 * @since 2016-05-05
 */
public class DaemonService extends Service {
    private static boolean sPower = true;

    @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());
            startService(new Intent(this, CancelService.class));
        } else {
            startForeground(250, new Notification());
        }
    }

    @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;
                    }
                    SystemClock.sleep(3000);
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
}
/**
 * @author AigeStudio
 * @since 2016-05-05
 */
public class CancelService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        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);
    }
}

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

AigeStudio:Android AigeStudio$ adb shell
root@vbox86p:/ # ps|grep aigestudio
u0_a61    26788 339   1006480 33824 ffffffff f74aa3b5 S com.aigestudio.daemon
u0_a61    26806 339   994116 24000 ffffffff f74aa3b5 S com.aigestudio.daemon:service
root@vbox86p:/ # cat /proc/26788/oom_adj
8
root@vbox86p:/ # cat /proc/26806/oom_adj                                       
1

是不是很6呢,前段時間就曾有人扒出支付寶曾經以這樣的方式讓自己的後台進程常駐,但是這個方法有個小小的bug,在一些手機上,發送前台通知會喚醒設備並點亮屏幕,這樣會很耗電而且在電量管理界面系統還會統計到你的進程點亮屏幕的次數,不是很好。
除了使Service置為前台顯示來提權外,還有很多不是很實用的方式,比如提升優先級和使用persistent權限等:



    
    

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

死後滿血復活

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

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:

public int onStartCommand(Intent intent, int flags, int startId) {
    onStart(intent, startId);
    return mStartCompatibility ? START_STICKY_COMPATIBILITY : START_STICKY;
}

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

進程守護

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



    
        
        
    

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

/**
 * @author AigeStudio
 * @since 2016-05-05
 */
public class DaemonService extends Service {
    private static boolean sPower = true, 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() {
                    while (sPower) {
                        if (System.currentTimeMillis() >= 123456789000000L) {
                            sPower = false;
                        }
                        Log.d("AigeStudio", "DaemonService");
                        startService(new Intent(DaemonService.this, ProtectService.class));
                        SystemClock.sleep(3000);
                    }
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }
}
/**
 * @author AigeStudio
 * @since 2016-05-05
 */
public class ProtectService extends Service {
    private static boolean sPower = true, 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() {
                    while (sPower) {
                        if (System.currentTimeMillis() >= 123456789000000L) {
                            sPower = false;
                        }
                        SystemClock.sleep(1500);
                        Log.d("AigeStudio", "ProtectService");
                        startService(new Intent(ProtectService.this, DaemonService.class));
                    }
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }
}

在原生系統及相當一部分的ROM下上述方法就已經很有用了,即便應用主進程被用戶在Recent Task中被清理也無妨上述進程的進行,該方法直至Android 6.0也相當有效,但是對於一些深度定制的ROM就顯得很雞肋,比如魅族、小米。
有些時候,我們會使用一個更為純淨的進程來作為守護進程而非借助Service,你可以使用C層fork,也可以直接Java創建一個新的進程,在5.0以前的版本中,這兩種方式創建的進程有一定的區別,不過5.0以後已經變得不再那麼重要了,不依賴Android環境的進程唯一的好處是可以做到更輕量,除此之外並無卵用,這裡以Java為例,使用Java的方式創建進程有兩種途徑,一是通過Runtime;二是通過ProcessBuilder,後者提供了更多的選擇,因此愛哥一般都會選擇後者,使用ProcessBuilder創建進程的過程也很簡單,三部曲:構建環境變量、指定用戶目錄、執行命令:

ProcessBuilder builder = new ProcessBuilder();
Map env = builder.environment();
String classpath = env.get("CLASSPATH");
if (null == classpath)
    classpath = context.getPackageCodePath();
else
    classpath = classpath + ":" + context.getPackageCodePath();
env.put("CLASSPATH", classpath);
builder.directory(new File("/"));
try {
    Process process = builder.command("sh").redirectErrorStream(false).start();
    OutputStream os = process.getOutputStream();
    String cmd = "id\n";
    os.write(cmd.getBytes("utf8"));
    os.flush();
    LogUtil.i("Exec cmd " + cmd);
    cmd = "cd " + FILE.getAbsolutePath() + "\n";
    os.write(cmd.getBytes("utf8"));
    os.flush();
    LogUtil.i("Exec cmd " + cmd);
    cmd = "app_process / " + Daemon.class.getName() + " --nice-name=" + PROCESS + " &\n";
    os.write(cmd.getBytes("utf8"));
    os.flush();
    LogUtil.i("Exec cmd " + cmd);
    os.write("exit\n".getBytes("utf8"));
    os.flush();
    LogUtil.i("Exec cmd " + cmd);
} catch (IOException e) {
    LogUtil.e("Exec cmd with error:" + e.toString());
}
啟動進程後我們只需要在main方法裡輪詢檢查目標進程或者說目標進程中的Service是否存活即可:
public static void main(String[] args) {
    Looper.prepare();
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (sPower) {
                String cmd = String.format("am startservice%s-n com.aigestudio.daemon/" +
                                "com.aigestudio.daemon.services.DaemonService",
                        SysUtil.isAfter17() ? " --user 0 " : " ");
                LogUtil.i("CMD exec " + cmd);
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                }
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    LogUtil.w("Thread sleep failed:" + e.toString());
                }
            }
        }
    }).start();
    Looper.loop();
    LogUtil.i("====================Daemon exit with error====================");
}

這裡我們采用直接調用adb命令的方式啟動Service組件,這種方式有種弊端,因為某些奇葩畸形又可能會限制應用對adb的使用甚至不對應用提供,所以為了保險起見我們最好提供第二種以代碼啟動Service組件的方式:

public static void main(String[] args) {
    Looper.prepare();
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (sPower) {
                String cmd = String.format("am startservice%s-n com.aigestudio.daemon/" +
                                "com.aigestudio.daemon.services.DaemonService",
                        SysUtil.isAfter17() ? " --user 0 " : " ");
                LogUtil.i("CMD exec " + cmd);
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    LogUtil.w("CMD exec failed:" + e.toString());
                    Intent intent = new Intent();
                    ComponentName component = new ComponentName("com.aigestudio.daemon",
                            DaemonService.class.getName());
                    intent.setComponent(component);
                    IActivityManager am = ActivityManagerNative.getDefault();
                    Method method;
                    try {
                        method = am.getClass().getMethod("startService",
                                IApplicationThread.class, Intent.class, String.class,
                                int.class);
                        Object cn = method.invoke(am, null, intent, intent.getType(), 0);
                        LogUtil.i("start service return: " + cn);
                    } catch (NoSuchMethodException ex) {
                        try {
                            method = am.getClass().getMethod("startService",
                                    IApplicationThread.class, Intent.class, String.class);
                            Object cn = method.invoke(am, null, intent, intent.getType());
                            LogUtil.i("start service return: " + cn);
                        } catch (NoSuchMethodException exc) {
                            LogUtil.i("start service method not found: " + exc);
                        } catch (Exception exc) {
                            LogUtil.e("Start service failed:" + exc.toString());
                        }
                    } catch (Exception ex) {
                        LogUtil.e("Start service failed:" + ex.toString());
                    }
                }
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    LogUtil.w("Thread sleep failed:" + e.toString());
                }
            }
        }
    }).start();
    Looper.loop();
    LogUtil.i("====================Daemon exit with error====================");
}

上述方法中你有可能找不到IActivityManager、ActivityManagerNative和IApplicationThread這三個類,因為在SDK中這幾個類是hide的,你需要一個完整的ROM包jar,這個我會另開一篇blog來教大家如何生成完整的jar,好了這裡也差不多了,還有一個問題是,每次我們在Service中輪詢啟動進程時有可能重復啟動,所以在此之前我們還應該在啟動之前做一次判斷進程是否已經被啟動,完整代碼如下:

package com.aigestudio.daemon.core;

import android.app.ActivityManagerNative;
import android.app.IActivityManager;
import android.app.IApplicationThread;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import android.os.Looper;

import com.aigestudio.daemon.services.DaemonService;
import com.aigestudio.daemon.utils.LogUtil;
import com.aigestudio.daemon.utils.SysUtil;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.Map;

public final class Daemon {
    private static final String PROCESS = "com.aigestudio.daemon.process";
    private static boolean sPower = true;
    private static final File FILE =
            new File(new File(Environment.getDataDirectory(), "data"), "com.aigestudio.daemon");

    private Daemon() {
    }

    public static void main(String[] args) {
        Looper.prepare();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (sPower) {
                    String cmd = String.format("am startservice%s-n com.aigestudio.daemon/" +
                                    "com.aigestudio.daemon.services.DaemonService",
                            SysUtil.isAfter17() ? " --user 0 " : " ");
                    LogUtil.i("CMD exec " + cmd);
                    try {
                        Runtime.getRuntime().exec(cmd);
                    } catch (IOException e) {
                        LogUtil.w("CMD exec failed:" + e.toString());
                        Intent intent = new Intent();
                        ComponentName component = new ComponentName("com.aigestudio.daemon",
                                DaemonService.class.getName());
                        intent.setComponent(component);
                        IActivityManager am = ActivityManagerNative.getDefault();
                        Method method;
                        try {
                            method = am.getClass().getMethod("startService",
                                    IApplicationThread.class, Intent.class, String.class,
                                    int.class);
                            Object cn = method.invoke(am, null, intent, intent.getType(), 0);
                            LogUtil.i("start service return: " + cn);
                        } catch (NoSuchMethodException ex) {
                            try {
                                method = am.getClass().getMethod("startService",
                                        IApplicationThread.class, Intent.class, String.class);
                                Object cn = method.invoke(am, null, intent, intent.getType());
                                LogUtil.i("start service return: " + cn);
                            } catch (NoSuchMethodException exc) {
                                LogUtil.i("start service method not found: " + exc);
                            } catch (Exception exc) {
                                LogUtil.e("Start service failed:" + exc.toString());
                            }
                        } catch (Exception ex) {
                            LogUtil.e("Start service failed:" + ex.toString());
                        }
                    }
                    try {
                        Thread.sleep(1500);
                    } catch (InterruptedException e) {
                        LogUtil.w("Thread sleep failed:" + e.toString());
                    }
                }
            }
        }).start();
        Looper.loop();
        LogUtil.i("====================Daemon exit with error====================");
    }

    public static void start(Context context) {
        LogUtil.i("====================Daemon will be start====================");
        File[] processes = new File("/proc").listFiles();
        for (File file : processes) {
            if (file.isDirectory()) {
                File cmd = new File(file, "cmdline");
                if (!cmd.exists())
                    continue;
                try {
                    BufferedReader br = new BufferedReader(new FileReader(cmd));
                    String line = br.readLine();
                    if (null != line && line.startsWith(PROCESS)) {
                        LogUtil.w("Daemon already running");
                        return;
                    }
                    br.close();
                } catch (IOException e) {
                    LogUtil.e("Check daemon running with error:" + e.toString());
                }
            }
        }
        ProcessBuilder builder = new ProcessBuilder();
        Map env = builder.environment();
        String classpath = env.get("CLASSPATH");
        if (null == classpath)
            classpath = context.getPackageCodePath();
        else
            classpath = classpath + ":" + context.getPackageCodePath();
        env.put("CLASSPATH", classpath);
        builder.directory(new File("/"));
        try {
            Process process = builder.command("sh").redirectErrorStream(false).start();
            OutputStream os = process.getOutputStream();
            String cmd = "id\n";
            os.write(cmd.getBytes("utf8"));
            os.flush();
            LogUtil.i("Exec cmd " + cmd);
            cmd = "cd " + FILE.getAbsolutePath() + "\n";
            os.write(cmd.getBytes("utf8"));
            os.flush();
            LogUtil.i("Exec cmd " + cmd);
            cmd = "app_process / " + Daemon.class.getName() + " --nice-name=" + PROCESS + " &\n";
            os.write(cmd.getBytes("utf8"));
            os.flush();
            LogUtil.i("Exec cmd " + cmd);
            os.write("exit\n".getBytes("utf8"));
            os.flush();
            LogUtil.i("Exec cmd " + cmd);
        } catch (IOException e) {
            LogUtil.e("Exec cmd with error:" + e.toString());
        }
    }
}

上述的方式可以在很大程度上在你進程被殺後拉起你的進程,大家需要注意的是,上述代碼中我設置的間隔時間分別為3000ms和1500ms,事實上你可以根據具體的業務處理調整該值。進程的被殺總是有個先後順序,不存在一下子多個進程同時被干掉的情況,除非系統崩潰,所以理論上來說輪詢間隔時間越小越容易在雙方都被殺死前喚醒對方,但是業務邏輯本身就復雜的話,建議還是不要將該值設置太小,否則對系統來說是一種負擔,同時也會使你的進程更容易更頻繁地讓系統殺死。

Receiver觸發

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

AlarmManager or JobScheduler循環觸發

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

PendingIntent intent = PendingIntent.getService(this, 0x123,
        new Intent(this, DaemonService.class), PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, AlarmManager.INTERVAL_HALF_HOUR,
        AlarmManager.INTERVAL_HALF_HOUR, intent);

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

/**
 * @author AigeStudio
 * @since 2016-05-05
 */
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)) {
                // 省去幾行代碼……

            } else if (Intent.ACTION_UID_REMOVED.equals(action)) {
                // 省去幾行代碼……

            } else {
                // 省去幾行代碼……

                Uri data = intent.getData();
                if (data != null) {
                    String pkg = data.getSchemeSpecificPart();
                    if (pkg != null) {
                        pkgList = new String[]{pkg};
                    }
                }
            }
            if (pkgList != null && (pkgList.length > 0)) {
                for (String pkg : pkgList) {
                    removeLocked(pkg);

                    // 省去幾行代碼……
                }
            }
        }
    }
}

從上述的源碼來看當該廣播接收者收到ACTION_PACKAGE_RESTARTED廣播時會執行removeLocked這個方法,這個方法就像它的名字那樣會移除掉與應用相關的Alarm並刷新Alarm的狀態:

void removeLocked(String packageName) {
    boolean didRemove = false;
    for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
        Batch b = mAlarmBatches.get(i);
        didRemove |= b.remove(packageName);
        if (b.size() == 0) {
            mAlarmBatches.remove(i);
        }
    }
    for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
        if (mPendingWhileIdleAlarms.get(i).operation.getTargetPackage().equals(packageName)) {
            mPendingWhileIdleAlarms.remove(i);
        }
    }
    if (didRemove) {
        rebatchAllAlarmsLocked(true);
        rescheduleKernelAlarmsLocked();
        updateNextAlarmClockLocked();
    }
}

因此,對於某些手機可以在清理應用時將其置為STOPPED狀態而言,即便設置AlarmManager也是沒卵用的。
與AlarmManager類似的在5.0新鮮出爐的JobScheduler相較而言要比AlarmManager好一些,鑒於兩者使用原理類似,這裡就不再逼逼了。

與系統Service捆綁

Android系統提供給我們一系列的Service,注意這裡我們所指的系統Service並非“SystemService”提供的那些玩意,而是類似於系統廣播的便於我們使用的Service,常見常用的就是IntentService,當然還有其它更多更不常用的系統Service,那麼為什麼愛哥要在這裡提到這玩意呢?因為某些系統Service一旦綁定就像擁有開了掛一樣的權限,這在大部分機型包括某些深度定制系統上簡直就像BUG般存在,以最BUG的NotificationListenerService為例,大家可能很少會用到這玩意,這玩意是用來讀取通知的,也就是說只要是通知不管你誰發的,NotificationListenerService都可以檢測到,使用它也很簡單,和IntentService一樣定義一個類繼承一下即可:

package com.aigestudio.daemon.core;

import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;

/**
 * @author AigeStudio
 * @since 2016-05-05
 */
public class DService extends NotificationListenerService {
    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
    }
}

裡面什麼邏輯都不用實現,是的你沒聽錯,什麼邏輯都不需要,然後在AndroidManifest中聲明權限:



    
        
            
                
            
        
    

這裡為了區別主進程,我將該Service置於一個單獨的進程中,然後啟動應用,注意,這裡我們的應用什麼邏輯都沒有,剔除掉上面所做的所有有關進程保護的邏輯,運行之後你發現看不到你NotificationListenerService所在的進程:

AigeStudio:Android AigeStudio$ adb shell
root@vbox86p:/ # ps|grep aigestudio
u0_a61    9513  339   1002012 30452 ffffffff f74aa3b5 S com.aigestudio.daemon

先別急,NotificationListenerService是個特殊的系統Service,需要非常特別的權限,需要你手動在“設置-提示音和通知-通知使用權限”中打開,注意這個“通知使用權限”選項,如果你設備裡沒有需要使用通知使用權限換句話說就是沒有含有NotificationListenerService的應用的話,這個設置選項是不可見的:
這裡寫圖片描述
這時我們勾選我們的應用,會彈出一個提示框:
這裡寫圖片描述
所以,你想好如何騙你的用戶勾選這個勾勾了麼,一旦勾上,一發不可收拾,這時你就會看到我們的進程啟動起來了:

root@vbox86p:/ # ps|grep aigestudio                                            
u0_a61    9513  339   1003044 30532 ffffffff f74aa3b5 S com.aigestudio.daemon
u0_a61    12869 339   993080 23792 ffffffff f74aa3b5 S com.aigestudio.daemon:service

好了,這時候,見證奇跡的時候來了,不管是某米、某族還是某某,請嘗試下它們的一鍵清理,你會發現不管怎麼殺,我們的進程都還在,除了一小部分名不經傳的手機因為修改系統邏輯將其殺死外,絕大部分手機都不會殺掉該進程,為什麼呢?好事的朋友一定會去check該進程的adj值:

root@vbox86p:/ # ps|grep aigestudio                                            
u0_a61    12869 339   993080 23792 ffffffff f74aa3b5 S com.aigestudio.daemon:service
root@vbox86p:/ # cat /proc/12869/oom_adj
0

你會發現我們的進程被置為前台進程了,而且不僅僅是這樣哦,即便你重啟設備開機,它也會首先被啟動,因為其內部邏輯會使其在系統啟動時綁定並開始監聽通知,當然我們這裡並沒有任何關於通知的邏輯,那麼你可能會問愛哥這又有什麼用呢?我們又不能在NotificationListenerService裡處理與通知不相關的邏輯,沒錯,是這樣,但是我們也沒必要啊,我們只需新建一個Service並使其與NotificationListenerService在同一進程下,那麼我們的這個Sefvice不就一樣不死了嗎:



    
        
        
            
                
            
        
    

這種方式唯一的一個缺點就是你需要欺騙用戶手動去開啟通知權限,需要給用戶一個合理的理由,所以對於跟通知權限根本不沾邊的應用想想還是算了吧。
與NotificationListenerService有點相像的是5.0新推出的與JobScheduler相關的JobService,不過其BUG程度遠不及NotificationListenerService,這裡不再多說了,大家感興趣的話可以去試試。關於NotificationListenerService我有太多話想說,愛哥發現在5.0及以上的系統上它內部邏輯很奇特,與其它的系統Service有很大的區別,而且網上很少關於它的介紹,即便有也是很老舊的源碼上分析的,有空愛哥單獨開一篇文章來好好捋捋它。

so注入系統進程

這種方式去做進程常駐需要獲得root權限,而且不容易控制,愛哥嘗試了多種不同的方法去注入提權,都不穩定,不過曾經有朋友問到愛哥,既然誠心誠意的發問了,愛哥就大發慈悲的回答各位,為了防止系統被破壞,為了保護系統生態的和平,貫徹愛與真實的邪惡,可愛又迷人的正義使者愛哥猶如穿梭在銀河中的火……這種方式還是不教大家了,畢竟技術是把雙刃劍。

KFC外帶全家桶

以全家桶的方式去相互喚醒相互拉活是目前來說最穩定最安全的方式,各大牛逼點的應用都有類似行為,當然對於很多小應用來說,沒有BAT那樣的實力,不過你依然可以使用一些第三方的網絡服務,比如XX推送,一旦設備上的某應用通過XX的推送獲得消息時,是可以間接喚醒其它應用並推送與其相關的消息的。好了,就先扯這麼多,敲一晚上我也是累了,該睡了。

讓應用常駐後台的意義與權衡

作為一個有情懷的開發者,我們都知道每當用戶關閉一個程序時,我們的程序就應該徹底地死去並釋放其所占用的系統資源,這個淺顯的道理不僅適用於我們移動應用開發,也適用於任何桌面程序的開發,但是相對於一些程序而言,總想在用戶關閉程序後還保持一個後台程序來處理一些所謂的“不見得光的任務”,對於一些必要的系統服務而言,這些後台程序是必須的可以理解的,但對於普通的程序來說,保持一個後台程序就顯得有點莫名其妙了,舉個例子,某文檔編輯器總會在關閉後在後台一直保持一個叫“rwcount”的進程,從字面上分析以及實際的測試來看目測是做讀寫計數統計的,然而我都沒有在編輯文檔並退出程序了還統計個雞毛。因此,每當我們想在我們的應用中加入後台進程常駐的功能時,我們是否應該多考慮權衡下是否真的需要後台常駐呢?再舉個例子,假如我們開發一款新聞客戶端應用,你說你需要讓其擁有一個後台常駐的Service,那麼這個後台常駐的Service常駐的目的是什麼呢?如果是為了Update一些數據or處理一些必要的邏輯,是否可以考慮在相關邏輯處理完畢後就釋放掉資源而不必常駐呢?當然愛哥知道有一些應用都有實時上報數據統計的需求,甚至有些應用為了能夠讓上報的服務存活而想出各式各樣的怪招,這個下面會講,在這裡,愛哥很好奇的是對於這些統計數據真的需要實時上報這麼BT?當然愛哥知道對於一小撮應用而言,每分鐘的統計數據都可能對結果產生很大的影響,但是這部分應用所占比例真的很小很小,小到完全可以忽略不計,而對於其他的一些有所謂的“實時上報”需求的應用,大多數都是閒的蛋疼,愛哥接觸過這方面做得最好的應用其實現邏輯是僅會在設備充電且WIFI連接且應用存活的狀態下才會做數據上報,為什麼我會這麼清楚,因為是我寫的……而且該應用的日活目前已接近1700W,所以說,當你確定需要應用有進程常駐後台時,一定要三思是否真的需要,還是可以做些變通?不過話說回來要是你不幸遇到那種敲了三兩行代碼就覺得自己很懂開發網上扒了兩張UI圖就覺得自己很會設計的上司,愛哥也只能默哀了。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved