1. 程式人生 > >Android程序保活(最新)帶你淺析這幾種可行性的保活方案

Android程序保活(最新)帶你淺析這幾種可行性的保活方案

1.概述

  據前人驗證,在沒有白名單的情況下,安卓系統要做一個任何情況下都不被殺死的應用是基本不可能的,但是我們可以做到應用基本不被殺死,如果殺死可以立即復活.經過上網查詢,程序常駐的方案眾說紛紜,但是很多的方案都是不靠譜的或不是最好的,結合很多資料,今天總結一下Android程序保活的一些可行方法.

2.問題

  系統為什麼會殺掉程序,殺的為什麼是我們的程序,這是根據什麼規則來決定的,是一次性幹掉多個程序,還是一個接著一個殺掉?保活套路一堆,如何進行程序保活才是比較恰當......

3.分析

  3.1程序的劃分

    Android中的程序也是有著嚴格的等級,分了三流九等,Android系統把程序劃為瞭如下幾種(重要性從高到低):

    程序的優先順序

3.1.1. 前臺程序 —— Foreground process

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

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

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

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

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

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

3.1.2. 可見程序 —— Visible process

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

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

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

3.1.3. 服務程序 —— Service process

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

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

3.1.4. 後臺程序 —— Background process

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

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

3.1.5. 空程序 —— Empty process

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

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

  具體地,活動程序指的就是使用者正在操作的程式,是前臺程序,可以看到且能夠操作;可見程序就是看得見摸不著的,不能直接操作的程序;服務程序是沒有介面的一直在後臺工作的程序,優先順序不高,當系統記憶體不足時會被殺死,再次充裕的時候會再次開啟;後臺程序就是使用者按了"back"或者"home"後,程式本身看不到了,但是其實還在執行的程式,比如Activity呼叫了onPause方法系統可能隨時終止它們,回收記憶體.空程序:某個程序不包含任何活躍的元件時該程序就會被置為空程序,完全沒用,殺了它只有好處沒壞處,第一個被處理!

    3.2記憶體閾值

  程序是怎麼被殺的呢?系統出於體驗和效能上的考慮,app在退到後臺時系統並不會真正的kill掉這個程序,而是將其快取起來。開啟的應用越多,後臺快取的程序也越多。在系統記憶體不足的情況下,系統開始依據自身的一套程序回收機制來判斷要kill掉哪些程序,以騰出記憶體來供給需要的app, 這套殺程序回收記憶體的機制就叫 Low Memory Killer。那這個不足怎麼來規定呢,那就是記憶體閾值,我們可以使用cat /sys/module/lowmemorykiller/parameters/minfree來檢視某個手機的記憶體閾值。

注意這些數字的單位是page. 1 page = 4 kb.上面的六個數字對應的就是(MB): 72,90,108,126,144,180,這些數字也就是對應的記憶體閥值,記憶體閾值在不同的手機上不一樣,一旦低於該值,Android便開始按順序關閉程序. 因此Android開始結束優先順序最低的空程序,即當可用記憶體小於180MB(46080*4/1024)。

  程序是有它的優先順序的,這個優先順序通過程序的adj值來反映,它是linux核心分配給每個系統程序的一個值,代表程序的優先順序,程序回收機制就是根據這個優先順序來決定是否進行回收,adj值定義在com.android.server.am.ProcessList類中,這個類路徑是${android-sdk-path}\sources\android-23\com\android\server\am\ProcessList.java。oom_adj的值越小,程序的優先順序越高,普通程序oom_adj值是大於等於0的,而系統程序oom_adj的值是小於0的,我們可以通過cat /proc/程序id/oom_adj可以看到當前程序的adj值。

  也就是說,oom_adj越大,佔用實體記憶體越多會被最先kill掉,OK,那麼現在對於程序如何保活這個問題就轉化成,如何降低oom_adj的值,以及如何使得我們應用佔的記憶體最少。

方案1

開啟一個畫素的Activity

  據說這個是手Q的程序保活方案,基本思想,系統一般是不會殺死前臺程序的。所以要使得程序常駐,我們只需要在鎖屏的時候在本程序開啟一個Activity,為了欺騙使用者,讓這個Activity的大小是1畫素,並且透明無切換動畫,在開螢幕的時候,把這個Activity關閉掉,所以這個就需要監聽系統鎖屏廣播.

public class SinglePixelActivity extends Activity {

    public static final String TAG = SinglePixelActivity.class.getSimpleName();

    public static void actionToSinglePixelActivity(Context pContext) {
        Intent intent = new Intent(pContext, SinglePixelActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        pContext.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        setContentView(R.layout.activity_singlepixel);
        Window window = getWindow();
        //放在左上角
        window.setGravity(Gravity.START | Gravity.TOP);
        WindowManager.LayoutParams attributes = window.getAttributes();
        //寬高設計為1個畫素
        attributes.width = 1;
        attributes.height = 1;
        //起始座標
        attributes.x = 0;
        attributes.y = 0;
        window.setAttributes(attributes);

        ScreenManager.getInstance(this).setActivity(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}
在螢幕關閉的時候把SinglePixelActivity啟動起來,在開屏的時候把SinglePixelActivity 關閉掉,所以要監聽系統鎖屏廣播,以介面的形式通知MainActivity啟動或者關閉SinglePixActivity。
public class ScreenBroadcastListener {

    private Context mContext;

    private ScreenBroadcastReceiver mScreenReceiver;

    private ScreenStateListener mListener;

    public ScreenBroadcastListener(Context context) {
        mContext = context.getApplicationContext();
        mScreenReceiver = new ScreenBroadcastReceiver();
    }

    interface ScreenStateListener {

        void onScreenOn();

        void onScreenOff();
    }

    /**
     * screen狀態廣播接收者
     */
    private class ScreenBroadcastReceiver extends BroadcastReceiver {
        private String action = null;

        @Override
        public void onReceive(Context context, Intent intent) {
            action = intent.getAction();
            if (Intent.ACTION_SCREEN_ON.equals(action)) { // 開屏
                mListener.onScreenOn();
            } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // 鎖屏
                mListener.onScreenOff();
            }
        }
    }

    public void registerListener(ScreenStateListener listener) {
        mListener = listener;
        registerListener();
    }

    private void registerListener() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        mContext.registerReceiver(mScreenReceiver, filter);
    }
}
public class ScreenManager {

    private Context mContext;

    private WeakReference<Activity> mActivityWref;

    public static ScreenManager gDefualt;

    public static ScreenManager getInstance(Context pContext) {
        if (gDefualt == null) {
            gDefualt = new ScreenManager(pContext.getApplicationContext());
        }
        return gDefualt;
    }
    private ScreenManager(Context pContext) {
        this.mContext = pContext;
    }

    public void setActivity(Activity pActivity) {
        mActivityWref = new WeakReference<Activity>(pActivity);
    }

    public void startActivity() {
            SinglePixelActivity.actionToSinglePixelActivity(mContext);
    }

    public void finishActivity() {
        //結束掉SinglePixelActivity
        if (mActivityWref != null) {
            Activity activity = mActivityWref.get();
            if (activity != null) {
                activity.finish();
            }
        }
    }
}

現在MainActivity改成如下

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final ScreenManager screenManager = ScreenManager.getInstance(MainActivity.this);
        ScreenBroadcastListener listener = new ScreenBroadcastListener(this);
         listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() {
            @Override
            public void onScreenOn() {
                screenManager.finishActivity();
            }

            @Override
            public void onScreenOff() {
                screenManager.startActivity();
            }
        });
    }
}

按下back之後,進行鎖屏,現在測試一下oom_adj的值

果然將程序的優先順序提高了。

方案2

據說這個微信也用過的程序保活方案,該方案實際利用了Android前臺service的漏洞。
原理如下
對於 API level < 18 :呼叫startForeground(ID, new Notification()),傳送空的Notification ,圖示則不會顯示。
對於 API level >= 18:在需要提優先順序的service A啟動一個InnerService,兩個服務同時startForeground,且繫結同樣的 ID。Stop 掉InnerService ,這樣通知欄圖示即被移除。

public class KeepLiveService extends Service {

    public static final int NOTIFICATION_ID=0x11;

    public KeepLiveService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();
         //API 18以下,直接傳送Notification並將其置為前臺
        if (Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN_MR2) {
            startForeground(NOTIFICATION_ID, new Notification());
        } else {
            //API 18以上,傳送Notification並將其置為前臺後,啟動InnerService
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(NOTIFICATION_ID, builder.build());
            startService(new Intent(this, InnerService.class));
        }
    }

    public  static class  InnerService extends Service{
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
        @Override
        public void onCreate() {
            super.onCreate();
            //傳送與KeepLiveService中ID相同的Notification,然後將其取消並取消自己的前臺顯示
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(NOTIFICATION_ID, builder.build());
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    stopForeground(true);
                    NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                    manager.cancel(NOTIFICATION_ID);
                    stopSelf();
                }
            },100);

        }
    }
}

在沒有采取前臺服務之前,啟動應用,oom_adj值是0,按下返回鍵之後,變成9(不同ROM可能不一樣)

在採取前臺服務之後,啟動應用,oom_adj值是0,按下返回鍵之後,變成2(不同ROM可能不一樣),確實程序的優先順序有所提高。

方案3

程序相互喚醒

  顧名思義,就是指的不同程序,不同app之間互相喚醒,如你手機裡裝了支付寶、淘寶、天貓、UC等阿里系的app,那麼你開啟任意一個阿里系的app後,有可能就順便把其他阿里系的app給喚醒了。

方案4

JobSheduler

JobSheduler是作為程序死後復活的一種手段,native程序方式最大缺點是費電, Native 程序費電的原因是感知主程序是否存活有兩種實現方式,在 Native 程序中通過死迴圈或定時器,輪訓判斷主程序是否存活,當主程序不存活時進行拉活。其次5.0以上系統不支援。 但是JobSheduler可以替代在Android5.0以上native程序方式,這種方式即使使用者強制關閉,也能被拉起來,親測可行。

  [email protected](Build.VERSION_CODES.LOLLIPOP)
public class MyJobService extends JobService {
    @Override
    public void onCreate() {
        super.onCreate();
        startJobSheduler();
    }

    public void startJobSheduler() {
        try {
            JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), MyJobService.class.getName()));
            builder.setPeriodic(5);
            builder.setPersisted(true);
            JobScheduler jobScheduler = (JobScheduler) this.getSystemService(Context.JOB_SCHEDULER_SERVICE);
            jobScheduler.schedule(builder.build());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters jobParameters) {
        return false;
    }
}

這個是系統自帶的,onStartCommand方法必須具有一個整形的返回值,這個整形的返回值用來告訴系統在服務啟動完畢後,如果被Kill,系統將如何操作,這種方案雖然可以,但是在某些情況or某些定製ROM上可能失效,認為可以多做一種保保守方案。

@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,NotificationListenerService就是一個監聽通知的服務,只要手機收到了通知,NotificationListenerService都能監聽到,即時使用者把程序殺死,也能重啟,所以說要是把這個服務放到我們的程序之中,那麼就得勁了

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class LiveService extends NotificationListenerService {

    public LiveService() {

    }

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
    }

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

但是這種方式需要許可權

  <service
            android:name=".LiveService"
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService" />
            </intent-filter>
        </service>

所以你的應用要是有訊息推送的話,那麼可以用這種方式去欺騙使用者。

總結

  多種保活方式,沒有說哪一種最好,只有是在什麼場景下,使用哪一種最合適;當然,這些方式不是我發明或發現的,但是我覺得如果不知道的好好了解一下,對自己會有很大的幫助.掌握一些程序保活的手段,這不是耍流氓,是很多場景如果要想為使用者服務,就必須有一個程序常駐,以便在特定的時候做特定的事情。誠然,但凡程序常駐記憶體,無論怎樣優化,都會或多或少的增加一些額外的效能開支,在為使用者最負責任的服務,最高品質的體現我們的價值的前提下,我們要儘可能減少記憶體和電量的消耗。

親愛的讀者,如果此貼對您有幫助,請您動下發財的貴手幫忙右上角【點贊】支援下,非常感謝!